@lessonkit/cli 1.5.0 → 1.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 +2 -2
- package/dist/bin.js +360 -26
- package/dist/index.js +360 -26
- package/package.json +6 -5
- package/template/vite-react/README.md +2 -2
- package/template/vite-react/package.json +6 -6
- package/template/vite-react/src/courseConfig.test.ts +5 -0
- package/template/vite-react/src/courseConfig.ts +3 -0
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ lessonkit package --target scorm12 # LMS artifact
|
|
|
30
30
|
| Flag | Purpose |
|
|
31
31
|
| --- | --- |
|
|
32
32
|
| `--here` | Scaffold in the current directory |
|
|
33
|
-
| `--force` |
|
|
33
|
+
| `--force` | With `--here`: scaffold in a non-empty directory; conflicting template files (e.g. `.gitignore`, `README.md`) are backed up to `.lessonkit-init-backup/` before overwrite. Non-conflicting files are kept. |
|
|
34
34
|
| `--skip-install` | Skip `npm install` after copying the template |
|
|
35
35
|
|
|
36
36
|
### Package targets
|
|
@@ -57,7 +57,7 @@ Subprocess timeout defaults to **30 minutes** (`LESSONKIT_CMD_TIMEOUT_MS`).
|
|
|
57
57
|
|
|
58
58
|
## Docs
|
|
59
59
|
|
|
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) · [
|
|
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) · [LMS Go-Live](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/lms-go-live.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))
|
|
61
61
|
|
|
62
62
|
## License
|
|
63
63
|
|
package/dist/bin.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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, rename, rm, writeFile } from "fs/promises";
|
|
9
|
+
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
10
10
|
import { existsSync } from "fs";
|
|
11
11
|
import { randomUUID } from "crypto";
|
|
12
12
|
import { basename, dirname, join, resolve } from "path";
|
|
@@ -127,6 +127,7 @@ async function runNpmInstall(cwd) {
|
|
|
127
127
|
// src/commands/init.ts
|
|
128
128
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git", "coverage", ".nyc_output"]);
|
|
129
129
|
var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
|
|
130
|
+
var INIT_BACKUP_DIR = ".lessonkit-init-backup";
|
|
130
131
|
function getTemplateDir() {
|
|
131
132
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
132
133
|
const candidates = [
|
|
@@ -138,6 +139,11 @@ function getTemplateDir() {
|
|
|
138
139
|
}
|
|
139
140
|
return candidates[0];
|
|
140
141
|
}
|
|
142
|
+
async function isDirEmpty(dir) {
|
|
143
|
+
if (!existsSync(dir)) return true;
|
|
144
|
+
const entries = await readdir(dir);
|
|
145
|
+
return entries.length === 0;
|
|
146
|
+
}
|
|
141
147
|
async function isDirEmptyOrDotfilesOnly(dir) {
|
|
142
148
|
if (!existsSync(dir)) return true;
|
|
143
149
|
const entries = await readdir(dir);
|
|
@@ -190,12 +196,128 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
190
196
|
appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
|
|
191
197
|
await writeFile(appPath, appSource, "utf8");
|
|
192
198
|
}
|
|
199
|
+
function toPosixRelativePath(relativePath) {
|
|
200
|
+
return relativePath.replace(/\\/g, "/");
|
|
201
|
+
}
|
|
202
|
+
async function listStagingFiles(stagingDir, relativeDir = "") {
|
|
203
|
+
const dir = relativeDir ? join(stagingDir, relativeDir) : stagingDir;
|
|
204
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
205
|
+
const files = [];
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
if (SKIP_DIRS.has(entry.name) || SKIP_FILES.has(entry.name)) continue;
|
|
208
|
+
const rel = relativeDir ? join(relativeDir, entry.name) : entry.name;
|
|
209
|
+
const relPosix = toPosixRelativePath(rel);
|
|
210
|
+
if (entry.isDirectory()) {
|
|
211
|
+
files.push(...await listStagingFiles(stagingDir, rel));
|
|
212
|
+
} else if (entry.isFile()) {
|
|
213
|
+
files.push(relPosix);
|
|
214
|
+
} else {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return files;
|
|
218
|
+
}
|
|
219
|
+
async function listProjectFiles(projectDir, relativeDir = "") {
|
|
220
|
+
const files = /* @__PURE__ */ new Set();
|
|
221
|
+
const dir = relativeDir ? join(projectDir, relativeDir) : projectDir;
|
|
222
|
+
if (!existsSync(dir)) return files;
|
|
223
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
if (relativeDir === "" && entry.name === INIT_BACKUP_DIR) continue;
|
|
226
|
+
if (relativeDir === "" && entry.name.startsWith(".lessonkit-init-")) continue;
|
|
227
|
+
if (SKIP_DIRS.has(entry.name) || SKIP_FILES.has(entry.name)) continue;
|
|
228
|
+
const rel = relativeDir ? join(relativeDir, entry.name) : entry.name;
|
|
229
|
+
const relPosix = toPosixRelativePath(rel);
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
for (const nested of await listProjectFiles(projectDir, rel)) {
|
|
232
|
+
files.add(nested);
|
|
233
|
+
}
|
|
234
|
+
} else if (entry.isFile()) {
|
|
235
|
+
files.add(relPosix);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return files;
|
|
239
|
+
}
|
|
240
|
+
async function backupConflictingFiles(stagingDir, projectDir) {
|
|
241
|
+
const backups = /* @__PURE__ */ new Map();
|
|
242
|
+
for (const relPath of await listStagingFiles(stagingDir)) {
|
|
243
|
+
const destPath = join(projectDir, relPath);
|
|
244
|
+
if (!existsSync(destPath)) continue;
|
|
245
|
+
const destStat = await stat(destPath);
|
|
246
|
+
if (destStat.isFile()) {
|
|
247
|
+
backups.set(relPath, await readFile(destPath));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return backups;
|
|
251
|
+
}
|
|
252
|
+
async function writeInitBackupDir(projectDir, backups) {
|
|
253
|
+
const backupDir = join(projectDir, INIT_BACKUP_DIR);
|
|
254
|
+
await mkdir(backupDir, { recursive: true });
|
|
255
|
+
for (const [relPath, content] of backups) {
|
|
256
|
+
const outPath = join(backupDir, relPath);
|
|
257
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
258
|
+
await writeFile(outPath, content);
|
|
259
|
+
}
|
|
260
|
+
return backupDir;
|
|
261
|
+
}
|
|
262
|
+
async function rollbackPromotedFiles(projectDir, stagingDir, preExistingRoots, preExistingFiles, backups) {
|
|
263
|
+
const failures = [];
|
|
264
|
+
const stagingFiles = await listStagingFiles(stagingDir);
|
|
265
|
+
for (const relPath of stagingFiles) {
|
|
266
|
+
if (backups.has(relPath) || preExistingFiles.has(relPath)) continue;
|
|
267
|
+
try {
|
|
268
|
+
await rm(join(projectDir, relPath), { force: true });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
failures.push(`remove ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let stagingEntries;
|
|
274
|
+
try {
|
|
275
|
+
stagingEntries = await readdir(stagingDir, { withFileTypes: true });
|
|
276
|
+
} catch {
|
|
277
|
+
if (failures.length > 0) {
|
|
278
|
+
throw new CliError(`Init rollback failed: ${failures.join("; ")}`, {
|
|
279
|
+
code: "RUNTIME",
|
|
280
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const entry of stagingEntries) {
|
|
286
|
+
if (preExistingRoots.has(entry.name)) continue;
|
|
287
|
+
try {
|
|
288
|
+
await rm(join(projectDir, entry.name), { recursive: true, force: true });
|
|
289
|
+
} catch (err) {
|
|
290
|
+
failures.push(
|
|
291
|
+
`remove ${entry.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const [relPath, content] of backups) {
|
|
296
|
+
try {
|
|
297
|
+
const destPath = join(projectDir, relPath);
|
|
298
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
299
|
+
await writeFile(destPath, content);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
failures.push(`restore ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (failures.length > 0) {
|
|
305
|
+
throw new CliError(`Init rollback failed: ${failures.join("; ")}`, {
|
|
306
|
+
code: "RUNTIME",
|
|
307
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
var PROMOTE_REPLACE_ENTRIES = /* @__PURE__ */ new Set(["node_modules", "package-lock.json"]);
|
|
193
312
|
async function promoteStagingToProjectDir(stagingDir, projectDir) {
|
|
194
313
|
await mkdir(projectDir, { recursive: true });
|
|
195
314
|
const entries = await readdir(stagingDir, { withFileTypes: true });
|
|
196
315
|
for (const entry of entries) {
|
|
197
316
|
const srcPath = join(stagingDir, entry.name);
|
|
198
317
|
const destPath = join(projectDir, entry.name);
|
|
318
|
+
if (PROMOTE_REPLACE_ENTRIES.has(entry.name) && existsSync(destPath)) {
|
|
319
|
+
await rm(destPath, { recursive: true, force: true });
|
|
320
|
+
}
|
|
199
321
|
if (entry.isDirectory()) {
|
|
200
322
|
await cp(srcPath, destPath, { recursive: true });
|
|
201
323
|
} else if (entry.isFile()) {
|
|
@@ -204,6 +326,20 @@ async function promoteStagingToProjectDir(stagingDir, projectDir) {
|
|
|
204
326
|
}
|
|
205
327
|
}
|
|
206
328
|
}
|
|
329
|
+
var __testInitHelpers = {
|
|
330
|
+
getTemplateDir,
|
|
331
|
+
isDirEmpty,
|
|
332
|
+
isDirEmptyOrDotfilesOnly,
|
|
333
|
+
escapeJsxString,
|
|
334
|
+
copyTemplate,
|
|
335
|
+
promoteStagingToProjectDir,
|
|
336
|
+
rollbackPromotedFiles,
|
|
337
|
+
backupConflictingFiles,
|
|
338
|
+
writeInitBackupDir,
|
|
339
|
+
listStagingFiles,
|
|
340
|
+
listProjectFiles,
|
|
341
|
+
INIT_BACKUP_DIR
|
|
342
|
+
};
|
|
207
343
|
async function runInit(opts, logger) {
|
|
208
344
|
const cwd = process.cwd();
|
|
209
345
|
const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
|
|
@@ -232,14 +368,8 @@ async function runInit(opts, logger) {
|
|
|
232
368
|
);
|
|
233
369
|
}
|
|
234
370
|
if (opts.here && !await isDirEmptyOrDotfilesOnly(projectDir) && !opts.force) {
|
|
235
|
-
throw new CliError(`Directory is not empty: ${projectDir}. Use --force to initialize anyway.`, {
|
|
236
|
-
code: "INVALID_PROJECT",
|
|
237
|
-
exitCode: EXIT_INVALID_PROJECT
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
if (opts.here && opts.force && !await isDirEmptyOrDotfilesOnly(projectDir)) {
|
|
241
371
|
throw new CliError(
|
|
242
|
-
`Directory is not empty: ${projectDir}. --force
|
|
372
|
+
`Directory is not empty: ${projectDir}. Use --here --force to scaffold anyway (conflicting files are backed up under ${INIT_BACKUP_DIR}/).`,
|
|
243
373
|
{
|
|
244
374
|
code: "INVALID_PROJECT",
|
|
245
375
|
exitCode: EXIT_INVALID_PROJECT
|
|
@@ -262,7 +392,48 @@ async function runInit(opts, logger) {
|
|
|
262
392
|
await runNpmInstall(stagingDir);
|
|
263
393
|
}
|
|
264
394
|
if (opts.here) {
|
|
265
|
-
await
|
|
395
|
+
const preExistingRoots = new Set(await readdir(projectDir));
|
|
396
|
+
const preExistingFiles = await listProjectFiles(projectDir);
|
|
397
|
+
const backups = await backupConflictingFiles(stagingDir, projectDir);
|
|
398
|
+
const conflicts = [...backups.keys()].sort();
|
|
399
|
+
if (conflicts.length > 0 && !opts.force) {
|
|
400
|
+
throw new CliError(
|
|
401
|
+
`Would overwrite existing file(s): ${conflicts.join(", ")}. Re-run with --force to back them up under ${INIT_BACKUP_DIR}/ and continue.`,
|
|
402
|
+
{
|
|
403
|
+
code: "INVALID_PROJECT",
|
|
404
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (conflicts.length > 0 && opts.force && !opts.json) {
|
|
409
|
+
const backupDir = await writeInitBackupDir(projectDir, backups);
|
|
410
|
+
logger.log(
|
|
411
|
+
`Backed up ${conflicts.length} conflicting file(s) to ${backupDir}: ${conflicts.join(", ")}`
|
|
412
|
+
);
|
|
413
|
+
} else if (conflicts.length > 0 && opts.force) {
|
|
414
|
+
await writeInitBackupDir(projectDir, backups);
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
await __testInitHelpers.promoteStagingToProjectDir(stagingDir, projectDir);
|
|
418
|
+
} catch (promoteErr) {
|
|
419
|
+
try {
|
|
420
|
+
await rollbackPromotedFiles(
|
|
421
|
+
projectDir,
|
|
422
|
+
stagingDir,
|
|
423
|
+
preExistingRoots,
|
|
424
|
+
preExistingFiles,
|
|
425
|
+
backups
|
|
426
|
+
);
|
|
427
|
+
} catch (rollbackErr) {
|
|
428
|
+
const promoteMessage = promoteErr instanceof Error ? promoteErr.message : String(promoteErr);
|
|
429
|
+
const rollbackMessage = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
430
|
+
throw new CliError(`${promoteMessage}; ${rollbackMessage}`, {
|
|
431
|
+
code: "RUNTIME",
|
|
432
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
throw promoteErr;
|
|
436
|
+
}
|
|
266
437
|
await rm(stagingDir, { recursive: true, force: true });
|
|
267
438
|
} else {
|
|
268
439
|
await rename(stagingDir, projectDir);
|
|
@@ -283,7 +454,9 @@ async function runInit(opts, logger) {
|
|
|
283
454
|
|
|
284
455
|
// src/commands/dev.ts
|
|
285
456
|
import { existsSync as existsSync3 } from "fs";
|
|
286
|
-
import {
|
|
457
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
458
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
459
|
+
import { assertSpaDistContentsSafe } from "@lessonkit/lxpack";
|
|
287
460
|
|
|
288
461
|
// src/lib/project.ts
|
|
289
462
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
@@ -293,10 +466,13 @@ import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "
|
|
|
293
466
|
import { parseLessonkitManifest } from "@lessonkit/lxpack";
|
|
294
467
|
var LESSONKIT_JSON = "lessonkit.json";
|
|
295
468
|
var PACKAGE_JSON = "package.json";
|
|
469
|
+
function isSchemaVersionOne(value) {
|
|
470
|
+
return value === 1 || value === "1";
|
|
471
|
+
}
|
|
296
472
|
function isProjectManifest(configPath) {
|
|
297
473
|
try {
|
|
298
474
|
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
299
|
-
return raw.schemaVersion
|
|
475
|
+
return isSchemaVersionOne(raw.schemaVersion) && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
|
|
300
476
|
} catch {
|
|
301
477
|
return false;
|
|
302
478
|
}
|
|
@@ -469,7 +645,12 @@ function resolvePackageOutput(project, target, override) {
|
|
|
469
645
|
if (override) {
|
|
470
646
|
try {
|
|
471
647
|
const resolved = resolveSafePackageOutputOverride(project.root, override);
|
|
472
|
-
|
|
648
|
+
const isZipOutput = override.trim().toLowerCase().endsWith(".zip");
|
|
649
|
+
return {
|
|
650
|
+
output: resolved,
|
|
651
|
+
dir: target === "standalone" && !isZipOutput,
|
|
652
|
+
outputBaseDir
|
|
653
|
+
};
|
|
473
654
|
} catch (err) {
|
|
474
655
|
const message = err instanceof Error ? err.message : String(err);
|
|
475
656
|
throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
|
|
@@ -515,7 +696,8 @@ async function runDev(opts) {
|
|
|
515
696
|
const pkg = await readPackageJson(project.root);
|
|
516
697
|
assertViteProject(pkg, project.root);
|
|
517
698
|
const viteJs = resolveViteJs(project.root);
|
|
518
|
-
|
|
699
|
+
const devArgs = stripOutDirFromViteArgs(opts.viteArgs ?? []);
|
|
700
|
+
await runCommand(process.execPath, [viteJs, ...devArgs], {
|
|
519
701
|
cwd: project.root,
|
|
520
702
|
timeoutMs: 0
|
|
521
703
|
});
|
|
@@ -526,11 +708,15 @@ async function runBuild(opts) {
|
|
|
526
708
|
const pkg = await readPackageJson(project.root);
|
|
527
709
|
assertViteProject(pkg, project.root);
|
|
528
710
|
const viteJs = resolveViteJs(project.root);
|
|
711
|
+
const distDir = resolveDistDir(project);
|
|
712
|
+
await mkdir2(dirname3(distDir), { recursive: true });
|
|
713
|
+
if (existsSync3(distDir)) {
|
|
714
|
+
await assertSpaDistContentsSafe({ main: distDir }, project.root);
|
|
715
|
+
}
|
|
529
716
|
const buildArgs = resolveViteBuildArgv(project, opts.viteArgs);
|
|
530
717
|
await runCommand(process.execPath, [viteJs, ...buildArgs], {
|
|
531
718
|
cwd: project.root
|
|
532
719
|
});
|
|
533
|
-
const distDir = resolveDistDir(project);
|
|
534
720
|
const indexHtml = join3(distDir, "index.html");
|
|
535
721
|
if (!existsSync3(indexHtml)) {
|
|
536
722
|
throw new CliError(
|
|
@@ -538,13 +724,13 @@ async function runBuild(opts) {
|
|
|
538
724
|
{ code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
|
|
539
725
|
);
|
|
540
726
|
}
|
|
727
|
+
await assertSpaDistContentsSafe({ main: distDir }, project.root);
|
|
541
728
|
return { ok: true, command: "build", projectRoot: project.root };
|
|
542
729
|
}
|
|
543
730
|
|
|
544
731
|
// src/commands/package.ts
|
|
545
732
|
import { existsSync as existsSync4 } from "fs";
|
|
546
|
-
import {
|
|
547
|
-
import { packageLessonkitCourse } from "@lessonkit/lxpack";
|
|
733
|
+
import { assertSpaDistContentsSafe as assertSpaDistContentsSafe2, packageLessonkitCourse } from "@lessonkit/lxpack";
|
|
548
734
|
async function runPackage(opts) {
|
|
549
735
|
let target;
|
|
550
736
|
try {
|
|
@@ -580,6 +766,7 @@ async function runPackage(opts) {
|
|
|
580
766
|
exitCode: EXIT_INVALID_PROJECT
|
|
581
767
|
});
|
|
582
768
|
}
|
|
769
|
+
await assertSpaDistContentsSafe2({ main: distDir }, project.root);
|
|
583
770
|
return { ok: true, command: "package", target, projectRoot: project.root, distDir };
|
|
584
771
|
}
|
|
585
772
|
assertNode18ForLxpack();
|
|
@@ -593,23 +780,23 @@ async function runPackage(opts) {
|
|
|
593
780
|
});
|
|
594
781
|
}
|
|
595
782
|
const outDir = resolveLxpackOutDir(project);
|
|
783
|
+
const trimmedOut = opts.out?.trim();
|
|
596
784
|
const { output: resolvedOutput, dir, outputBaseDir } = resolvePackageOutput(
|
|
597
785
|
project,
|
|
598
786
|
target,
|
|
599
|
-
|
|
787
|
+
trimmedOut
|
|
600
788
|
);
|
|
601
|
-
const trimmedOut = opts.out?.trim();
|
|
602
|
-
const output = trimmedOut && !isAbsolute(trimmedOut) ? trimmedOut : resolvedOutput;
|
|
603
789
|
const result = await packageLessonkitCourse({
|
|
604
790
|
descriptor: project.course,
|
|
605
791
|
outDir,
|
|
606
792
|
spaDistDir: distDir,
|
|
607
793
|
projectRoot: project.root,
|
|
608
794
|
target,
|
|
609
|
-
output,
|
|
795
|
+
output: trimmedOut ? resolvedOutput : void 0,
|
|
610
796
|
dir,
|
|
611
797
|
outputBaseDir,
|
|
612
|
-
strictParity: opts.strictParity
|
|
798
|
+
strictParity: opts.strictParity,
|
|
799
|
+
strictBuild: opts.strict
|
|
613
800
|
});
|
|
614
801
|
if (!result.ok) {
|
|
615
802
|
throw new CliError("Packaging failed.", {
|
|
@@ -641,6 +828,112 @@ async function runPackage(opts) {
|
|
|
641
828
|
};
|
|
642
829
|
}
|
|
643
830
|
|
|
831
|
+
// src/commands/export.ts
|
|
832
|
+
import { existsSync as existsSync5 } from "fs";
|
|
833
|
+
import { relative, resolve as resolve4 } from "path";
|
|
834
|
+
import { exportLkcourse, resolveSafePackageOutputOverride as resolveSafePackageOutputOverride2 } from "@lessonkit/lxpack";
|
|
835
|
+
function resolveExportOutput(projectRoot, override, defaultName) {
|
|
836
|
+
if (override) {
|
|
837
|
+
try {
|
|
838
|
+
return resolveSafePackageOutputOverride2(projectRoot, override);
|
|
839
|
+
} catch (err) {
|
|
840
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
841
|
+
throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return resolve4(projectRoot, `${defaultName ?? "course"}.lkcourse`);
|
|
845
|
+
}
|
|
846
|
+
async function runExport(opts) {
|
|
847
|
+
const project = await loadProject(opts.cwd ?? process.cwd());
|
|
848
|
+
const distDir = resolve4(project.root, project.paths.spaDistDir);
|
|
849
|
+
if (opts.noBuild && !existsSync5(distDir)) {
|
|
850
|
+
throw new CliError(
|
|
851
|
+
`dist directory not found at ${distDir}. Run lessonkit build before export with --no-build.`,
|
|
852
|
+
{
|
|
853
|
+
code: "INVALID_PROJECT",
|
|
854
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (!opts.noBuild) {
|
|
859
|
+
await runBuild({ cwd: project.root, json: opts.json });
|
|
860
|
+
}
|
|
861
|
+
if (!existsSync5(distDir)) {
|
|
862
|
+
throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
|
|
863
|
+
code: "INVALID_PROJECT",
|
|
864
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
const resolvedOut = resolveExportOutput(project.root, opts.out, project.name);
|
|
868
|
+
const outRelative = relative(project.root, resolvedOut).replace(/\\/g, "/");
|
|
869
|
+
const result = await exportLkcourse({
|
|
870
|
+
projectRoot: project.root,
|
|
871
|
+
manifest: project,
|
|
872
|
+
outPath: outRelative,
|
|
873
|
+
includeBlockTree: Boolean(opts.withBlockTree)
|
|
874
|
+
});
|
|
875
|
+
if (!result.ok) {
|
|
876
|
+
throw new CliError(
|
|
877
|
+
result.issues.map((i) => `${i.path}: ${i.message}`).join("; "),
|
|
878
|
+
{ code: "EXPORT_FAILED", exitCode: EXIT_PACKAGING }
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
ok: true,
|
|
883
|
+
command: "export",
|
|
884
|
+
projectRoot: project.root,
|
|
885
|
+
archivePath: result.archivePath,
|
|
886
|
+
fileCount: result.fileCount,
|
|
887
|
+
includeBlockTree: result.includeBlockTree
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/commands/blocks.ts
|
|
892
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
893
|
+
import { createRequire as createRequire2 } from "module";
|
|
894
|
+
function loadBlockCatalog() {
|
|
895
|
+
const require3 = createRequire2(import.meta.url);
|
|
896
|
+
const catalogPath = require3.resolve("@lessonkit/react/block-catalog.v3.json");
|
|
897
|
+
return JSON.parse(readFileSync2(catalogPath, "utf8"));
|
|
898
|
+
}
|
|
899
|
+
function filterEntries(entries, opts) {
|
|
900
|
+
return entries.filter((entry) => {
|
|
901
|
+
if (opts.category && entry.category !== opts.category) return false;
|
|
902
|
+
if (opts.tier && entry.tier !== opts.tier) return false;
|
|
903
|
+
return true;
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
async function runBlocksList(opts) {
|
|
907
|
+
const catalog = loadBlockCatalog();
|
|
908
|
+
const entries = filterEntries(catalog.entries, opts);
|
|
909
|
+
if (!opts.json) {
|
|
910
|
+
const lines = [
|
|
911
|
+
"type category h5pMachineName",
|
|
912
|
+
...entries.map(
|
|
913
|
+
(entry) => [
|
|
914
|
+
entry.type,
|
|
915
|
+
entry.category ?? "\u2014",
|
|
916
|
+
entry.h5pMachineName ?? "\u2014"
|
|
917
|
+
].join(" ")
|
|
918
|
+
)
|
|
919
|
+
];
|
|
920
|
+
return {
|
|
921
|
+
ok: true,
|
|
922
|
+
command: "blocks list",
|
|
923
|
+
schemaVersion: catalog.schemaVersion,
|
|
924
|
+
count: entries.length,
|
|
925
|
+
text: lines.join("\n")
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
ok: true,
|
|
930
|
+
command: "blocks list",
|
|
931
|
+
schemaVersion: catalog.schemaVersion,
|
|
932
|
+
count: entries.length,
|
|
933
|
+
entries
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
644
937
|
// src/lib/logger.ts
|
|
645
938
|
function createLogger(opts) {
|
|
646
939
|
if (opts?.json) {
|
|
@@ -655,7 +948,7 @@ function createLogger(opts) {
|
|
|
655
948
|
}
|
|
656
949
|
|
|
657
950
|
// src/index.ts
|
|
658
|
-
var require2 =
|
|
951
|
+
var require2 = createRequire3(import.meta.url);
|
|
659
952
|
var { version } = require2("../package.json");
|
|
660
953
|
async function handleCommand(fn, logger, json) {
|
|
661
954
|
try {
|
|
@@ -678,7 +971,7 @@ function createProgram(baseLogger = console) {
|
|
|
678
971
|
program.name("lessonkit").description("LessonKit CLI").version(version);
|
|
679
972
|
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(
|
|
680
973
|
"--force",
|
|
681
|
-
"
|
|
974
|
+
"With --here: scaffold in a non-empty directory; back up conflicting template paths under .lessonkit-init-backup/ before overwrite"
|
|
682
975
|
).option("--json", "Emit structured JSON result").action(async (name, opts) => {
|
|
683
976
|
const logger = createLogger({ json: opts.json });
|
|
684
977
|
await handleCommand(
|
|
@@ -712,7 +1005,7 @@ function createProgram(baseLogger = console) {
|
|
|
712
1005
|
);
|
|
713
1006
|
}
|
|
714
1007
|
);
|
|
715
|
-
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("--json", "Emit structured JSON result").action(async (opts) => {
|
|
1008
|
+
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) => {
|
|
716
1009
|
const logger = createLogger({ json: opts.json });
|
|
717
1010
|
await handleCommand(
|
|
718
1011
|
async () => {
|
|
@@ -722,7 +1015,8 @@ function createProgram(baseLogger = console) {
|
|
|
722
1015
|
noBuild: opts.build === false,
|
|
723
1016
|
out: opts.out,
|
|
724
1017
|
json: opts.json,
|
|
725
|
-
strictParity: opts.strictParity
|
|
1018
|
+
strictParity: opts.strictParity,
|
|
1019
|
+
strict: opts.strict
|
|
726
1020
|
});
|
|
727
1021
|
if (!opts.json && result.ok && result.command === "package") {
|
|
728
1022
|
if (result.target === "react-vite") {
|
|
@@ -740,6 +1034,46 @@ function createProgram(baseLogger = console) {
|
|
|
740
1034
|
Boolean(opts.json)
|
|
741
1035
|
);
|
|
742
1036
|
});
|
|
1037
|
+
addCwdAndJson(
|
|
1038
|
+
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")
|
|
1039
|
+
).action(async (opts) => {
|
|
1040
|
+
const logger = createLogger({ json: opts.json });
|
|
1041
|
+
await handleCommand(
|
|
1042
|
+
async () => {
|
|
1043
|
+
const result = await runExport({
|
|
1044
|
+
cwd: opts.cwd,
|
|
1045
|
+
out: opts.out,
|
|
1046
|
+
noBuild: opts.build === false,
|
|
1047
|
+
withBlockTree: opts.withBlockTree,
|
|
1048
|
+
json: opts.json
|
|
1049
|
+
});
|
|
1050
|
+
if (!opts.json && result.ok && result.command === "export") {
|
|
1051
|
+
logger.log(`Exported .lkcourse \u2192 ${result.archivePath} (${result.fileCount} files)`);
|
|
1052
|
+
}
|
|
1053
|
+
return result;
|
|
1054
|
+
},
|
|
1055
|
+
logger,
|
|
1056
|
+
Boolean(opts.json)
|
|
1057
|
+
);
|
|
1058
|
+
});
|
|
1059
|
+
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) => {
|
|
1060
|
+
const logger = createLogger({ json: opts.json });
|
|
1061
|
+
await handleCommand(
|
|
1062
|
+
async () => {
|
|
1063
|
+
const result = await runBlocksList({
|
|
1064
|
+
json: opts.json,
|
|
1065
|
+
category: opts.category,
|
|
1066
|
+
tier: opts.tier
|
|
1067
|
+
});
|
|
1068
|
+
if (!opts.json && result.ok && "text" in result && typeof result.text === "string") {
|
|
1069
|
+
logger.log(result.text);
|
|
1070
|
+
}
|
|
1071
|
+
return result;
|
|
1072
|
+
},
|
|
1073
|
+
logger,
|
|
1074
|
+
Boolean(opts.json)
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
743
1077
|
program.command("publish").description("[maintainers] Not implemented \u2014 use Changesets (see RELEASING.md)").action(() => {
|
|
744
1078
|
baseLogger.log(
|
|
745
1079
|
"lessonkit publish is not implemented. Monorepo releases use Changesets: npm run changeset && npm run version-packages && npm run release. See RELEASING.md."
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createRequire as
|
|
2
|
+
import { createRequire as createRequire3 } from "module";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
|
|
5
5
|
// src/commands/init.ts
|
|
6
6
|
import { slugifyId } from "@lessonkit/core";
|
|
7
|
-
import { cp, mkdir, readdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
7
|
+
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
8
8
|
import { existsSync } from "fs";
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
10
10
|
import { basename, dirname, join, resolve } from "path";
|
|
@@ -125,6 +125,7 @@ async function runNpmInstall(cwd) {
|
|
|
125
125
|
// src/commands/init.ts
|
|
126
126
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git", "coverage", ".nyc_output"]);
|
|
127
127
|
var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
|
|
128
|
+
var INIT_BACKUP_DIR = ".lessonkit-init-backup";
|
|
128
129
|
function getTemplateDir() {
|
|
129
130
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
130
131
|
const candidates = [
|
|
@@ -136,6 +137,11 @@ function getTemplateDir() {
|
|
|
136
137
|
}
|
|
137
138
|
return candidates[0];
|
|
138
139
|
}
|
|
140
|
+
async function isDirEmpty(dir) {
|
|
141
|
+
if (!existsSync(dir)) return true;
|
|
142
|
+
const entries = await readdir(dir);
|
|
143
|
+
return entries.length === 0;
|
|
144
|
+
}
|
|
139
145
|
async function isDirEmptyOrDotfilesOnly(dir) {
|
|
140
146
|
if (!existsSync(dir)) return true;
|
|
141
147
|
const entries = await readdir(dir);
|
|
@@ -188,12 +194,128 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
|
|
|
188
194
|
appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
|
|
189
195
|
await writeFile(appPath, appSource, "utf8");
|
|
190
196
|
}
|
|
197
|
+
function toPosixRelativePath(relativePath) {
|
|
198
|
+
return relativePath.replace(/\\/g, "/");
|
|
199
|
+
}
|
|
200
|
+
async function listStagingFiles(stagingDir, relativeDir = "") {
|
|
201
|
+
const dir = relativeDir ? join(stagingDir, relativeDir) : stagingDir;
|
|
202
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
203
|
+
const files = [];
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (SKIP_DIRS.has(entry.name) || SKIP_FILES.has(entry.name)) continue;
|
|
206
|
+
const rel = relativeDir ? join(relativeDir, entry.name) : entry.name;
|
|
207
|
+
const relPosix = toPosixRelativePath(rel);
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
files.push(...await listStagingFiles(stagingDir, rel));
|
|
210
|
+
} else if (entry.isFile()) {
|
|
211
|
+
files.push(relPosix);
|
|
212
|
+
} else {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return files;
|
|
216
|
+
}
|
|
217
|
+
async function listProjectFiles(projectDir, relativeDir = "") {
|
|
218
|
+
const files = /* @__PURE__ */ new Set();
|
|
219
|
+
const dir = relativeDir ? join(projectDir, relativeDir) : projectDir;
|
|
220
|
+
if (!existsSync(dir)) return files;
|
|
221
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (relativeDir === "" && entry.name === INIT_BACKUP_DIR) continue;
|
|
224
|
+
if (relativeDir === "" && entry.name.startsWith(".lessonkit-init-")) continue;
|
|
225
|
+
if (SKIP_DIRS.has(entry.name) || SKIP_FILES.has(entry.name)) continue;
|
|
226
|
+
const rel = relativeDir ? join(relativeDir, entry.name) : entry.name;
|
|
227
|
+
const relPosix = toPosixRelativePath(rel);
|
|
228
|
+
if (entry.isDirectory()) {
|
|
229
|
+
for (const nested of await listProjectFiles(projectDir, rel)) {
|
|
230
|
+
files.add(nested);
|
|
231
|
+
}
|
|
232
|
+
} else if (entry.isFile()) {
|
|
233
|
+
files.add(relPosix);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return files;
|
|
237
|
+
}
|
|
238
|
+
async function backupConflictingFiles(stagingDir, projectDir) {
|
|
239
|
+
const backups = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const relPath of await listStagingFiles(stagingDir)) {
|
|
241
|
+
const destPath = join(projectDir, relPath);
|
|
242
|
+
if (!existsSync(destPath)) continue;
|
|
243
|
+
const destStat = await stat(destPath);
|
|
244
|
+
if (destStat.isFile()) {
|
|
245
|
+
backups.set(relPath, await readFile(destPath));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return backups;
|
|
249
|
+
}
|
|
250
|
+
async function writeInitBackupDir(projectDir, backups) {
|
|
251
|
+
const backupDir = join(projectDir, INIT_BACKUP_DIR);
|
|
252
|
+
await mkdir(backupDir, { recursive: true });
|
|
253
|
+
for (const [relPath, content] of backups) {
|
|
254
|
+
const outPath = join(backupDir, relPath);
|
|
255
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
256
|
+
await writeFile(outPath, content);
|
|
257
|
+
}
|
|
258
|
+
return backupDir;
|
|
259
|
+
}
|
|
260
|
+
async function rollbackPromotedFiles(projectDir, stagingDir, preExistingRoots, preExistingFiles, backups) {
|
|
261
|
+
const failures = [];
|
|
262
|
+
const stagingFiles = await listStagingFiles(stagingDir);
|
|
263
|
+
for (const relPath of stagingFiles) {
|
|
264
|
+
if (backups.has(relPath) || preExistingFiles.has(relPath)) continue;
|
|
265
|
+
try {
|
|
266
|
+
await rm(join(projectDir, relPath), { force: true });
|
|
267
|
+
} catch (err) {
|
|
268
|
+
failures.push(`remove ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
let stagingEntries;
|
|
272
|
+
try {
|
|
273
|
+
stagingEntries = await readdir(stagingDir, { withFileTypes: true });
|
|
274
|
+
} catch {
|
|
275
|
+
if (failures.length > 0) {
|
|
276
|
+
throw new CliError(`Init rollback failed: ${failures.join("; ")}`, {
|
|
277
|
+
code: "RUNTIME",
|
|
278
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
for (const entry of stagingEntries) {
|
|
284
|
+
if (preExistingRoots.has(entry.name)) continue;
|
|
285
|
+
try {
|
|
286
|
+
await rm(join(projectDir, entry.name), { recursive: true, force: true });
|
|
287
|
+
} catch (err) {
|
|
288
|
+
failures.push(
|
|
289
|
+
`remove ${entry.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const [relPath, content] of backups) {
|
|
294
|
+
try {
|
|
295
|
+
const destPath = join(projectDir, relPath);
|
|
296
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
297
|
+
await writeFile(destPath, content);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
failures.push(`restore ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (failures.length > 0) {
|
|
303
|
+
throw new CliError(`Init rollback failed: ${failures.join("; ")}`, {
|
|
304
|
+
code: "RUNTIME",
|
|
305
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
var PROMOTE_REPLACE_ENTRIES = /* @__PURE__ */ new Set(["node_modules", "package-lock.json"]);
|
|
191
310
|
async function promoteStagingToProjectDir(stagingDir, projectDir) {
|
|
192
311
|
await mkdir(projectDir, { recursive: true });
|
|
193
312
|
const entries = await readdir(stagingDir, { withFileTypes: true });
|
|
194
313
|
for (const entry of entries) {
|
|
195
314
|
const srcPath = join(stagingDir, entry.name);
|
|
196
315
|
const destPath = join(projectDir, entry.name);
|
|
316
|
+
if (PROMOTE_REPLACE_ENTRIES.has(entry.name) && existsSync(destPath)) {
|
|
317
|
+
await rm(destPath, { recursive: true, force: true });
|
|
318
|
+
}
|
|
197
319
|
if (entry.isDirectory()) {
|
|
198
320
|
await cp(srcPath, destPath, { recursive: true });
|
|
199
321
|
} else if (entry.isFile()) {
|
|
@@ -202,6 +324,20 @@ async function promoteStagingToProjectDir(stagingDir, projectDir) {
|
|
|
202
324
|
}
|
|
203
325
|
}
|
|
204
326
|
}
|
|
327
|
+
var __testInitHelpers = {
|
|
328
|
+
getTemplateDir,
|
|
329
|
+
isDirEmpty,
|
|
330
|
+
isDirEmptyOrDotfilesOnly,
|
|
331
|
+
escapeJsxString,
|
|
332
|
+
copyTemplate,
|
|
333
|
+
promoteStagingToProjectDir,
|
|
334
|
+
rollbackPromotedFiles,
|
|
335
|
+
backupConflictingFiles,
|
|
336
|
+
writeInitBackupDir,
|
|
337
|
+
listStagingFiles,
|
|
338
|
+
listProjectFiles,
|
|
339
|
+
INIT_BACKUP_DIR
|
|
340
|
+
};
|
|
205
341
|
async function runInit(opts, logger) {
|
|
206
342
|
const cwd = process.cwd();
|
|
207
343
|
const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
|
|
@@ -230,14 +366,8 @@ async function runInit(opts, logger) {
|
|
|
230
366
|
);
|
|
231
367
|
}
|
|
232
368
|
if (opts.here && !await isDirEmptyOrDotfilesOnly(projectDir) && !opts.force) {
|
|
233
|
-
throw new CliError(`Directory is not empty: ${projectDir}. Use --force to initialize anyway.`, {
|
|
234
|
-
code: "INVALID_PROJECT",
|
|
235
|
-
exitCode: EXIT_INVALID_PROJECT
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
if (opts.here && opts.force && !await isDirEmptyOrDotfilesOnly(projectDir)) {
|
|
239
369
|
throw new CliError(
|
|
240
|
-
`Directory is not empty: ${projectDir}. --force
|
|
370
|
+
`Directory is not empty: ${projectDir}. Use --here --force to scaffold anyway (conflicting files are backed up under ${INIT_BACKUP_DIR}/).`,
|
|
241
371
|
{
|
|
242
372
|
code: "INVALID_PROJECT",
|
|
243
373
|
exitCode: EXIT_INVALID_PROJECT
|
|
@@ -260,7 +390,48 @@ async function runInit(opts, logger) {
|
|
|
260
390
|
await runNpmInstall(stagingDir);
|
|
261
391
|
}
|
|
262
392
|
if (opts.here) {
|
|
263
|
-
await
|
|
393
|
+
const preExistingRoots = new Set(await readdir(projectDir));
|
|
394
|
+
const preExistingFiles = await listProjectFiles(projectDir);
|
|
395
|
+
const backups = await backupConflictingFiles(stagingDir, projectDir);
|
|
396
|
+
const conflicts = [...backups.keys()].sort();
|
|
397
|
+
if (conflicts.length > 0 && !opts.force) {
|
|
398
|
+
throw new CliError(
|
|
399
|
+
`Would overwrite existing file(s): ${conflicts.join(", ")}. Re-run with --force to back them up under ${INIT_BACKUP_DIR}/ and continue.`,
|
|
400
|
+
{
|
|
401
|
+
code: "INVALID_PROJECT",
|
|
402
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (conflicts.length > 0 && opts.force && !opts.json) {
|
|
407
|
+
const backupDir = await writeInitBackupDir(projectDir, backups);
|
|
408
|
+
logger.log(
|
|
409
|
+
`Backed up ${conflicts.length} conflicting file(s) to ${backupDir}: ${conflicts.join(", ")}`
|
|
410
|
+
);
|
|
411
|
+
} else if (conflicts.length > 0 && opts.force) {
|
|
412
|
+
await writeInitBackupDir(projectDir, backups);
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
await __testInitHelpers.promoteStagingToProjectDir(stagingDir, projectDir);
|
|
416
|
+
} catch (promoteErr) {
|
|
417
|
+
try {
|
|
418
|
+
await rollbackPromotedFiles(
|
|
419
|
+
projectDir,
|
|
420
|
+
stagingDir,
|
|
421
|
+
preExistingRoots,
|
|
422
|
+
preExistingFiles,
|
|
423
|
+
backups
|
|
424
|
+
);
|
|
425
|
+
} catch (rollbackErr) {
|
|
426
|
+
const promoteMessage = promoteErr instanceof Error ? promoteErr.message : String(promoteErr);
|
|
427
|
+
const rollbackMessage = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
428
|
+
throw new CliError(`${promoteMessage}; ${rollbackMessage}`, {
|
|
429
|
+
code: "RUNTIME",
|
|
430
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
throw promoteErr;
|
|
434
|
+
}
|
|
264
435
|
await rm(stagingDir, { recursive: true, force: true });
|
|
265
436
|
} else {
|
|
266
437
|
await rename(stagingDir, projectDir);
|
|
@@ -281,7 +452,9 @@ async function runInit(opts, logger) {
|
|
|
281
452
|
|
|
282
453
|
// src/commands/dev.ts
|
|
283
454
|
import { existsSync as existsSync3 } from "fs";
|
|
284
|
-
import {
|
|
455
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
456
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
457
|
+
import { assertSpaDistContentsSafe } from "@lessonkit/lxpack";
|
|
285
458
|
|
|
286
459
|
// src/lib/project.ts
|
|
287
460
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
@@ -291,10 +464,13 @@ import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "
|
|
|
291
464
|
import { parseLessonkitManifest } from "@lessonkit/lxpack";
|
|
292
465
|
var LESSONKIT_JSON = "lessonkit.json";
|
|
293
466
|
var PACKAGE_JSON = "package.json";
|
|
467
|
+
function isSchemaVersionOne(value) {
|
|
468
|
+
return value === 1 || value === "1";
|
|
469
|
+
}
|
|
294
470
|
function isProjectManifest(configPath) {
|
|
295
471
|
try {
|
|
296
472
|
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
297
|
-
return raw.schemaVersion
|
|
473
|
+
return isSchemaVersionOne(raw.schemaVersion) && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
|
|
298
474
|
} catch {
|
|
299
475
|
return false;
|
|
300
476
|
}
|
|
@@ -467,7 +643,12 @@ function resolvePackageOutput(project, target, override) {
|
|
|
467
643
|
if (override) {
|
|
468
644
|
try {
|
|
469
645
|
const resolved = resolveSafePackageOutputOverride(project.root, override);
|
|
470
|
-
|
|
646
|
+
const isZipOutput = override.trim().toLowerCase().endsWith(".zip");
|
|
647
|
+
return {
|
|
648
|
+
output: resolved,
|
|
649
|
+
dir: target === "standalone" && !isZipOutput,
|
|
650
|
+
outputBaseDir
|
|
651
|
+
};
|
|
471
652
|
} catch (err) {
|
|
472
653
|
const message = err instanceof Error ? err.message : String(err);
|
|
473
654
|
throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
|
|
@@ -513,7 +694,8 @@ async function runDev(opts) {
|
|
|
513
694
|
const pkg = await readPackageJson(project.root);
|
|
514
695
|
assertViteProject(pkg, project.root);
|
|
515
696
|
const viteJs = resolveViteJs(project.root);
|
|
516
|
-
|
|
697
|
+
const devArgs = stripOutDirFromViteArgs(opts.viteArgs ?? []);
|
|
698
|
+
await runCommand(process.execPath, [viteJs, ...devArgs], {
|
|
517
699
|
cwd: project.root,
|
|
518
700
|
timeoutMs: 0
|
|
519
701
|
});
|
|
@@ -524,11 +706,15 @@ async function runBuild(opts) {
|
|
|
524
706
|
const pkg = await readPackageJson(project.root);
|
|
525
707
|
assertViteProject(pkg, project.root);
|
|
526
708
|
const viteJs = resolveViteJs(project.root);
|
|
709
|
+
const distDir = resolveDistDir(project);
|
|
710
|
+
await mkdir2(dirname3(distDir), { recursive: true });
|
|
711
|
+
if (existsSync3(distDir)) {
|
|
712
|
+
await assertSpaDistContentsSafe({ main: distDir }, project.root);
|
|
713
|
+
}
|
|
527
714
|
const buildArgs = resolveViteBuildArgv(project, opts.viteArgs);
|
|
528
715
|
await runCommand(process.execPath, [viteJs, ...buildArgs], {
|
|
529
716
|
cwd: project.root
|
|
530
717
|
});
|
|
531
|
-
const distDir = resolveDistDir(project);
|
|
532
718
|
const indexHtml = join3(distDir, "index.html");
|
|
533
719
|
if (!existsSync3(indexHtml)) {
|
|
534
720
|
throw new CliError(
|
|
@@ -536,13 +722,13 @@ async function runBuild(opts) {
|
|
|
536
722
|
{ code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
|
|
537
723
|
);
|
|
538
724
|
}
|
|
725
|
+
await assertSpaDistContentsSafe({ main: distDir }, project.root);
|
|
539
726
|
return { ok: true, command: "build", projectRoot: project.root };
|
|
540
727
|
}
|
|
541
728
|
|
|
542
729
|
// src/commands/package.ts
|
|
543
730
|
import { existsSync as existsSync4 } from "fs";
|
|
544
|
-
import {
|
|
545
|
-
import { packageLessonkitCourse } from "@lessonkit/lxpack";
|
|
731
|
+
import { assertSpaDistContentsSafe as assertSpaDistContentsSafe2, packageLessonkitCourse } from "@lessonkit/lxpack";
|
|
546
732
|
async function runPackage(opts) {
|
|
547
733
|
let target;
|
|
548
734
|
try {
|
|
@@ -578,6 +764,7 @@ async function runPackage(opts) {
|
|
|
578
764
|
exitCode: EXIT_INVALID_PROJECT
|
|
579
765
|
});
|
|
580
766
|
}
|
|
767
|
+
await assertSpaDistContentsSafe2({ main: distDir }, project.root);
|
|
581
768
|
return { ok: true, command: "package", target, projectRoot: project.root, distDir };
|
|
582
769
|
}
|
|
583
770
|
assertNode18ForLxpack();
|
|
@@ -591,23 +778,23 @@ async function runPackage(opts) {
|
|
|
591
778
|
});
|
|
592
779
|
}
|
|
593
780
|
const outDir = resolveLxpackOutDir(project);
|
|
781
|
+
const trimmedOut = opts.out?.trim();
|
|
594
782
|
const { output: resolvedOutput, dir, outputBaseDir } = resolvePackageOutput(
|
|
595
783
|
project,
|
|
596
784
|
target,
|
|
597
|
-
|
|
785
|
+
trimmedOut
|
|
598
786
|
);
|
|
599
|
-
const trimmedOut = opts.out?.trim();
|
|
600
|
-
const output = trimmedOut && !isAbsolute(trimmedOut) ? trimmedOut : resolvedOutput;
|
|
601
787
|
const result = await packageLessonkitCourse({
|
|
602
788
|
descriptor: project.course,
|
|
603
789
|
outDir,
|
|
604
790
|
spaDistDir: distDir,
|
|
605
791
|
projectRoot: project.root,
|
|
606
792
|
target,
|
|
607
|
-
output,
|
|
793
|
+
output: trimmedOut ? resolvedOutput : void 0,
|
|
608
794
|
dir,
|
|
609
795
|
outputBaseDir,
|
|
610
|
-
strictParity: opts.strictParity
|
|
796
|
+
strictParity: opts.strictParity,
|
|
797
|
+
strictBuild: opts.strict
|
|
611
798
|
});
|
|
612
799
|
if (!result.ok) {
|
|
613
800
|
throw new CliError("Packaging failed.", {
|
|
@@ -639,6 +826,112 @@ async function runPackage(opts) {
|
|
|
639
826
|
};
|
|
640
827
|
}
|
|
641
828
|
|
|
829
|
+
// src/commands/export.ts
|
|
830
|
+
import { existsSync as existsSync5 } from "fs";
|
|
831
|
+
import { relative, resolve as resolve4 } from "path";
|
|
832
|
+
import { exportLkcourse, resolveSafePackageOutputOverride as resolveSafePackageOutputOverride2 } from "@lessonkit/lxpack";
|
|
833
|
+
function resolveExportOutput(projectRoot, override, defaultName) {
|
|
834
|
+
if (override) {
|
|
835
|
+
try {
|
|
836
|
+
return resolveSafePackageOutputOverride2(projectRoot, override);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
839
|
+
throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return resolve4(projectRoot, `${defaultName ?? "course"}.lkcourse`);
|
|
843
|
+
}
|
|
844
|
+
async function runExport(opts) {
|
|
845
|
+
const project = await loadProject(opts.cwd ?? process.cwd());
|
|
846
|
+
const distDir = resolve4(project.root, project.paths.spaDistDir);
|
|
847
|
+
if (opts.noBuild && !existsSync5(distDir)) {
|
|
848
|
+
throw new CliError(
|
|
849
|
+
`dist directory not found at ${distDir}. Run lessonkit build before export with --no-build.`,
|
|
850
|
+
{
|
|
851
|
+
code: "INVALID_PROJECT",
|
|
852
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
if (!opts.noBuild) {
|
|
857
|
+
await runBuild({ cwd: project.root, json: opts.json });
|
|
858
|
+
}
|
|
859
|
+
if (!existsSync5(distDir)) {
|
|
860
|
+
throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
|
|
861
|
+
code: "INVALID_PROJECT",
|
|
862
|
+
exitCode: EXIT_INVALID_PROJECT
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const resolvedOut = resolveExportOutput(project.root, opts.out, project.name);
|
|
866
|
+
const outRelative = relative(project.root, resolvedOut).replace(/\\/g, "/");
|
|
867
|
+
const result = await exportLkcourse({
|
|
868
|
+
projectRoot: project.root,
|
|
869
|
+
manifest: project,
|
|
870
|
+
outPath: outRelative,
|
|
871
|
+
includeBlockTree: Boolean(opts.withBlockTree)
|
|
872
|
+
});
|
|
873
|
+
if (!result.ok) {
|
|
874
|
+
throw new CliError(
|
|
875
|
+
result.issues.map((i) => `${i.path}: ${i.message}`).join("; "),
|
|
876
|
+
{ code: "EXPORT_FAILED", exitCode: EXIT_PACKAGING }
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
ok: true,
|
|
881
|
+
command: "export",
|
|
882
|
+
projectRoot: project.root,
|
|
883
|
+
archivePath: result.archivePath,
|
|
884
|
+
fileCount: result.fileCount,
|
|
885
|
+
includeBlockTree: result.includeBlockTree
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/commands/blocks.ts
|
|
890
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
891
|
+
import { createRequire as createRequire2 } from "module";
|
|
892
|
+
function loadBlockCatalog() {
|
|
893
|
+
const require3 = createRequire2(import.meta.url);
|
|
894
|
+
const catalogPath = require3.resolve("@lessonkit/react/block-catalog.v3.json");
|
|
895
|
+
return JSON.parse(readFileSync2(catalogPath, "utf8"));
|
|
896
|
+
}
|
|
897
|
+
function filterEntries(entries, opts) {
|
|
898
|
+
return entries.filter((entry) => {
|
|
899
|
+
if (opts.category && entry.category !== opts.category) return false;
|
|
900
|
+
if (opts.tier && entry.tier !== opts.tier) return false;
|
|
901
|
+
return true;
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
async function runBlocksList(opts) {
|
|
905
|
+
const catalog = loadBlockCatalog();
|
|
906
|
+
const entries = filterEntries(catalog.entries, opts);
|
|
907
|
+
if (!opts.json) {
|
|
908
|
+
const lines = [
|
|
909
|
+
"type category h5pMachineName",
|
|
910
|
+
...entries.map(
|
|
911
|
+
(entry) => [
|
|
912
|
+
entry.type,
|
|
913
|
+
entry.category ?? "\u2014",
|
|
914
|
+
entry.h5pMachineName ?? "\u2014"
|
|
915
|
+
].join(" ")
|
|
916
|
+
)
|
|
917
|
+
];
|
|
918
|
+
return {
|
|
919
|
+
ok: true,
|
|
920
|
+
command: "blocks list",
|
|
921
|
+
schemaVersion: catalog.schemaVersion,
|
|
922
|
+
count: entries.length,
|
|
923
|
+
text: lines.join("\n")
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
ok: true,
|
|
928
|
+
command: "blocks list",
|
|
929
|
+
schemaVersion: catalog.schemaVersion,
|
|
930
|
+
count: entries.length,
|
|
931
|
+
entries
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
642
935
|
// src/lib/logger.ts
|
|
643
936
|
function createLogger(opts) {
|
|
644
937
|
if (opts?.json) {
|
|
@@ -653,7 +946,7 @@ function createLogger(opts) {
|
|
|
653
946
|
}
|
|
654
947
|
|
|
655
948
|
// src/index.ts
|
|
656
|
-
var require2 =
|
|
949
|
+
var require2 = createRequire3(import.meta.url);
|
|
657
950
|
var { version } = require2("../package.json");
|
|
658
951
|
async function handleCommand(fn, logger, json) {
|
|
659
952
|
try {
|
|
@@ -676,7 +969,7 @@ function createProgram(baseLogger = console) {
|
|
|
676
969
|
program.name("lessonkit").description("LessonKit CLI").version(version);
|
|
677
970
|
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(
|
|
678
971
|
"--force",
|
|
679
|
-
"
|
|
972
|
+
"With --here: scaffold in a non-empty directory; back up conflicting template paths under .lessonkit-init-backup/ before overwrite"
|
|
680
973
|
).option("--json", "Emit structured JSON result").action(async (name, opts) => {
|
|
681
974
|
const logger = createLogger({ json: opts.json });
|
|
682
975
|
await handleCommand(
|
|
@@ -710,7 +1003,7 @@ function createProgram(baseLogger = console) {
|
|
|
710
1003
|
);
|
|
711
1004
|
}
|
|
712
1005
|
);
|
|
713
|
-
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("--json", "Emit structured JSON result").action(async (opts) => {
|
|
1006
|
+
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) => {
|
|
714
1007
|
const logger = createLogger({ json: opts.json });
|
|
715
1008
|
await handleCommand(
|
|
716
1009
|
async () => {
|
|
@@ -720,7 +1013,8 @@ function createProgram(baseLogger = console) {
|
|
|
720
1013
|
noBuild: opts.build === false,
|
|
721
1014
|
out: opts.out,
|
|
722
1015
|
json: opts.json,
|
|
723
|
-
strictParity: opts.strictParity
|
|
1016
|
+
strictParity: opts.strictParity,
|
|
1017
|
+
strict: opts.strict
|
|
724
1018
|
});
|
|
725
1019
|
if (!opts.json && result.ok && result.command === "package") {
|
|
726
1020
|
if (result.target === "react-vite") {
|
|
@@ -738,6 +1032,46 @@ function createProgram(baseLogger = console) {
|
|
|
738
1032
|
Boolean(opts.json)
|
|
739
1033
|
);
|
|
740
1034
|
});
|
|
1035
|
+
addCwdAndJson(
|
|
1036
|
+
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")
|
|
1037
|
+
).action(async (opts) => {
|
|
1038
|
+
const logger = createLogger({ json: opts.json });
|
|
1039
|
+
await handleCommand(
|
|
1040
|
+
async () => {
|
|
1041
|
+
const result = await runExport({
|
|
1042
|
+
cwd: opts.cwd,
|
|
1043
|
+
out: opts.out,
|
|
1044
|
+
noBuild: opts.build === false,
|
|
1045
|
+
withBlockTree: opts.withBlockTree,
|
|
1046
|
+
json: opts.json
|
|
1047
|
+
});
|
|
1048
|
+
if (!opts.json && result.ok && result.command === "export") {
|
|
1049
|
+
logger.log(`Exported .lkcourse \u2192 ${result.archivePath} (${result.fileCount} files)`);
|
|
1050
|
+
}
|
|
1051
|
+
return result;
|
|
1052
|
+
},
|
|
1053
|
+
logger,
|
|
1054
|
+
Boolean(opts.json)
|
|
1055
|
+
);
|
|
1056
|
+
});
|
|
1057
|
+
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) => {
|
|
1058
|
+
const logger = createLogger({ json: opts.json });
|
|
1059
|
+
await handleCommand(
|
|
1060
|
+
async () => {
|
|
1061
|
+
const result = await runBlocksList({
|
|
1062
|
+
json: opts.json,
|
|
1063
|
+
category: opts.category,
|
|
1064
|
+
tier: opts.tier
|
|
1065
|
+
});
|
|
1066
|
+
if (!opts.json && result.ok && "text" in result && typeof result.text === "string") {
|
|
1067
|
+
logger.log(result.text);
|
|
1068
|
+
}
|
|
1069
|
+
return result;
|
|
1070
|
+
},
|
|
1071
|
+
logger,
|
|
1072
|
+
Boolean(opts.json)
|
|
1073
|
+
);
|
|
1074
|
+
});
|
|
741
1075
|
program.command("publish").description("[maintainers] Not implemented \u2014 use Changesets (see RELEASING.md)").action(() => {
|
|
742
1076
|
baseLogger.log(
|
|
743
1077
|
"lessonkit publish is not implemented. Monorepo releases use Changesets: npm run changeset && npm run version-packages && npm run release. See RELEASING.md."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "LessonKit CLI — init, dev, build, and package learning experiences.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
],
|
|
37
37
|
"scripts": {
|
|
38
38
|
"copy-template": "node scripts/copy-template.mjs",
|
|
39
|
-
"build": "npm run copy-template && tsup src/index.ts src/bin.ts --format esm --splitting=false --platform node --target node18 --dts",
|
|
40
|
-
"dev": "npm run copy-template && tsup src/index.ts src/bin.ts --format esm --watch --splitting=false --platform node --target node18 --dts",
|
|
39
|
+
"build": "npm run copy-template && tsup src/index.ts src/bin.ts --format esm --splitting=false --platform node --target node18 --dts --tsconfig tsconfig.build.json",
|
|
40
|
+
"dev": "npm run copy-template && tsup src/index.ts src/bin.ts --format esm --watch --splitting=false --platform node --target node18 --dts --tsconfig tsconfig.build.json",
|
|
41
41
|
"prepublishOnly": "npm run build",
|
|
42
42
|
"typecheck": "tsc -p tsconfig.json",
|
|
43
43
|
"test": "vitest run --passWithNoTests",
|
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
"lint": "eslint --max-warnings 0 \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\""
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@lessonkit/core": "1.
|
|
49
|
-
"@lessonkit/lxpack": "1.
|
|
48
|
+
"@lessonkit/core": "1.7.0",
|
|
49
|
+
"@lessonkit/lxpack": "1.7.0",
|
|
50
|
+
"@lessonkit/react": "1.7.0",
|
|
50
51
|
"commander": "^15.0.0"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
@@ -45,8 +45,8 @@ After `npm run package:scorm12`, the CLI prints the resolved ZIP path. Default:
|
|
|
45
45
|
|
|
46
46
|
## Production
|
|
47
47
|
|
|
48
|
-
Copy `.env.example` to `.env` and set your LRS/analytics proxy URLs before `npm run build`. See
|
|
48
|
+
Copy `.env.example` to `.env` and set your LRS/analytics proxy URLs before `npm run build`. See [LMS Go-Live](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/lms-go-live.html) and the [backend proxy cookbook](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/backend-proxy-cookbook.html).
|
|
49
49
|
|
|
50
50
|
## Docs
|
|
51
51
|
|
|
52
|
-
[5-minute guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/getting-started-in-5-minutes.html) · [
|
|
52
|
+
[5-minute guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/getting-started-in-5-minutes.html) · [LMS Go-Live](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/lms-go-live.html) · [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)
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
"test:coverage": "vitest run --coverage --passWithNoTests=false"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@lessonkit/core": "^1.
|
|
20
|
-
"@lessonkit/react": "^1.
|
|
21
|
-
"@lessonkit/themes": "^1.
|
|
22
|
-
"@lessonkit/xapi": "^1.
|
|
19
|
+
"@lessonkit/core": "^1.7.0",
|
|
20
|
+
"@lessonkit/react": "^1.7.0",
|
|
21
|
+
"@lessonkit/themes": "^1.7.0",
|
|
22
|
+
"@lessonkit/xapi": "^1.7.0",
|
|
23
23
|
"react": "^19.2.7",
|
|
24
24
|
"react-dom": "^19.2.7"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@lessonkit/cli": "^1.
|
|
28
|
-
"@lessonkit/lxpack": "^1.
|
|
27
|
+
"@lessonkit/cli": "^1.7.0",
|
|
28
|
+
"@lessonkit/lxpack": "^1.7.0",
|
|
29
29
|
"@testing-library/react": "^16.3.0",
|
|
30
30
|
"@testing-library/dom": "^10.4.1",
|
|
31
31
|
"@types/react": "^19.2.17",
|
|
@@ -46,6 +46,11 @@ describe("createCourseConfig", () => {
|
|
|
46
46
|
config.observability?.onXapiTransportError?.(new Error("transport"));
|
|
47
47
|
config.observability?.onXapiMappingError?.(new Error("mapping"));
|
|
48
48
|
config.observability?.onLxpackBridgeError?.(new Error("bridge"));
|
|
49
|
+
config.observability?.onInvalidSessionId?.({
|
|
50
|
+
invalidId: "bad:id",
|
|
51
|
+
fallbackId: "tab-1",
|
|
52
|
+
source: "provided",
|
|
53
|
+
});
|
|
49
54
|
|
|
50
55
|
expect(warn).toHaveBeenCalled();
|
|
51
56
|
warn.mockRestore();
|
|
@@ -34,6 +34,9 @@ function createObservability(): NonNullable<LessonkitConfig["observability"]> {
|
|
|
34
34
|
onLxpackBridgeError: (err) => report("lxpack-bridge-error", { err }),
|
|
35
35
|
onXapiTransportError: (err) => report("xapi-transport", { err }),
|
|
36
36
|
onXapiMappingError: (err) => report("xapi-mapping", { err }),
|
|
37
|
+
onXapiDeadLetterPersistError: (err, ctx) =>
|
|
38
|
+
report("xapi-dead-letter-persist", { err, statementId: ctx.statement.id }),
|
|
39
|
+
onInvalidSessionId: (ctx) => report("invalid-session-id", ctx),
|
|
37
40
|
};
|
|
38
41
|
}
|
|
39
42
|
|