@lessonkit/cli 0.6.0 → 0.8.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,25 +1,557 @@
1
1
  // src/index.ts
2
2
  import { createRequire } from "module";
3
3
  import { Command } from "commander";
4
+
5
+ // src/commands/init.ts
6
+ import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
7
+ import { existsSync } from "fs";
8
+ import { basename, dirname, join, resolve } from "path";
9
+ import { fileURLToPath } from "url";
10
+
11
+ // src/lib/errors.ts
12
+ var EXIT_RUNTIME = 1;
13
+ var EXIT_INVALID_PROJECT = 2;
14
+ var EXIT_PACKAGING = 3;
15
+ var CliError = class extends Error {
16
+ code;
17
+ exitCode;
18
+ issues;
19
+ constructor(message, opts) {
20
+ super(message);
21
+ this.name = "CliError";
22
+ this.code = opts.code;
23
+ this.exitCode = opts.exitCode;
24
+ this.issues = opts.issues;
25
+ }
26
+ };
27
+ function formatCliError(error) {
28
+ if (error instanceof CliError) {
29
+ const json = {
30
+ ok: false,
31
+ code: error.code,
32
+ message: error.message,
33
+ exitCode: error.exitCode,
34
+ issues: error.issues
35
+ };
36
+ let message2 = error.message;
37
+ if (error.issues?.length) {
38
+ const details = error.issues.map((i) => i.path ? `${i.path}: ${i.message}` : i.message).join("\n");
39
+ message2 = `${error.message}
40
+ ${details}`;
41
+ }
42
+ return { message: message2, exitCode: error.exitCode, json };
43
+ }
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ return {
46
+ message,
47
+ exitCode: EXIT_RUNTIME,
48
+ json: { ok: false, code: "RUNTIME", message, exitCode: EXIT_RUNTIME }
49
+ };
50
+ }
51
+
52
+ // src/lib/exec.ts
53
+ import { spawn } from "child_process";
54
+ async function runCommand(command, args, opts) {
55
+ await new Promise((resolvePromise, rejectPromise) => {
56
+ const child = spawn(command, args, {
57
+ cwd: opts.cwd,
58
+ env: opts.env ?? process.env,
59
+ stdio: "inherit",
60
+ shell: false
61
+ });
62
+ child.on("error", (err) => {
63
+ rejectPromise(
64
+ new CliError(`Failed to run ${command}: ${err.message}`, {
65
+ code: "RUNTIME",
66
+ exitCode: EXIT_RUNTIME
67
+ })
68
+ );
69
+ });
70
+ child.on("close", (code) => {
71
+ if (code === 0) {
72
+ resolvePromise();
73
+ return;
74
+ }
75
+ rejectPromise(
76
+ new CliError(`${command} exited with code ${code ?? "unknown"}.`, {
77
+ code: "RUNTIME",
78
+ exitCode: EXIT_RUNTIME
79
+ })
80
+ );
81
+ });
82
+ });
83
+ }
84
+ async function runNpmInstall(cwd) {
85
+ await runCommand("npm", ["install"], { cwd });
86
+ }
87
+
88
+ // src/commands/init.ts
89
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git"]);
90
+ var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
91
+ function getTemplateDir() {
92
+ 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";
97
+ }
98
+ async function isDirEmpty(dir) {
99
+ if (!existsSync(dir)) return true;
100
+ const entries = await readdir(dir);
101
+ return entries.length === 0;
102
+ }
103
+ async function copyTemplate(src, dest) {
104
+ await mkdir(dest, { recursive: true });
105
+ const entries = await readdir(src, { withFileTypes: true });
106
+ for (const entry of entries) {
107
+ if (SKIP_DIRS.has(entry.name) || SKIP_FILES.has(entry.name)) continue;
108
+ const srcPath = join(src, entry.name);
109
+ const destPath = join(dest, entry.name);
110
+ if (entry.isDirectory()) {
111
+ await copyTemplate(srcPath, destPath);
112
+ } else if (entry.isFile()) {
113
+ await cp(srcPath, destPath);
114
+ }
115
+ }
116
+ }
117
+ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
118
+ const pkgPath = join(projectDir, "package.json");
119
+ const lessonkitPath = join(projectDir, "lessonkit.json");
120
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
121
+ pkg.name = projectName;
122
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
123
+ `, "utf8");
124
+ const lessonkit = JSON.parse(await readFile(lessonkitPath, "utf8"));
125
+ lessonkit.name = slug;
126
+ const course = lessonkit.course;
127
+ course.courseId = slug;
128
+ course.title = projectName;
129
+ await writeFile(lessonkitPath, `${JSON.stringify(lessonkit, null, 2)}
130
+ `, "utf8");
131
+ const appPath = join(projectDir, "src", "App.tsx");
132
+ let appSource = await readFile(appPath, "utf8");
133
+ appSource = appSource.replace(/courseId="my-course"/g, `courseId="${slug}"`);
134
+ appSource = appSource.replace(/title="My LessonKit Course"/g, `title="${projectName.replace(/"/g, '\\"')}"`);
135
+ appSource = appSource.replace(/preset="dark"/g, 'preset="default"');
136
+ appSource = appSource.replace(/mode="dark"/g, 'mode="light"');
137
+ await writeFile(appPath, appSource, "utf8");
138
+ }
139
+ async function runInit(opts, logger) {
140
+ const cwd = process.cwd();
141
+ const rawName = opts.name ?? (opts.here ? slugifyName(basename(process.cwd()) || "my-course") : void 0);
142
+ if (!rawName && !opts.here) {
143
+ throw new CliError("Project name is required. Usage: lessonkit init <name> or lessonkit init --here", {
144
+ code: "INVALID_PROJECT",
145
+ exitCode: EXIT_INVALID_PROJECT
146
+ });
147
+ }
148
+ const slug = slugifyName(rawName ?? "my-course");
149
+ const projectName = rawName ?? slug;
150
+ const projectDir = opts.here ? cwd : resolve(cwd, slug);
151
+ if (!opts.here && existsSync(projectDir)) {
152
+ throw new CliError(`Directory already exists: ${projectDir}. Use --force to initialize anyway.`, {
153
+ code: "INVALID_PROJECT",
154
+ exitCode: EXIT_INVALID_PROJECT
155
+ });
156
+ }
157
+ if (opts.here && !await isDirEmpty(projectDir) && !opts.force) {
158
+ throw new CliError(`Directory is not empty: ${projectDir}. Use --force to initialize anyway.`, {
159
+ code: "INVALID_PROJECT",
160
+ exitCode: EXIT_INVALID_PROJECT
161
+ });
162
+ }
163
+ const templateDir = getTemplateDir();
164
+ if (!existsSync(templateDir)) {
165
+ throw new CliError(`Bundled template not found at ${templateDir}. Reinstall @lessonkit/cli.`, {
166
+ code: "RUNTIME",
167
+ exitCode: EXIT_INVALID_PROJECT
168
+ });
169
+ }
170
+ await copyTemplate(templateDir, projectDir);
171
+ await applyTemplateSubstitutions(projectDir, projectName, slug);
172
+ if (!opts.skipInstall) {
173
+ if (!opts.json) logger.log(`Installing dependencies in ${projectDir}\u2026`);
174
+ await runNpmInstall(projectDir);
175
+ }
176
+ if (!opts.json) {
177
+ logger.log(`Created LessonKit project at ${projectDir}`);
178
+ logger.log(`Next: cd ${opts.here ? "." : slug} && lessonkit dev`);
179
+ }
180
+ return { ok: true, command: "init", projectRoot: projectDir };
181
+ }
182
+
183
+ // src/lib/project.ts
184
+ import { readFileSync, existsSync as existsSync2 } from "fs";
185
+ import { readFile as readFile2 } from "fs/promises";
186
+ import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
187
+ import { validateDescriptor } from "@lessonkit/lxpack";
188
+ var LESSONKIT_JSON = "lessonkit.json";
189
+ var PACKAGE_JSON = "package.json";
190
+ var DEFAULT_PATHS = {
191
+ spaDistDir: "dist",
192
+ lxpackOutDir: ".lxpack/course",
193
+ outputBaseDir: ".lxpack/out"
194
+ };
195
+ function isProjectManifest(configPath) {
196
+ try {
197
+ const raw = JSON.parse(readFileSync(configPath, "utf8"));
198
+ return raw.schemaVersion === 1 && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object";
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+ function findProjectRoot(startDir = process.cwd()) {
204
+ let dir = resolve2(startDir);
205
+ const fsRoot = parse(dir).root;
206
+ while (true) {
207
+ const configPath = join2(dir, LESSONKIT_JSON);
208
+ if (existsSync2(configPath) && isProjectManifest(configPath)) {
209
+ return dir;
210
+ }
211
+ if (dir === fsRoot) {
212
+ throw new CliError(`Could not find ${LESSONKIT_JSON} in ${startDir} or any parent directory.`, {
213
+ code: "INVALID_PROJECT",
214
+ exitCode: EXIT_INVALID_PROJECT
215
+ });
216
+ }
217
+ dir = dirname2(dir);
218
+ }
219
+ }
220
+ async function loadLessonkitJson(projectRoot) {
221
+ const configPath = join2(projectRoot, LESSONKIT_JSON);
222
+ let raw;
223
+ try {
224
+ raw = JSON.parse(await readFile2(configPath, "utf8"));
225
+ } catch {
226
+ throw new CliError(`Failed to read or parse ${configPath}.`, {
227
+ code: "INVALID_PROJECT",
228
+ exitCode: EXIT_INVALID_PROJECT
229
+ });
230
+ }
231
+ if (!raw || typeof raw !== "object") {
232
+ throw new CliError(`${configPath} must be a JSON object.`, {
233
+ code: "INVALID_PROJECT",
234
+ exitCode: EXIT_INVALID_PROJECT
235
+ });
236
+ }
237
+ const config = raw;
238
+ const schemaVersion = config.schemaVersion;
239
+ if (schemaVersion !== 1) {
240
+ throw new CliError(`${configPath}: schemaVersion must be 1 (got ${String(schemaVersion)}).`, {
241
+ code: "INVALID_PROJECT",
242
+ exitCode: EXIT_INVALID_PROJECT
243
+ });
244
+ }
245
+ const name = config.name;
246
+ if (typeof name !== "string" || !name.trim()) {
247
+ throw new CliError(`${configPath}: "name" must be a non-empty string.`, {
248
+ code: "INVALID_PROJECT",
249
+ exitCode: EXIT_INVALID_PROJECT
250
+ });
251
+ }
252
+ const courseRaw = config.course;
253
+ if (!courseRaw || typeof courseRaw !== "object") {
254
+ throw new CliError(`${configPath}: "course" must be an object.`, {
255
+ code: "INVALID_PROJECT",
256
+ exitCode: EXIT_INVALID_PROJECT
257
+ });
258
+ }
259
+ const validation = validateDescriptor(courseRaw);
260
+ if (!validation.ok) {
261
+ throw new CliError(`${configPath}: invalid course descriptor.`, {
262
+ code: "INVALID_PROJECT",
263
+ exitCode: EXIT_INVALID_PROJECT,
264
+ issues: validation.issues.map((i) => ({
265
+ path: i.path,
266
+ message: i.message
267
+ }))
268
+ });
269
+ }
270
+ if (validation.descriptor.layout === "per-lesson-spa") {
271
+ throw new CliError(
272
+ `${configPath}: per-lesson-spa layout is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly.`,
273
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
274
+ );
275
+ }
276
+ const pathsRaw = config.paths;
277
+ const paths = { ...DEFAULT_PATHS };
278
+ if (pathsRaw && typeof pathsRaw === "object") {
279
+ const p = pathsRaw;
280
+ if (typeof p.spaDistDir === "string" && p.spaDistDir.trim()) paths.spaDistDir = p.spaDistDir;
281
+ if (typeof p.lxpackOutDir === "string" && p.lxpackOutDir.trim()) paths.lxpackOutDir = p.lxpackOutDir;
282
+ if (typeof p.outputBaseDir === "string" && p.outputBaseDir.trim()) paths.outputBaseDir = p.outputBaseDir;
283
+ }
284
+ return {
285
+ root: projectRoot,
286
+ schemaVersion: 1,
287
+ name,
288
+ course: validation.descriptor,
289
+ paths
290
+ };
291
+ }
292
+ async function loadProject(cwd = process.cwd()) {
293
+ const root = findProjectRoot(cwd);
294
+ return loadLessonkitJson(root);
295
+ }
296
+ async function readPackageJson(projectRoot) {
297
+ const pkgPath = join2(projectRoot, PACKAGE_JSON);
298
+ try {
299
+ return JSON.parse(await readFile2(pkgPath, "utf8"));
300
+ } catch {
301
+ throw new CliError(`Failed to read or parse ${pkgPath}.`, {
302
+ code: "INVALID_PROJECT",
303
+ exitCode: EXIT_INVALID_PROJECT
304
+ });
305
+ }
306
+ }
307
+ function assertViteProject(pkg, projectRoot) {
308
+ const vite = pkg.devDependencies?.vite ?? pkg.dependencies?.vite ?? (existsSync2(join2(projectRoot, "node_modules", ".bin", "vite")) || existsSync2(join2(projectRoot, "node_modules", ".bin", "vite.cmd")) ? "present" : void 0);
309
+ if (!vite) {
310
+ throw new CliError(
311
+ `No Vite dependency found in ${join2(projectRoot, PACKAGE_JSON)}. LessonKit projects require Vite.`,
312
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
313
+ );
314
+ }
315
+ }
316
+ function resolveViteBin(projectRoot) {
317
+ let dir = resolve2(projectRoot);
318
+ const fsRoot = parse(dir).root;
319
+ while (true) {
320
+ const binDir = join2(dir, "node_modules", ".bin");
321
+ const bin = join2(binDir, "vite");
322
+ if (existsSync2(bin)) return bin;
323
+ const binCmd = join2(binDir, "vite.cmd");
324
+ if (existsSync2(binCmd)) return binCmd;
325
+ if (dir === fsRoot) break;
326
+ dir = dirname2(dir);
327
+ }
328
+ throw new CliError(
329
+ `Vite binary not found near ${projectRoot}. Run npm install in the project first.`,
330
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
331
+ );
332
+ }
333
+ function assertNode20ForLxpack() {
334
+ const major = Number(process.versions.node.split(".")[0]);
335
+ if (major < 20) {
336
+ throw new CliError(
337
+ `LMS packaging requires Node.js 20+ (current: ${process.versions.node}). See docs/PACKAGING.md.`,
338
+ { code: "NODE_VERSION", exitCode: EXIT_INVALID_PROJECT }
339
+ );
340
+ }
341
+ }
342
+
343
+ // src/lib/paths.ts
344
+ import { resolve as resolve3 } from "path";
345
+ function resolveDistDir(project) {
346
+ return resolve3(project.root, project.paths.spaDistDir);
347
+ }
348
+ function resolveLxpackOutDir(project) {
349
+ return resolve3(project.root, project.paths.lxpackOutDir);
350
+ }
351
+ function resolvePackageOutput(project, target, override) {
352
+ const outputBaseDir = project.paths.outputBaseDir;
353
+ if (override) {
354
+ return { output: override, dir: target === "standalone", outputBaseDir };
355
+ }
356
+ if (target === "standalone") {
357
+ return { output: `${outputBaseDir}/standalone`, dir: true, outputBaseDir };
358
+ }
359
+ return { output: `${outputBaseDir}/course-${target}.zip`, dir: false, outputBaseDir };
360
+ }
361
+ var DEFAULT_SPA_DIST_DIR = "dist";
362
+ function resolveViteBuildArgs(project) {
363
+ const args = ["build"];
364
+ if (project.paths.spaDistDir !== DEFAULT_SPA_DIST_DIR) {
365
+ args.push("--outDir", project.paths.spaDistDir);
366
+ }
367
+ return args;
368
+ }
369
+ var PACKAGE_TARGETS = [
370
+ "react-vite",
371
+ "scorm12",
372
+ "scorm2004",
373
+ "xapi",
374
+ "cmi5",
375
+ "standalone"
376
+ ];
377
+ function parsePackageTarget(value) {
378
+ if (!value) {
379
+ throw new Error("TARGET_REQUIRED");
380
+ }
381
+ if (PACKAGE_TARGETS.includes(value)) {
382
+ return value;
383
+ }
384
+ throw new Error(`Unknown target "${value}". Valid targets: ${PACKAGE_TARGETS.join(", ")}`);
385
+ }
386
+
387
+ // src/commands/dev.ts
388
+ async function runDev(opts) {
389
+ const project = await loadProject(opts.cwd ?? process.cwd());
390
+ const pkg = await readPackageJson(project.root);
391
+ assertViteProject(pkg, project.root);
392
+ const viteBin = resolveViteBin(project.root);
393
+ await runCommand(viteBin, opts.viteArgs ?? [], { cwd: project.root });
394
+ return { ok: true, command: "dev", projectRoot: project.root };
395
+ }
396
+ async function runBuild(opts) {
397
+ const project = await loadProject(opts.cwd ?? process.cwd());
398
+ const pkg = await readPackageJson(project.root);
399
+ assertViteProject(pkg, project.root);
400
+ const viteBin = resolveViteBin(project.root);
401
+ const buildArgs = resolveViteBuildArgs(project);
402
+ await runCommand(viteBin, [...buildArgs, ...opts.viteArgs ?? []], { cwd: project.root });
403
+ return { ok: true, command: "build", projectRoot: project.root };
404
+ }
405
+
406
+ // src/commands/package.ts
407
+ import { existsSync as existsSync3 } from "fs";
408
+ import { packageLessonkitCourse } from "@lessonkit/lxpack";
409
+ async function runPackage(opts) {
410
+ let target;
411
+ try {
412
+ target = parsePackageTarget(opts.target);
413
+ } catch (err) {
414
+ const message = err instanceof Error ? err.message : String(err);
415
+ if (message === "TARGET_REQUIRED") {
416
+ throw new CliError("--target is required. Example: lessonkit package --target scorm12", {
417
+ code: "TARGET_REQUIRED",
418
+ exitCode: EXIT_INVALID_PROJECT
419
+ });
420
+ }
421
+ throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
422
+ }
423
+ const project = await loadProject(opts.cwd ?? process.cwd());
424
+ const distDir = resolveDistDir(project);
425
+ if (target === "react-vite") {
426
+ await runBuild({ cwd: project.root, json: opts.json });
427
+ if (!existsSync3(distDir)) {
428
+ throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
429
+ code: "RUNTIME",
430
+ exitCode: EXIT_INVALID_PROJECT
431
+ });
432
+ }
433
+ return { ok: true, target, projectRoot: project.root, distDir };
434
+ }
435
+ assertNode20ForLxpack();
436
+ if (!opts.noBuild || !existsSync3(distDir)) {
437
+ await runBuild({ cwd: project.root, json: opts.json });
438
+ }
439
+ if (!existsSync3(distDir)) {
440
+ throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
441
+ code: "INVALID_PROJECT",
442
+ exitCode: EXIT_INVALID_PROJECT
443
+ });
444
+ }
445
+ const outDir = resolveLxpackOutDir(project);
446
+ const { output, dir, outputBaseDir } = resolvePackageOutput(project, target, opts.out);
447
+ const result = await packageLessonkitCourse({
448
+ descriptor: project.course,
449
+ outDir,
450
+ spaDistDir: distDir,
451
+ target,
452
+ output,
453
+ dir,
454
+ outputBaseDir
455
+ });
456
+ if (!result.ok) {
457
+ throw new CliError("Packaging failed.", {
458
+ code: "PACKAGING",
459
+ exitCode: EXIT_PACKAGING,
460
+ issues: result.issues
461
+ });
462
+ }
463
+ return {
464
+ ok: true,
465
+ target,
466
+ projectRoot: project.root,
467
+ outputPath: result.outputPath,
468
+ outputDir: result.outputDir,
469
+ fileCount: result.fileCount
470
+ };
471
+ }
472
+
473
+ // src/lib/logger.ts
474
+ function createLogger(opts) {
475
+ if (opts?.json) {
476
+ return {
477
+ log: () => {
478
+ },
479
+ error: () => {
480
+ }
481
+ };
482
+ }
483
+ return console;
484
+ }
485
+
486
+ // src/index.ts
4
487
  var require2 = createRequire(import.meta.url);
