@lessonkit/cli 0.9.1 → 0.9.3

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/bin.js CHANGED
@@ -5,6 +5,7 @@ import { createRequire } from "module";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/init.ts
8
+ import { slugifyId } from "@lessonkit/core";
8
9
  import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
9
10
  import { existsSync } from "fs";
10
11
  import { basename, dirname, join, resolve } from "path";
@@ -92,10 +93,14 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".gi
92
93
  var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
93
94
  function getTemplateDir() {
94
95
  const thisDir = dirname(fileURLToPath(import.meta.url));
95
- return resolve(thisDir, "../../template/vite-react");
96
- }
97
- function slugifyName(name) {
98
- return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "my-course";
96
+ const candidates = [
97
+ resolve(thisDir, "../template/vite-react"),
98
+ resolve(thisDir, "../../template/vite-react")
99
+ ];
100
+ for (const candidate of candidates) {
101
+ if (existsSync(candidate)) return candidate;
102
+ }
103
+ return candidates[0];
99
104
  }
100
105
  async function isDirEmpty(dir) {
101
106
  if (!existsSync(dir)) return true;
@@ -108,7 +113,7 @@ async function isDirEmptyOrDotfilesOnly(dir) {
108
113
  return entries.every((name) => name.startsWith("."));
109
114
  }
110
115
  function escapeJsxString(value) {
111
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
116
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\r\n|\n|\r/g, "\\n");
112
117
  }
113
118
  async function copyTemplate(src, dest) {
114
119
  await mkdir(dest, { recursive: true });
@@ -146,14 +151,14 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
146
151
  }
147
152
  async function runInit(opts, logger) {
148
153
  const cwd = process.cwd();
149
- const rawName = opts.name ?? (opts.here ? slugifyName(basename(process.cwd()) || "my-course") : void 0);
154
+ const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
150
155
  if (!rawName && !opts.here) {
151
156
  throw new CliError("Project name is required. Usage: lessonkit init <name> or lessonkit init --here", {
152
157
  code: "INVALID_PROJECT",
153
158
  exitCode: EXIT_INVALID_PROJECT
154
159
  });
155
160
  }
156
- const slug = slugifyName(rawName ?? "my-course");
161
+ const slug = slugifyId(rawName ?? "my-course");
157
162
  const projectName = rawName ?? slug;
158
163
  const projectDir = opts.here ? cwd : resolve(cwd, slug);
159
164
  if (!opts.here && existsSync(projectDir)) {
@@ -276,6 +281,19 @@ async function loadLessonkitJson(projectRoot) {
276
281
  exitCode: EXIT_INVALID_PROJECT
277
282
  });
278
283
  }
284
+ const courseObj = courseRaw;
285
+ if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
286
+ throw new CliError(`${configPath}: "course.lessons" must be an array.`, {
287
+ code: "INVALID_PROJECT",
288
+ exitCode: EXIT_INVALID_PROJECT
289
+ });
290
+ }
291
+ if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
292
+ throw new CliError(`${configPath}: "course.assessments" must be an array.`, {
293
+ code: "INVALID_PROJECT",
294
+ exitCode: EXIT_INVALID_PROJECT
295
+ });
296
+ }
279
297
  const validation = validateDescriptor(courseRaw);
