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