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