280
298
  if (!validation.ok) {
281
299
  throw new CliError(`${configPath}: invalid course descriptor.`, {
@@ -295,11 +313,48 @@ async function loadLessonkitJson(projectRoot) {
295
313
  }
296
314
  const pathsRaw = config.paths;
297
315
  const paths = { ...DEFAULT_PATHS };
316
+ if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
317
+ throw new CliError(`${configPath}: "paths" must be an object.`, {
318
+ code: "INVALID_PROJECT",
319
+ exitCode: EXIT_INVALID_PROJECT
320
+ });
321
+ }
298
322
  if (pathsRaw && typeof pathsRaw === "object") {
299
323
  const p = pathsRaw;
300
- if (typeof p.spaDistDir === "string" && p.spaDistDir.trim()) paths.spaDistDir = p.spaDistDir;
301
- if (typeof p.lxpackOutDir === "string" && p.lxpackOutDir.trim()) paths.lxpackOutDir = p.lxpackOutDir;
302
- if (typeof p.outputBaseDir === "string" && p.outputBaseDir.trim()) paths.outputBaseDir = p.outputBaseDir;
324
+ if (p.spaDistDir !== void 0) {
325
+ if (typeof p.spaDistDir !== "string" || !p.spaDistDir.trim()) {
326
+ throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
327
+ code: "INVALID_PROJECT",
328
+ exitCode: EXIT_INVALID_PROJECT
329
+ });
330
+ }
331
+ paths.spaDistDir = p.spaDistDir;
332
+ }
333
+ if (p.lxpackOutDir !== void 0) {
334
+ if (typeof p.lxpackOutDir !== "string" || !p.lxpackOutDir.trim()) {
335
+ throw new CliError(`${configPath}: "paths.lxpackOutDir" must be a non-empty string.`, {
336
+ code: "INVALID_PROJECT",
337
+ exitCode: EXIT_INVALID_PROJECT
338
+ });
339
+ }
340
+ paths.lxpackOutDir = p.lxpackOutDir;
341
+ }
342
+ if (p.outputBaseDir !== void 0) {
343
+ if (typeof p.outputBaseDir !== "string" || !p.outputBaseDir.trim()) {
344
+ throw new CliError(`${configPath}: "paths.outputBaseDir" must be a non-empty string.`, {
345
+ code: "INVALID_PROJECT",
346
+ exitCode: EXIT_INVALID_PROJECT
347
+ });
348
+ }
349
+ paths.outputBaseDir = p.outputBaseDir;
350
+ }
351
+ }
352
+ const courseSpaDistDir = validation.descriptor.spaDistDir?.trim();
353
+ if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
354
+ throw new CliError(
355
+ `${configPath}: "course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`,
356
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
357
+ );
303
358
  }
304
359
  const pathIssues = validateProjectPaths(projectRoot, paths);
