@lessonkit/cli 1.4.0 → 1.6.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 +26 -5
- package/dist/bin.js +343 -42
- package/dist/index.d.ts +7 -0
- package/dist/index.js +343 -42
- package/package.json +6 -5
- package/template/vite-react/.env.example +6 -3
- package/template/vite-react/README.md +27 -2
- package/template/vite-react/package.json +7 -7
- package/template/vite-react/src/App.tsx +2 -2
- package/template/vite-react/src/courseConfig.test.ts +2 -0
- package/template/vite-react/src/courseConfig.ts +11 -1
- package/template/vite-react/src/vite-env.d.ts +3 -0
package/README.md
CHANGED
|
@@ -4,25 +4,37 @@
|
|
|
4
4
|
[](https://lessonkit.readthedocs.io/en/latest/reference/cli.html)
|
|
5
5
|
[](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Scaffold, develop, build, and package LessonKit courses.
|
|
7
|
+
Scaffold, develop, build, and package LessonKit courses.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install -g @lessonkit/cli
|
|
13
|
-
# or one-shot:
|
|
13
|
+
# or one-shot (recommended for new courses):
|
|
14
14
|
npx @lessonkit/cli init my-course
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
**Node.js:** **20.19+** recommended for `init` (Vite 8 scaffold). **18+** minimum for `dev`, `build`, and `package`.
|
|
18
|
+
|
|
17
19
|
## Commands
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
lessonkit init my-course # scaffold Vite + React project
|
|
22
|
+
lessonkit init my-course # scaffold Vite + React project (runs npm install)
|
|
21
23
|
lessonkit dev # Vite dev server
|
|
22
24
|
lessonkit build # production build → dist/
|
|
23
25
|
lessonkit package --target scorm12 # LMS artifact
|
|
24
26
|
```
|
|
25
27
|
|
|
28
|
+
### Init flags
|
|
29
|
+
|
|
30
|
+
| Flag | Purpose |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `--here` | Scaffold in the current directory |
|
|
33
|
+
| `--force` | Overwrite existing files in the target directory |
|
|
34
|
+
| `--skip-install` | Skip `npm install` after copying the template |
|
|
35
|
+
|
|
36
|
+
### Package targets
|
|
37
|
+
|
|
26
38
|
| Target | Output |
|
|
27
39
|
| --- | --- |
|
|
28
40
|
| `react-vite` | Vite build only |
|
|
@@ -32,11 +44,20 @@ lessonkit package --target scorm12 # LMS artifact
|
|
|
32
44
|
|
|
33
45
|
Every project includes a root `lessonkit.json` manifest (`schemaVersion: 1`).
|
|
34
46
|
|
|
35
|
-
Subprocess timeout defaults to **30 minutes** (`LESSONKIT_CMD_TIMEOUT_MS`).
|
|
47
|
+
Subprocess timeout defaults to **30 minutes** (`LESSONKIT_CMD_TIMEOUT_MS`).
|
|
48
|
+
|
|
49
|
+
## Common issues
|
|
50
|
+
|
|
51
|
+
| Symptom | Fix |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `lessonkit: command not found` | Use `npx @lessonkit/cli` or `npm run dev` in a scaffolded project |
|
|
54
|
+
| `init` fails on Node version | Use Node **20.19+** for Vite 8 scaffold |
|
|
55
|
+
| SCORM zip not at project root | Default path: `.lxpack/course/.lxpack/out/course-scorm12.zip` — trust CLI stdout |
|
|
56
|
+
| ID parity errors on `package` | Align `courseId`, `lessonId`, `checkId` between React and `lessonkit.json` |
|
|
36
57
|
|
|
37
58
|
## Docs
|
|
38
59
|
|
|
39
|
-
[CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [Packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [
|
|
60
|
+
[5-minute guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/getting-started-in-5-minutes.html) · [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [Ship to LMS checklist](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/ship-to-lms.html) · [Packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [Published template](https://github.com/eddiethedean/lessonkit/tree/main/packages/cli/template/vite-react) (monorepo source: [`templates/vite-react`](https://github.com/eddiethedean/lessonkit/tree/main/templates/vite-react))
|
|
40
61
|
|
|
41
62
|
## License
|
|
42
63
|
|
package/dist/bin.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire as
|
|
4
|
+
import { createRequire as createRequire3 } from "module";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/init.ts
|
|
8
8
|
import { slugifyId } from "@lessonkit/core";
|
|
9
|
-
import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
9
|
+
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
10
10
|
import { existsSync } from "fs";
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
11
12
|
import { basename, dirname, join, resolve } from "path";
|
|
12
13
|
import { fileURLToPath } from "url";
|
|
13
14
|
|
|
@@ -124,7 +125,7 @@ async function runNpmInstall(cwd) {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
// src/commands/init.ts
|
|
127
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git"]);
|
|
128
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git", "coverage", ".nyc_output"]);
|
|
128
129
|
var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
|
|
129
130
|
function getTemplateDir() {
|
|
130
131
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -148,7 +149,7 @@ async function isDirEmptyOrDotfilesOnly(dir) {
|
|
|
148
149
|
return entries.every((name) => name.startsWith("."));
|
|
149
150
|
}
|
|
150
151
|
function escapeJsxString(value) {
|
|
151
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\r\n|\n|\r/g, "\\n");
|
|
152
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/\r\n|\n|\r/g, "\\n");
|
|
152
153
|
}
|
|
153
154
|
async function copyTemplate(src, dest) {
|
|
154
155
|
await mkdir(dest, { recursive: true });
|
|
@@ -169,7 +170,7 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
169
170
|
const pkgPath = join(projectDir, "package.json");
|
|
170
171
|
const lessonkitPath = join(projectDir, "lessonkit.json");
|
|
171
172
|
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
172
|
-
pkg.name =
|
|
173
|
+
pkg.name = slug;
|
|
173
174
|
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
174
175
|
`, "utf8");
|
|
175
176
|
const lessonkit = JSON.parse(await readFile(lessonkitPath, "utf8"));
|
|
@@ -177,11 +178,11 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
177
178
|
const course = lessonkit.course;
|
|
178
179
|
course.courseId = slug;
|
|
179
180
|
course.title = projectName;
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
const courseTracking = course.tracking ?? {};
|
|
182
|
+
const courseXapi = courseTracking.xapi ?? {};
|
|
183
|
+
courseXapi.activityIri = `https://example.com/courses/${slug}`;
|
|
184
|
+
courseTracking.xapi = courseXapi;
|
|
185
|
+
course.tracking = courseTracking;
|
|
185
186
|
await writeFile(lessonkitPath, `${JSON.stringify(lessonkit, null, 2)}
|
|
186
187
|
`, "utf8");
|
|
187
188
|
const courseConfigPath = join(projectDir, "src", "courseConfig.ts");
|
|
@@ -194,6 +195,75 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
194
195
|
appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
|
|
195
196
|
await writeFile(appPath, appSource, "utf8");
|
|
196
197
|
}
|
|
198
|
+
async function backupConflictingFiles(stagingDir, projectDir) {
|
|
199
|
+
const backups = /* @__PURE__ */ new Map();
|
|
200
|
+
const stagingEntries = await readdir(stagingDir, { withFileTypes: true });
|
|
201
|
+
for (const entry of stagingEntries) {
|
|
202
|
+
const destPath = join(projectDir, entry.name);
|
|
203
|
+
if (!existsSync(destPath)) continue;
|
|
204
|
+
const destStat = await stat(destPath);
|
|
205
|
+
if (destStat.isFile()) {
|
|
206
|
+
backups.set(entry.name, await readFile(destPath));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return backups;
|
|
210
|
+
}
|
|
211
|
+
async function rollbackPromotedFiles(projectDir, stagingDir, preExisting, backups) {
|
|
212
|
+
const failures = [];
|
|
213
|
+
let stagingEntries;
|
|
214
|
+
try {
|
|
215
|
+
stagingEntries = await readdir(stagingDir, { withFileTypes: true });
|
|
216
|
+
} catch {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
for (const entry of stagingEntries) {
|
|
220
|
+
if (preExisting.has(entry.name)) continue;
|
|
221
|
+
try {
|
|
222
|
+
await rm(join(projectDir, entry.name), { recursive: true, force: true });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
failures.push(
|
|
225
|
+
`remove ${entry.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const [name, content] of backups) {
|
|
230
|
+
try {
|
|
231
|
+
await writeFile(join(projectDir, name), content);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
failures.push(`restore ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (failures.length > 0) {
|
|
237
|
+
throw new CliError(`Init rollback failed: ${failures.join("; ")}`, {
|
|
238
|
+
code: "RUNTIME",
|
|
239
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function promoteStagingToProjectDir(stagingDir, projectDir) {
|
|
244
|
+
await mkdir(projectDir, { recursive: true });
|
|
245
|
+
const entries = await readdir(stagingDir, { withFileTypes: true });
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const srcPath = join(stagingDir, entry.name);
|
|
248
|
+
const destPath = join(projectDir, entry.name);
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
await cp(srcPath, destPath, { recursive: true });
|
|
251
|
+
} else if (entry.isFile()) {
|
|
252
|
+
await cp(srcPath, destPath);
|
|
253
|
+
} else {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
var __testInitHelpers = {
|
|
258
|
+
getTemplateDir,
|
|
259
|
+
isDirEmpty,
|
|
260
|
+
isDirEmptyOrDotfilesOnly,
|
|
261
|
+
escapeJsxString,
|
|
262
|
+
copyTemplate,
|
|
263
|
+
promoteStagingToProjectDir,
|
|
264
|
+
rollbackPromotedFiles,
|
|
265
|
+
backupConflictingFiles
|
|
266
|
+
};
|
|
197
267
|
async function runInit(opts, logger) {
|
|
198
268
|
const cwd = process.cwd();
|
|
199
269
|
const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
|
|
@@ -221,11 +291,14 @@ async function runInit(opts, logger) {
|
|
|
221
291
|
}
|
|
222
292
|
);
|
|
223
293
|
}
|
|
224
|
-
if (opts.here && !await
|
|
225
|
-
throw new CliError(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
294
|
+
if (opts.here && !await isDirEmptyOrDotfilesOnly(projectDir) && !opts.force) {
|
|
295
|
+
throw new CliError(
|
|
296
|
+
`Directory is not empty: ${projectDir}. Use --here --force only when the directory is empty or contains dotfiles only (e.g. .git).`,
|
|
297
|
+
{
|
|
298
|
+
code: "INVALID_PROJECT",
|
|
299
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
300
|
+
}
|
|
301
|
+
);
|
|
229
302
|
}
|
|
230
303
|
if (opts.here && opts.force && !await isDirEmptyOrDotfilesOnly(projectDir)) {
|
|
231
304
|
throw new CliError(
|
|
@@ -243,11 +316,42 @@ async function runInit(opts, logger) {
|
|
|
243
316
|
exitCode: EXIT_INVALID_PROJECT
|
|
244
317
|
});
|
|
245
318
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
319
|
+
const stagingDir = opts.here ? join(cwd, `.lessonkit-init-${randomUUID()}`) : join(cwd, `.${slug}-init-${randomUUID()}`);
|
|
320
|
+
try {
|
|
321
|
+
await copyTemplate(templateDir, stagingDir);
|
|
322
|
+
await applyTemplateSubstitutions(stagingDir, projectName, slug);
|
|
323
|
+
if (!opts.skipInstall) {
|
|
324
|
+
if (!opts.json) logger.log(`Installing dependencies in ${stagingDir}\u2026`);
|
|
325
|
+
await runNpmInstall(stagingDir);
|
|
326
|
+
}
|
|
327
|
+
if (opts.here) {
|
|
328
|
+
const preExisting = new Set(await readdir(projectDir));
|
|
329
|
+
const backups = await backupConflictingFiles(stagingDir, projectDir);
|
|
330
|
+
try {
|
|
331
|
+
await __testInitHelpers.promoteStagingToProjectDir(stagingDir, projectDir);
|
|
332
|
+
} catch (promoteErr) {
|
|
333
|
+
try {
|
|
334
|
+
await rollbackPromotedFiles(projectDir, stagingDir, preExisting, backups);
|
|
335
|
+
} catch (rollbackErr) {
|
|
336
|
+
const promoteMessage = promoteErr instanceof Error ? promoteErr.message : String(promoteErr);
|
|
337
|
+
const rollbackMessage = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
338
|
+
throw new CliError(`${promoteMessage}; ${rollbackMessage}`, {
|
|
339
|
+
code: "RUNTIME",
|
|
340
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
throw promoteErr;
|
|
344
|
+
}
|
|
345
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
346
|
+
} else {
|
|
347
|
+
await rename(stagingDir, projectDir);
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(
|
|
351
|
+
/* v8 ignore next */
|
|
352
|
+
() => void 0
|
|
353
|
+
);
|
|
354
|
+
throw err;
|
|
251
355
|
}
|
|
252
356
|
if (!opts.json) {
|
|
253
357
|
logger.log(`Created LessonKit project at ${projectDir}`);
|
|
@@ -256,6 +360,12 @@ async function runInit(opts, logger) {
|
|
|
256
360
|
return { ok: true, command: "init", projectRoot: projectDir };
|
|
257
361
|
}
|
|
258
362
|
|
|
363
|
+
// src/commands/dev.ts
|
|
364
|
+
import { existsSync as existsSync3 } from "fs";
|
|
365
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
366
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
367
|
+
import { assertSpaDistContentsSafe } from "@lessonkit/lxpack";
|
|
368
|
+
|
|
259
369
|
// src/lib/project.ts
|
|
260
370
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
261
371
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -264,10 +374,13 @@ import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "
|
|
|
264
374
|
import { parseLessonkitManifest } from "@lessonkit/lxpack";
|
|
265
375
|
var LESSONKIT_JSON = "lessonkit.json";
|
|
266
376
|
var PACKAGE_JSON = "package.json";
|
|
377
|
+
function isSchemaVersionOne(value) {
|
|
378
|
+
return value === 1 || value === "1";
|
|
379
|
+
}
|
|
267
380
|
function isProjectManifest(configPath) {
|
|
268
381
|
try {
|
|
269
382
|
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
270
|
-
return raw.schemaVersion
|
|
383
|
+
return isSchemaVersionOne(raw.schemaVersion) && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
|
|
271
384
|
} catch {
|
|
272
385
|
return false;
|
|
273
386
|
}
|
|
@@ -440,7 +553,12 @@ function resolvePackageOutput(project, target, override) {
|
|
|
440
553
|
if (override) {
|
|
441
554
|
try {
|
|
442
555
|
const resolved = resolveSafePackageOutputOverride(project.root, override);
|
|
443
|
-
|
|
556
|
+
const isZipOutput = override.trim().toLowerCase().endsWith(".zip");
|
|
557
|
+
return {
|
|
558
|
+
output: resolved,
|
|
559
|
+
dir: target === "standalone" && !isZipOutput,
|
|
560
|
+
outputBaseDir
|
|
561
|
+
};
|
|
444
562
|
} catch (err) {
|
|
445
563
|
const message = err instanceof Error ? err.message : String(err);
|
|
446
564
|
throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
|
|
@@ -451,13 +569,24 @@ function resolvePackageOutput(project, target, override) {
|
|
|
451
569
|
}
|
|
452
570
|
return { output: `${outputBaseDir}/course-${target}.zip`, dir: false, outputBaseDir };
|
|
453
571
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
572
|
+
function stripOutDirFromViteArgs(viteArgs) {
|
|
573
|
+
const stripped = [];
|
|
574
|
+
for (let i = 0; i < viteArgs.length; i++) {
|
|
575
|
+
const arg = viteArgs[i];
|
|
576
|
+
if (arg === "--outDir" || arg === "-o") {
|
|
577
|
+
i++;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (arg.startsWith("--outDir=")) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
stripped.push(arg);
|
|
459
584
|
}
|
|
460
|
-
return
|
|
585
|
+
return stripped;
|
|
586
|
+
}
|
|
587
|
+
function resolveViteBuildArgv(project, viteArgs = []) {
|
|
588
|
+
const passthrough = stripOutDirFromViteArgs(viteArgs);
|
|
589
|
+
return ["build", ...passthrough, "--outDir", project.paths.spaDistDir];
|
|
461
590
|
}
|
|
462
591
|
function parsePackageTarget(value) {
|
|
463
592
|
if (!value) {
|
|
@@ -475,7 +604,8 @@ async function runDev(opts) {
|
|
|
475
604
|
const pkg = await readPackageJson(project.root);
|
|
476
605
|
assertViteProject(pkg, project.root);
|
|
477
606
|
const viteJs = resolveViteJs(project.root);
|
|
478
|
-
|
|
607
|
+
const devArgs = stripOutDirFromViteArgs(opts.viteArgs ?? []);
|
|
608
|
+
await runCommand(process.execPath, [viteJs, ...devArgs], {
|
|
479
609
|
cwd: project.root,
|
|
480
610
|
timeoutMs: 0
|
|
481
611
|
});
|
|
@@ -486,15 +616,28 @@ async function runBuild(opts) {
|
|
|
486
616
|
const pkg = await readPackageJson(project.root);
|
|
487
617
|
assertViteProject(pkg, project.root);
|
|
488
618
|
const viteJs = resolveViteJs(project.root);
|
|
489
|
-
const
|
|
490
|
-
await
|
|
619
|
+
const distDir = resolveDistDir(project);
|
|
620
|
+
await mkdir2(dirname3(distDir), { recursive: true });
|
|
621
|
+
if (existsSync3(distDir)) {
|
|
622
|
+
await assertSpaDistContentsSafe({ main: distDir }, project.root);
|
|
623
|
+
}
|
|
624
|
+
const buildArgs = resolveViteBuildArgv(project, opts.viteArgs);
|
|
625
|
+
await runCommand(process.execPath, [viteJs, ...buildArgs], {
|
|
491
626
|
cwd: project.root
|
|
492
627
|
});
|
|
628
|
+
const indexHtml = join3(distDir, "index.html");
|
|
629
|
+
if (!existsSync3(indexHtml)) {
|
|
630
|
+
throw new CliError(
|
|
631
|
+
`Build did not produce index.html at ${indexHtml}. Check paths.spaDistDir in lessonkit.json.`,
|
|
632
|
+
{ code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
|
|
633
|
+
);
|
|
634
|
+
}
|
|
493
635
|
return { ok: true, command: "build", projectRoot: project.root };
|
|
494
636
|
}
|
|
495
637
|
|
|
496
638
|
// src/commands/package.ts
|
|
497
|
-
import { existsSync as
|
|
639
|
+
import { existsSync as existsSync4 } from "fs";
|
|
640
|
+
import { isAbsolute } from "path";
|
|
498
641
|
import { packageLessonkitCourse } from "@lessonkit/lxpack";
|
|
499
642
|
async function runPackage(opts) {
|
|
500
643
|
let target;
|
|
@@ -512,7 +655,7 @@ async function runPackage(opts) {
|
|
|
512
655
|
}
|
|
513
656
|
const project = await loadProject(opts.cwd ?? process.cwd());
|
|
514
657
|
const distDir = resolveDistDir(project);
|
|
515
|
-
if (opts.noBuild && !
|
|
658
|
+
if (opts.noBuild && !existsSync4(distDir)) {
|
|
516
659
|
throw new CliError(
|
|
517
660
|
`dist directory not found at ${distDir}. Run lessonkit build before packaging with --no-build.`,
|
|
518
661
|
{
|
|
@@ -525,7 +668,7 @@ async function runPackage(opts) {
|
|
|
525
668
|
if (!opts.noBuild) {
|
|
526
669
|
await runBuild({ cwd: project.root, json: opts.json });
|
|
527
670
|
}
|
|
528
|
-
if (!
|
|
671
|
+
if (!existsSync4(distDir)) {
|
|
529
672
|
throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
|
|
530
673
|
code: "INVALID_PROJECT",
|
|
531
674
|
exitCode: EXIT_INVALID_PROJECT
|
|
@@ -537,14 +680,20 @@ async function runPackage(opts) {
|
|
|
537
680
|
if (!opts.noBuild) {
|
|
538
681
|
await runBuild({ cwd: project.root, json: opts.json });
|
|
539
682
|
}
|
|
540
|
-
if (!
|
|
683
|
+
if (!existsSync4(distDir)) {
|
|
541
684
|
throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
|
|
542
685
|
code: "INVALID_PROJECT",
|
|
543
686
|
exitCode: EXIT_INVALID_PROJECT
|
|
544
687
|
});
|
|
545
688
|
}
|
|
546
689
|
const outDir = resolveLxpackOutDir(project);
|
|
547
|
-
const { output, dir, outputBaseDir } = resolvePackageOutput(
|
|
690
|
+
const { output: resolvedOutput, dir, outputBaseDir } = resolvePackageOutput(
|
|
691
|
+
project,
|
|
692
|
+
target,
|
|
693
|
+
opts.out
|
|
694
|
+
);
|
|
695
|
+
const trimmedOut = opts.out?.trim();
|
|
696
|
+
const output = trimmedOut && !isAbsolute(trimmedOut) ? trimmedOut : resolvedOutput;
|
|
548
697
|
const result = await packageLessonkitCourse({
|
|
549
698
|
descriptor: project.course,
|
|
550
699
|
outDir,
|
|
@@ -553,7 +702,9 @@ async function runPackage(opts) {
|
|
|
553
702
|
target,
|
|
554
703
|
output,
|
|
555
704
|
dir,
|
|
556
|
-
outputBaseDir
|
|
705
|
+
outputBaseDir,
|
|
706
|
+
strictParity: opts.strictParity,
|
|
707
|
+
strictBuild: opts.strict
|
|
557
708
|
});
|
|
558
709
|
if (!result.ok) {
|
|
559
710
|
throw new CliError("Packaging failed.", {
|
|
@@ -585,6 +736,112 @@ async function runPackage(opts) {
|
|
|
585
736
|
};
|
|
586
737
|
}
|
|
587
738
|
|
|
739
|
+
// src/commands/export.ts
|
|
740
|
+
import { existsSync as existsSync5 } from "fs";
|
|
741
|
+
import { relative, resolve as resolve4 } from "path";
|
|
742
|
+
import { exportLkcourse, resolveSafePackageOutputOverride as resolveSafePackageOutputOverride2 } from "@lessonkit/lxpack";
|
|
743
|
+
function resolveExportOutput(projectRoot, override, defaultName) {
|
|
744
|
+
if (override) {
|
|
745
|
+
try {
|
|
746
|
+
return resolveSafePackageOutputOverride2(projectRoot, override);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
749
|
+
throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return resolve4(projectRoot, `${defaultName ?? "course"}.lkcourse`);
|
|
753
|
+
}
|
|
754
|
+
async function runExport(opts) {
|
|
755
|
+
const project = await loadProject(opts.cwd ?? process.cwd());
|
|
756
|
+
const distDir = resolve4(project.root, project.paths.spaDistDir);
|
|
757
|
+
if (opts.noBuild && !existsSync5(distDir)) {
|
|
758
|
+
throw new CliError(
|
|
759
|
+
`dist directory not found at ${distDir}. Run lessonkit build before export with --no-build.`,
|
|
760
|
+
{
|
|
761
|
+
code: "INVALID_PROJECT",
|
|
762
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
763
|
+
}
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (!opts.noBuild) {
|
|
767
|
+
await runBuild({ cwd: project.root, json: opts.json });
|
|
768
|
+
}
|
|
769
|
+
if (!existsSync5(distDir)) {
|
|
770
|
+
throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
|
|
771
|
+
code: "INVALID_PROJECT",
|
|
772
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const resolvedOut = resolveExportOutput(project.root, opts.out, project.name);
|
|
776
|
+
const outRelative = relative(project.root, resolvedOut).replace(/\\/g, "/");
|
|
777
|
+
const result = await exportLkcourse({
|
|
778
|
+
projectRoot: project.root,
|
|
779
|
+
manifest: project,
|
|
780
|
+
outPath: outRelative,
|
|
781
|
+
includeBlockTree: Boolean(opts.withBlockTree)
|
|
782
|
+
});
|
|
783
|
+
if (!result.ok) {
|
|
784
|
+
throw new CliError(
|
|
785
|
+
result.issues.map((i) => `${i.path}: ${i.message}`).join("; "),
|
|
786
|
+
{ code: "EXPORT_FAILED", exitCode: EXIT_PACKAGING }
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
ok: true,
|
|
791
|
+
command: "export",
|
|
792
|
+
projectRoot: project.root,
|
|
793
|
+
archivePath: result.archivePath,
|
|
794
|
+
fileCount: result.fileCount,
|
|
795
|
+
includeBlockTree: result.includeBlockTree
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/commands/blocks.ts
|
|
800
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
801
|
+
import { createRequire as createRequire2 } from "module";
|
|
802
|
+
function loadBlockCatalog() {
|
|
803
|
+
const require3 = createRequire2(import.meta.url);
|
|
804
|
+
const catalogPath = require3.resolve("@lessonkit/react/block-catalog.v3.json");
|
|
805
|
+
return JSON.parse(readFileSync2(catalogPath, "utf8"));
|
|
806
|
+
}
|
|
807
|
+
function filterEntries(entries, opts) {
|
|
808
|
+
return entries.filter((entry) => {
|
|
809
|
+
if (opts.category && entry.category !== opts.category) return false;
|
|
810
|
+
if (opts.tier && entry.tier !== opts.tier) return false;
|
|
811
|
+
return true;
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async function runBlocksList(opts) {
|
|
815
|
+
const catalog = loadBlockCatalog();
|
|
816
|
+
const entries = filterEntries(catalog.entries, opts);
|
|
817
|
+
if (!opts.json) {
|
|
818
|
+
const lines = [
|
|
819
|
+
"type category h5pMachineName",
|
|
820
|
+
...entries.map(
|
|
821
|
+
(entry) => [
|
|
822
|
+
entry.type,
|
|
823
|
+
entry.category ?? "\u2014",
|
|
824
|
+
entry.h5pMachineName ?? "\u2014"
|
|
825
|
+
].join(" ")
|
|
826
|
+
)
|
|
827
|
+
];
|
|
828
|
+
return {
|
|
829
|
+
ok: true,
|
|
830
|
+
command: "blocks list",
|
|
831
|
+
schemaVersion: catalog.schemaVersion,
|
|
832
|
+
count: entries.length,
|
|
833
|
+
text: lines.join("\n")
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
ok: true,
|
|
838
|
+
command: "blocks list",
|
|
839
|
+
schemaVersion: catalog.schemaVersion,
|
|
840
|
+
count: entries.length,
|
|
841
|
+
entries
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
588
845
|
// src/lib/logger.ts
|
|
589
846
|
function createLogger(opts) {
|
|
590
847
|
if (opts?.json) {
|
|
@@ -599,7 +856,7 @@ function createLogger(opts) {
|
|
|
599
856
|
}
|
|
600
857
|
|
|
601
858
|
// src/index.ts
|
|
602
|
-
var require2 =
|
|
859
|
+
var require2 = createRequire3(import.meta.url);
|
|
603
860
|
var { version } = require2("../package.json");
|
|
604
861
|
async function handleCommand(fn, logger, json) {
|
|
605
862
|
try {
|
|
@@ -656,7 +913,7 @@ function createProgram(baseLogger = console) {
|
|
|
656
913
|
);
|
|
657
914
|
}
|
|
658
915
|
);
|
|
659
|
-
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) => {
|
|
916
|
+
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("--strict-parity", "Treat React ID parity warnings as packaging errors").option("--strict", "Treat Vite build warnings as packaging failures").option("--json", "Emit structured JSON result").action(async (opts) => {
|
|
660
917
|
const logger = createLogger({ json: opts.json });
|
|
661
918
|
await handleCommand(
|
|
662
919
|
async () => {
|
|
@@ -665,7 +922,9 @@ function createProgram(baseLogger = console) {
|
|
|
665
922
|
cwd: opts.cwd,
|
|
666
923
|
noBuild: opts.build === false,
|
|
667
924
|
out: opts.out,
|
|
668
|
-
json: opts.json
|
|
925
|
+
json: opts.json,
|
|
926
|
+
strictParity: opts.strictParity,
|
|
927
|
+
strict: opts.strict
|
|
669
928
|
});
|
|
670
929
|
if (!opts.json && result.ok && result.command === "package") {
|
|
671
930
|
if (result.target === "react-vite") {
|
|
@@ -683,8 +942,50 @@ function createProgram(baseLogger = console) {
|
|
|
683
942
|
Boolean(opts.json)
|
|
684
943
|
);
|
|
685
944
|
});
|
|
686
|
-
|
|
687
|
-
|
|
945
|
+
addCwdAndJson(
|
|
946
|
+
program.command("export").description("Export a portable .lkcourse archive (manifest + interchange + dist)").option("--out <path>", "Output .lkcourse path (relative to project root)").option("--no-build", "Skip implicit Vite build").option("--with-block-tree", "Include optional block-tree.json from src scan")
|
|
947
|
+
).action(async (opts) => {
|
|
948
|
+
const logger = createLogger({ json: opts.json });
|
|
949
|
+
await handleCommand(
|
|
950
|
+
async () => {
|
|
951
|
+
const result = await runExport({
|
|
952
|
+
cwd: opts.cwd,
|
|
953
|
+
out: opts.out,
|
|
954
|
+
noBuild: opts.build === false,
|
|
955
|
+
withBlockTree: opts.withBlockTree,
|
|
956
|
+
json: opts.json
|
|
957
|
+
});
|
|
958
|
+
if (!opts.json && result.ok && result.command === "export") {
|
|
959
|
+
logger.log(`Exported .lkcourse \u2192 ${result.archivePath} (${result.fileCount} files)`);
|
|
960
|
+
}
|
|
961
|
+
return result;
|
|
962
|
+
},
|
|
963
|
+
logger,
|
|
964
|
+
Boolean(opts.json)
|
|
965
|
+
);
|
|
966
|
+
});
|
|
967
|
+
program.command("blocks").description("Block registry commands").command("list").description("List runtime blocks from block-catalog.v3.json").option("--json", "Emit structured JSON result").option("--category <category>", "Filter by category (container, assessment, content, compound)").option("--tier <tier>", "Filter by tier (A, B, C, D, E)").action(async (opts) => {
|
|
968
|
+
const logger = createLogger({ json: opts.json });
|
|
969
|
+
await handleCommand(
|
|
970
|
+
async () => {
|
|
971
|
+
const result = await runBlocksList({
|
|
972
|
+
json: opts.json,
|
|
973
|
+
category: opts.category,
|
|
974
|
+
tier: opts.tier
|
|
975
|
+
});
|
|
976
|
+
if (!opts.json && result.ok && "text" in result && typeof result.text === "string") {
|
|
977
|
+
logger.log(result.text);
|
|
978
|
+
}
|
|
979
|
+
return result;
|
|
980
|
+
},
|
|
981
|
+
logger,
|
|
982
|
+
Boolean(opts.json)
|
|
983
|
+
);
|
|
984
|
+
});
|
|
985
|
+
program.command("publish").description("[maintainers] Not implemented \u2014 use Changesets (see RELEASING.md)").action(() => {
|
|
986
|
+
baseLogger.log(
|
|
987
|
+
"lessonkit publish is not implemented. Monorepo releases use Changesets: npm run changeset && npm run version-packages && npm run release. See RELEASING.md."
|
|
988
|
+
);
|
|
688
989
|
});
|
|
689
990
|
return program;
|
|
690
991
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,14 @@ type CliLogger = {
|
|
|
5
5
|
error: (...args: unknown[]) => void;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Build the Commander program used by the `lessonkit` CLI binary.
|
|
10
|
+
* Useful for embedding init/build/package in Node scripts and tests.
|
|
11
|
+
*/
|
|
8
12
|
declare function createProgram(baseLogger?: CliLogger): Command;
|
|
13
|
+
/**
|
|
14
|
+
* Parse argv and run the LessonKit CLI (same as the `lessonkit` binary entrypoint).
|
|
15
|
+
*/
|
|
9
16
|
declare function run(argv?: string[], logger?: CliLogger): Promise<void>;
|
|
10
17
|
|
|
11
18
|
export { type CliLogger, createProgram, run };
|