5
488
  var { version } = require2("../package.json");
6
- function createProgram(logger = console) {
489
+ async function handleCommand(fn, logger, json) {
490
+ try {
491
+ const result = await fn();
492
+ if (json) {
493
+ console.log(JSON.stringify(result));
494
+ }
495
+ } catch (error) {
496
+ const formatted = formatCliError(error);
497
+ if (json) {
498
+ console.log(JSON.stringify(formatted.json));
499
+ } else {
500
+ logger.error(formatted.message);
501
+ }
502
+ process.exitCode = formatted.exitCode;
503
+ }
504
+ }
505
+ function createProgram(baseLogger = console) {
7
506
  const program = new Command();
8
507
  program.name("lessonkit").description("LessonKit CLI").version(version);
9
- program.command("init").description("Initialize a LessonKit project (stub)").action(() => {
10
- logger.log("lessonkit init (coming soon)");
11
- });
12
- program.command("dev").description("Run a LessonKit project in dev mode (stub)").action(() => {
13
- logger.log("lessonkit dev (coming soon)");
508
+ program.command("init").description("Initialize a LessonKit project from the Vite + React template").argument("[name]", "Project directory name").option("--here", "Initialize in the current directory").option("--skip-install", "Skip npm install").option("--force", "Initialize into a non-empty directory").option("--json", "Emit structured JSON result").action(async (name, opts) => {
509
+ const logger = createLogger({ json: opts.json });
510
+ await handleCommand(
511
+ () => runInit({ name, here: opts.here, skipInstall: opts.skipInstall, force: opts.force, json: opts.json }, logger),
512
+ logger,
513
+ Boolean(opts.json)
514
+ );
14
515
  });
15
- program.command("build").description("Build a LessonKit project (stub)").action(() => {
16
- logger.log("lessonkit build (coming soon)");
516
+ const addCwdAndJson = (cmd) => cmd.option("--cwd <dir>", "Project root directory").option("--json", "Emit structured JSON result");
517
+ addCwdAndJson(
518
+ program.command("dev").description("Run the Vite dev server").allowUnknownOption().allowExcessArguments()
519
+ ).action(async (opts, command) => {
520
+ const logger = createLogger({ json: opts.json });
521
+ const viteArgs = command.args;
522
+ await handleCommand(
523
+ () => runDev({ cwd: opts.cwd, json: opts.json, viteArgs }),
524
+ logger,
525
+ Boolean(opts.json)
526
+ );
17
527
  });
18
- program.command("package").description("Package to SCORM/xAPI formats (stub)").action(() => {
19
- logger.log("lessonkit package (coming soon)");
528
+ addCwdAndJson(program.command("build").description("Production Vite build")).action(
529
+ async (opts, command) => {
530
+ const logger = createLogger({ json: opts.json });
531
+ const viteArgs = command.args;
532
+ await handleCommand(
533
+ () => runBuild({ cwd: opts.cwd, json: opts.json, viteArgs }),
534
+ logger,
535
+ Boolean(opts.json)
536
+ );
537
+ }
538
+ );
539
+ program.command("package").description("Build or package for web / LMS delivery").requiredOption("--target <target>", `Export target (${PACKAGE_TARGETS.join(", ")})`).option("--cwd <dir>", "Project root directory").option("--no-build", "Skip implicit Vite build for LMS targets").option("--out <path>", "Override output artifact path").option("--json", "Emit structured JSON result").action(async (opts) => {
540
+ const logger = createLogger({ json: opts.json });
541
+ await handleCommand(
542
+ () => runPackage({
543
+ target: opts.target,
544
+ cwd: opts.cwd,
545
+ noBuild: opts.build === false,
546
+ out: opts.out,
547
+ json: opts.json
548
+ }),
549
+ logger,
550
+ Boolean(opts.json)
551
+ );
20
552
  });
21
553
  program.command("publish").description("Publish package artifacts (stub)").action(() => {
22
- logger.log("lessonkit publish (coming soon)");
554
+ baseLogger.log("lessonkit publish is not implemented. See RELEASING.md for npm publish workflow.");
23
555
  });
24
556
  return program;
25
557
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@lessonkit/cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
- "description": "LessonKit CLI (early stub).",
5
+ "description": "LessonKit CLI init, dev, build, and package learning experiences.",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
@@ -28,11 +28,13 @@
28
28
  ".": "./dist/index.js"
29
29
  },
30
30
  "files": [
31
- "dist"
31
+ "dist",
32
+ "template"
32
33
  ],
33
34
  "scripts": {
34
- "build": "tsup src/index.ts src/bin.ts --format esm --splitting=false --platform node --target node18",
35
- "dev": "tsup src/index.ts src/bin.ts --format esm --watch --splitting=false --platform node --target node18",
35
+ "copy-template": "node scripts/copy-template.mjs",
36
+ "build": "npm run copy-template && tsup src/index.ts src/bin.ts --format esm --splitting=false --platform node --target node18 --dts",
37
+ "dev": "npm run copy-template && tsup src/index.ts src/bin.ts --format esm --watch --splitting=false --platform node --target node18 --dts",
36
38
  "prepublishOnly": "npm run build",
37
39
  "typecheck": "tsc -p tsconfig.json",
38
40
  "test": "vitest run --passWithNoTests",
@@ -40,8 +42,12 @@
40
42
  "lint": "echo \"(no lint configured yet)\""
41
43
  },
42
44
  "dependencies": {
45
+ "@lessonkit/lxpack": "0.8.0",
43
46
  "commander": "^14.0.1"
44
47
  },
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
45
51
  "devDependencies": {
46
52
  "@types/node": "^24.0.0",
47
53
  "tsup": "^8.5.0",
@@ -0,0 +1,19 @@
1
+ # LessonKit Vite + React template
2
+
3
+ [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/)
4
+ [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
5
+
6
+ Starter template copied by `lessonkit init`. See the [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) and [React quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html).
7
+
8
+ ## Run
9
+
10
+ ```bash
11
+ npm install
12
+ npm run dev
13
+ ```
14
+
15
+ ## Notes
16
+
17
+ - Depends on [`@lessonkit/react`](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/components-and-hooks.html).
18
+ - Copied by [`@lessonkit/cli`](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) when you run `lessonkit init`.
19
+ - Package for an LMS with the [packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html).