305
360
  if (pathIssues.length) {
@@ -380,8 +435,13 @@ function resolveLxpackOutDir(project) {
380
435
  function resolvePackageOutput(project, target, override) {
381
436
  const outputBaseDir = project.paths.outputBaseDir;
382
437
  if (override) {
383
- const resolved = resolveSafePackageOutputOverride(project.root, override);
384
- return { output: resolved, dir: target === "standalone", outputBaseDir };
438
+ try {
439
+ const resolved = resolveSafePackageOutputOverride(project.root, override);
440
+ return { output: resolved, dir: target === "standalone", outputBaseDir };
441
+ } catch (err) {
442
+ const message = err instanceof Error ? err.message : String(err);
443
+ throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
444
+ }
385
445
  }
386
446
  if (target === "standalone") {
387
447
  return { output: `${outputBaseDir}/standalone`, dir: true, outputBaseDir };
@@ -458,7 +518,7 @@ async function runPackage(opts) {
458
518
  }
459
519
  if (!existsSync3(distDir)) {
460
520
  throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
461
- code: "RUNTIME",
521
+ code: "INVALID_PROJECT",
462
522
  exitCode: EXIT_INVALID_PROJECT
463
523
  });
464
524
  }
@@ -480,6 +540,7 @@ async function runPackage(opts) {
480
540
  descriptor: project.course,
481
541
  outDir,
482
542
  spaDistDir: distDir,
543
+ projectRoot: project.root,
483
544
  target,
484
545
  output,
485
546
  dir,
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from "module";
3
3
  import { Command } from "commander";
4
4
 
5
5
  // src/commands/init.ts
6
+ import { slugifyId } from "@lessonkit/core";
6
7
  import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
7
8
  import { existsSync } from "fs";
8
9
  import { basename, dirname, join, resolve } from "path";
@@ -90,10 +91,14 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".gi
90
91
  var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
91
92
  function getTemplateDir() {
92
93
  const thisDir = dirname(fileURLToPath(import.meta.url));
93
- return resolve(thisDir, "../../template/vite-react");
94
- }
95
- function slugifyName(name) {
96
- return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "my-course";
94
+ const candidates = [
95
+ resolve(thisDir, "../template/vite-react"),
96
+ resolve(thisDir, "../../template/vite-react")
97
+ ];
98
+ for (const candidate of candidates) {
99
+ if (existsSync(candidate)) return candidate;
100
+ }
101
+ return candidates[0];
97
102
  }
98
103
  async function isDirEmpty(dir) {
99
104
  if (!existsSync(dir)) return true;
@@ -106,7 +111,7 @@ async function isDirEmptyOrDotfilesOnly(dir) {
106
111
  return entries.every((name) => name.startsWith("."));
107
112
  }
108
113
  function escapeJsxString(value) {
109
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
114
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\r\n|\n|\r/g, "\\n");
110
115
  }
111
116
  async function copyTemplate(src, dest) {
112
117
  await mkdir(dest, { recursive: true });
@@ -144,14 +149,14 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
144
149
  }
145
150
  async function runInit(opts, logger) {
146
151
  const cwd = process.cwd();
147
- const rawName = opts.name ?? (opts.here ? slugifyName(basename(process.cwd()) || "my-course") : void 0);
152
+ const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
148
153
  if (!rawName && !opts.here) {
149
154
  throw new CliError("Project name is required. Usage: lessonkit init <name> or lessonkit init --here", {
150
155
  code: "INVALID_PROJECT",
151
156
  exitCode: EXIT_INVALID_PROJECT
152
157
  });
153
158
  }
154
- const slug = slugifyName(rawName ?? "my-course");
159
+ const slug = slugifyId(rawName ?? "my-course");
155
160
  const projectName = rawName ?? slug;
156
161
  const projectDir = opts.here ? cwd : resolve(cwd, slug);
157
162
  if (!opts.here && existsSync(projectDir)) {
@@ -274,6 +279,19 @@ async function loadLessonkitJson(projectRoot) {
274
279
  exitCode: EXIT_INVALID_PROJECT
275
280
  });
276
281
  }
282
+ const courseObj = courseRaw;
283
+ if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
284
+ throw new CliError(`${configPath}: "course.lessons" must be an array.`, {
285
+ code: "INVALID_PROJECT",
286
+ exitCode: EXIT_INVALID_PROJECT
287
+ });
288
+ }
289
+ if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
290
+ throw new CliError(`${configPath}: "course.assessments" must be an array.`, {
291
+ code: "INVALID_PROJECT",
292
+ exitCode: EXIT_INVALID_PROJECT
293
+ });
294
+ }
277
295
  const validation = validateDescriptor(courseRaw);
278
296
  if (!validation.ok) {
279
297
  throw new CliError(`${configPath}: invalid course descriptor.`, {
@@ -293,11 +311,48 @@ async function loadLessonkitJson(projectRoot) {
293
311
  }
294
312
  const pathsRaw = config.paths;
295
313
  const paths = { ...DEFAULT_PATHS };
314
+ if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
315
+ throw new CliError(`${configPath}: "paths" must be an object.`, {
316
+ code: "INVALID_PROJECT",
317
+ exitCode: EXIT_INVALID_PROJECT
318
+ });
319
+ }
296
320
  if (pathsRaw && typeof pathsRaw === "object") {
297
321
  const p = pathsRaw;
298
- if (typeof p.spaDistDir === "string" && p.spaDistDir.trim()) paths.spaDistDir = p.spaDistDir;
299
- if (typeof p.lxpackOutDir === "string" && p.lxpackOutDir.trim()) paths.lxpackOutDir = p.lxpackOutDir;
300
- if (typeof p.outputBaseDir === "string" && p.outputBaseDir.trim()) paths.outputBaseDir = p.outputBaseDir;
322
+ if (p.spaDistDir !== void 0) {
323
+ if (typeof p.spaDistDir !== "string" || !p.spaDistDir.trim()) {
324
+ throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
325
+ code: "INVALID_PROJECT",
326
+ exitCode: EXIT_INVALID_PROJECT
327
+ });
328
+ }
329
+ paths.spaDistDir = p.spaDistDir;
330
+ }
331
+ if (p.lxpackOutDir !== void 0) {
332
+ if (typeof p.lxpackOutDir !== "string" || !p.lxpackOutDir.trim()) {
333
+ throw new CliError(`${configPath}: "paths.lxpackOutDir" must be a non-empty string.`, {
334
+ code: "INVALID_PROJECT",
335
+ exitCode: EXIT_INVALID_PROJECT
336
+ });
337
+ }
338
+ paths.lxpackOutDir = p.lxpackOutDir;
339
+ }
340
+ if (p.outputBaseDir !== void 0) {
341
+ if (typeof p.outputBaseDir !== "string" || !p.outputBaseDir.trim()) {
342
+ throw new CliError(`${configPath}: "paths.outputBaseDir" must be a non-empty string.`, {
343
+ code: "INVALID_PROJECT",
344
+ exitCode: EXIT_INVALID_PROJECT
345
+ });
346
+ }
347
+ paths.outputBaseDir = p.outputBaseDir;
348
+ }
349
+ }
350
+ const courseSpaDistDir = validation.descriptor.spaDistDir?.trim();
351
+ if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
352
+ throw new CliError(
353
+ `${configPath}: "course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`,
354
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
355
+ );
301
356
  }
302
357
  const pathIssues = validateProjectPaths(projectRoot, paths);
303
358
  if (pathIssues.length) {
@@ -378,8 +433,13 @@ function resolveLxpackOutDir(project) {
378
433
  function resolvePackageOutput(project, target, override) {
379
434
  const outputBaseDir = project.paths.outputBaseDir;
380
435
  if (override) {
381
- const resolved = resolveSafePackageOutputOverride(project.root, override);
382
- return { output: resolved, dir: target === "standalone", outputBaseDir };
436
+ try {
437
+ const resolved = resolveSafePackageOutputOverride(project.root, override);
438
+ return { output: resolved, dir: target === "standalone", outputBaseDir };
439
+ } catch (err) {
440
+ const message = err instanceof Error ? err.message : String(err);
441
+ throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
442
+ }
383
443
  }
384
444
  if (target === "standalone") {
385
445
  return { output: `${outputBaseDir}/standalone`, dir: true, outputBaseDir };
@@ -456,7 +516,7 @@ async function runPackage(opts) {
456
516
  }
457
517
  if (!existsSync3(distDir)) {
458
518
  throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
459
- code: "RUNTIME",
519
+ code: "INVALID_PROJECT",
460
520
  exitCode: EXIT_INVALID_PROJECT
461
521
  });
462
522
  }
@@ -478,6 +538,7 @@ async function runPackage(opts) {
478
538
  descriptor: project.course,
479
539
  outDir,
480
540
  spaDistDir: distDir,
541
+ projectRoot: project.root,
481
542
  target,
482
543
  output,
483
544
  dir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "private": false,
5
5
  "description": "LessonKit CLI — init, dev, build, and package learning experiences.",
6
6
  "license": "Apache-2.0",
@@ -42,7 +42,8 @@
42
42
  "lint": "echo \"(no lint configured yet)\""
43
43
  },
44
44
  "dependencies": {
45
- "@lessonkit/lxpack": "0.9.1",
45
+ "@lessonkit/core": "0.9.3",
46
+ "@lessonkit/lxpack": "0.9.3",
46
47
  "commander": "^14.0.1"
47
48
  },
48
49
  "engines": {