@lessonkit/cli 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # `@lessonkit/cli`
2
2
 
3
3
  [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
4
+ [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/)
4
5
  [![npm](https://img.shields.io/npm/v/@lessonkit/cli.svg)](https://www.npmjs.com/package/@lessonkit/cli)
5
- [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](../../LICENSE)
6
+ [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
6
7
 
7
8
  LessonKit CLI — scaffold, dev, build, and package learning experiences.
8
9
 
10
+ **Docs:** [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [Packaging & CLI guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [Vibe coding: shipping to LMS](https://lessonkit.readthedocs.io/en/latest/guides/vibe-coding/shipping-to-lms.html)
11
+
9
12
  ## Install
10
13
 
11
14
  ```bash
@@ -34,7 +37,7 @@ lessonkit package --target scorm12
34
37
  | `lessonkit dev` | Run Vite dev server |
35
38
  | `lessonkit build` | Production Vite build |
36
39
  | `lessonkit package --target <target>` | Build or package for web / LMS |
37
- | `lessonkit publish` | Stub — see [`RELEASING.md`](../../RELEASING.md) |
40
+ | `lessonkit publish` | Stub — see [`RELEASING.md`](https://github.com/eddiethedean/lessonkit/blob/main/RELEASING.md) |
38
41
 
39
42
  ### Package targets
40
43
 
@@ -43,10 +46,10 @@ lessonkit package --target scorm12
43
46
 
44
47
  ## Project manifest
45
48
 
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.
49
+ Projects include a `lessonkit.json` at the root. See the [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) for the schema, flags, exit codes, and JSON output mode.
47
50
 
48
51
  ## Related
49
52
 
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
53
+ - [Packaging reference](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html) — LXPack output layout
54
+ - [React quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html)
55
+ - [`templates/vite-react`](https://github.com/eddiethedean/lessonkit/tree/main/templates/vite-react) — starter template
package/dist/bin.js CHANGED
@@ -102,6 +102,14 @@ async function isDirEmpty(dir) {
102
102
  const entries = await readdir(dir);
103
103
  return entries.length === 0;
104
104
  }
105
+ async function isDirEmptyOrDotfilesOnly(dir) {
106
+ if (!existsSync(dir)) return true;
107
+ const entries = await readdir(dir);
108
+ return entries.every((name) => name.startsWith("."));
109
+ }
110
+ function escapeJsxString(value) {
111
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
112
+ }
105
113
  async function copyTemplate(src, dest) {
106
114
  await mkdir(dest, { recursive: true });
107
115
  const entries = await readdir(src, { withFileTypes: true });
@@ -133,9 +141,7 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
133
141
  const appPath = join(projectDir, "src", "App.tsx");
134
142
  let appSource = await readFile(appPath, "utf8");
135
143
  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"');
144
+ appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
139
145
  await writeFile(appPath, appSource, "utf8");
140
146
  }
141
147
  async function runInit(opts, logger) {
@@ -151,10 +157,13 @@ async function runInit(opts, logger) {
151
157
  const projectName = rawName ?? slug;
152
158
  const projectDir = opts.here ? cwd : resolve(cwd, slug);
153
159
  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
- });
160
+ throw new CliError(
161
+ `Directory already exists: ${projectDir}. Choose a different name or remove the directory.`,
162
+ {
163
+ code: "INVALID_PROJECT",
164
+ exitCode: EXIT_INVALID_PROJECT
165
+ }
166
+ );
158
167
  }
159
168
  if (opts.here && !await isDirEmpty(projectDir) && !opts.force) {
160
169
  throw new CliError(`Directory is not empty: ${projectDir}. Use --force to initialize anyway.`, {
@@ -162,6 +171,15 @@ async function runInit(opts, logger) {
162
171
  exitCode: EXIT_INVALID_PROJECT
163
172
  });
164
173
  }
174
+ if (opts.here && opts.force && !await isDirEmptyOrDotfilesOnly(projectDir)) {
175
+ throw new CliError(
176
+ `Directory is not empty: ${projectDir}. --force only initializes when the directory is empty or contains dotfiles only (e.g. .git).`,
177
+ {
178
+ code: "INVALID_PROJECT",
179
+ exitCode: EXIT_INVALID_PROJECT
180
+ }
181
+ );
182
+ }
165
183
  const templateDir = getTemplateDir();
166
184
  if (!existsSync(templateDir)) {
167
185
  throw new CliError(`Bundled template not found at ${templateDir}. Reinstall @lessonkit/cli.`, {
@@ -186,7 +204,7 @@ async function runInit(opts, logger) {
186
204
  import { readFileSync, existsSync as existsSync2 } from "fs";
187
205
  import { readFile as readFile2 } from "fs/promises";
188
206
  import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
189
- import { validateDescriptor } from "@lessonkit/lxpack";
207
+ import { validateDescriptor, validateProjectPaths } from "@lessonkit/lxpack";
190
208
  var LESSONKIT_JSON = "lessonkit.json";
191
209
  var PACKAGE_JSON = "package.json";
192
210
  var DEFAULT_PATHS = {
@@ -283,6 +301,14 @@ async function loadLessonkitJson(projectRoot) {
283
301
  if (typeof p.lxpackOutDir === "string" && p.lxpackOutDir.trim()) paths.lxpackOutDir = p.lxpackOutDir;
284
302
  if (typeof p.outputBaseDir === "string" && p.outputBaseDir.trim()) paths.outputBaseDir = p.outputBaseDir;
285
303
  }
304
+ const pathIssues = validateProjectPaths(projectRoot, paths);
305
+ if (pathIssues.length) {
306
+ throw new CliError(`${configPath}: invalid paths.`, {
307
+ code: "INVALID_PROJECT",
308
+ exitCode: EXIT_INVALID_PROJECT,
309
+ issues: pathIssues
310
+ });
311
+ }
286
312
  return {
287
313
  root: projectRoot,
288
314
  schemaVersion: 1,
@@ -344,6 +370,7 @@ function assertNode20ForLxpack() {
344
370
 
345
371
  // src/lib/paths.ts
346
372
  import { resolve as resolve3 } from "path";
373
+ import { resolveSafePackageOutputOverride } from "@lessonkit/lxpack";
347
374
  function resolveDistDir(project) {
348
375
  return resolve3(project.root, project.paths.spaDistDir);
349
376
  }
@@ -353,7 +380,8 @@ function resolveLxpackOutDir(project) {
353
380
  function resolvePackageOutput(project, target, override) {
354
381
  const outputBaseDir = project.paths.outputBaseDir;
355
382
  if (override) {
356
- return { output: override, dir: target === "standalone", outputBaseDir };
383
+ const resolved = resolveSafePackageOutputOverride(project.root, override);
384
+ return { output: resolved, dir: target === "standalone", outputBaseDir };
357
385
  }
358
386
  if (target === "standalone") {
359
387
  return { output: `${outputBaseDir}/standalone`, dir: true, outputBaseDir };
@@ -425,7 +453,9 @@ async function runPackage(opts) {
425
453
  const project = await loadProject(opts.cwd ?? process.cwd());
426
454
  const distDir = resolveDistDir(project);
427
455
  if (target === "react-vite") {
428
- await runBuild({ cwd: project.root, json: opts.json });
456
+ if (!opts.noBuild || !existsSync3(distDir)) {
457
+ await runBuild({ cwd: project.root, json: opts.json });
458
+ }
429
459
  if (!existsSync3(distDir)) {
430
460
  throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
431
461
  code: "RUNTIME",
package/dist/index.js CHANGED
@@ -100,6 +100,14 @@ async function isDirEmpty(dir) {
100
100
  const entries = await readdir(dir);
101
101
  return entries.length === 0;
102
102
  }
103
+ async function isDirEmptyOrDotfilesOnly(dir) {
104
+ if (!existsSync(dir)) return true;
105
+ const entries = await readdir(dir);
106
+ return entries.every((name) => name.startsWith("."));
107
+ }
108
+ function escapeJsxString(value) {
109
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
110
+ }
103
111
  async function copyTemplate(src, dest) {
104
112
  await mkdir(dest, { recursive: true });
105
113
  const entries = await readdir(src, { withFileTypes: true });
@@ -131,9 +139,7 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
131
139
  const appPath = join(projectDir, "src", "App.tsx");
132
140
  let appSource = await readFile(appPath, "utf8");
133
141
  appSource = appSource.replace(/courseId="my-course"/g, `courseId="${slug}"`);
134
- appSource = appSource.replace(/title="My LessonKit Course"/g, `title="${projectName.replace(/"/g, '\\"')}"`);
135
- appSource = appSource.replace(/preset="dark"/g, 'preset="default"');
136
- appSource = appSource.replace(/mode="dark"/g, 'mode="light"');
142
+ appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
137
143
  await writeFile(appPath, appSource, "utf8");
138
144
  }
139
145
  async function runInit(opts, logger) {
@@ -149,10 +155,13 @@ async function runInit(opts, logger) {
149
155
  const projectName = rawName ?? slug;
150
156
  const projectDir = opts.here ? cwd : resolve(cwd, slug);
151
157
  if (!opts.here && existsSync(projectDir)) {
152
- throw new CliError(`Directory already exists: ${projectDir}. Use --force to initialize anyway.`, {
153
- code: "INVALID_PROJECT",
154
- exitCode: EXIT_INVALID_PROJECT
155
- });
158
+ throw new CliError(
159
+ `Directory already exists: ${projectDir}. Choose a different name or remove the directory.`,
160
+ {
161
+ code: "INVALID_PROJECT",
162
+ exitCode: EXIT_INVALID_PROJECT
163
+ }
164
+ );
156
165
  }
157
166
  if (opts.here && !await isDirEmpty(projectDir) && !opts.force) {
158
167
  throw new CliError(`Directory is not empty: ${projectDir}. Use --force to initialize anyway.`, {
@@ -160,6 +169,15 @@ async function runInit(opts, logger) {
160
169
  exitCode: EXIT_INVALID_PROJECT
161
170
  });
162
171
  }
172
+ if (opts.here && opts.force && !await isDirEmptyOrDotfilesOnly(projectDir)) {
173
+ throw new CliError(
174
+ `Directory is not empty: ${projectDir}. --force only initializes when the directory is empty or contains dotfiles only (e.g. .git).`,
175
+ {
176
+ code: "INVALID_PROJECT",
177
+ exitCode: EXIT_INVALID_PROJECT
178
+ }
179
+ );
180
+ }
163
181
  const templateDir = getTemplateDir();
164
182
  if (!existsSync(templateDir)) {
165
183
  throw new CliError(`Bundled template not found at ${templateDir}. Reinstall @lessonkit/cli.`, {
@@ -184,7 +202,7 @@ async function runInit(opts, logger) {
184
202
  import { readFileSync, existsSync as existsSync2 } from "fs";
185
203
  import { readFile as readFile2 } from "fs/promises";
186
204
  import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
187
- import { validateDescriptor } from "@lessonkit/lxpack";
205
+ import { validateDescriptor, validateProjectPaths } from "@lessonkit/lxpack";
188
206
  var LESSONKIT_JSON = "lessonkit.json";
189
207
  var PACKAGE_JSON = "package.json";
190
208
  var DEFAULT_PATHS = {
@@ -281,6 +299,14 @@ async function loadLessonkitJson(projectRoot) {
281
299
  if (typeof p.lxpackOutDir === "string" && p.lxpackOutDir.trim()) paths.lxpackOutDir = p.lxpackOutDir;
282
300
  if (typeof p.outputBaseDir === "string" && p.outputBaseDir.trim()) paths.outputBaseDir = p.outputBaseDir;
283
301
  }
302
+ const pathIssues = validateProjectPaths(projectRoot, paths);
303
+ if (pathIssues.length) {
304
+ throw new CliError(`${configPath}: invalid paths.`, {
305
+ code: "INVALID_PROJECT",
306
+ exitCode: EXIT_INVALID_PROJECT,
307
+ issues: pathIssues
308
+ });
309
+ }
284
310
  return {
285
311
  root: projectRoot,
286
312
  schemaVersion: 1,
@@ -342,6 +368,7 @@ function assertNode20ForLxpack() {
342
368
 
343
369
  // src/lib/paths.ts
344
370
  import { resolve as resolve3 } from "path";
371
+ import { resolveSafePackageOutputOverride } from "@lessonkit/lxpack";
345
372
  function resolveDistDir(project) {
346
373
  return resolve3(project.root, project.paths.spaDistDir);
347
374
  }
@@ -351,7 +378,8 @@ function resolveLxpackOutDir(project) {
351
378
  function resolvePackageOutput(project, target, override) {
352
379
  const outputBaseDir = project.paths.outputBaseDir;
353
380
  if (override) {
354
- return { output: override, dir: target === "standalone", outputBaseDir };
381
+ const resolved = resolveSafePackageOutputOverride(project.root, override);
382
+ return { output: resolved, dir: target === "standalone", outputBaseDir };
355
383
  }
356
384
  if (target === "standalone") {
357
385
  return { output: `${outputBaseDir}/standalone`, dir: true, outputBaseDir };
@@ -423,7 +451,9 @@ async function runPackage(opts) {
423
451
  const project = await loadProject(opts.cwd ?? process.cwd());
424
452
  const distDir = resolveDistDir(project);
425
453
  if (target === "react-vite") {
426
- await runBuild({ cwd: project.root, json: opts.json });
454
+ if (!opts.noBuild || !existsSync3(distDir)) {
455
+ await runBuild({ cwd: project.root, json: opts.json });
456
+ }
427
457
  if (!existsSync3(distDir)) {
428
458
  throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
429
459
  code: "RUNTIME",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "description": "LessonKit CLI — init, dev, build, and package learning experiences.",
6
6
  "license": "Apache-2.0",
@@ -42,7 +42,7 @@
42
42
  "lint": "echo \"(no lint configured yet)\""
43
43
  },
44
44
  "dependencies": {
45
- "@lessonkit/lxpack": "0.7.0",
45
+ "@lessonkit/lxpack": "0.8.1",
46
46
  "commander": "^14.0.1"
47
47
  },
48
48
  "engines": {
@@ -1,9 +1,9 @@
1
1
  # LessonKit Vite + React template
2
2
 
3
- [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
4
- [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](../../LICENSE)
3
+ [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/)
4
+ [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
5
5
 
6
- This is a starter template used by `lessonkit init`. See [`docs/CLI.md`](../../docs/CLI.md).
6
+ Starter template copied by `lessonkit init`. See the [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) and [React quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html).
7
7
 
8
8
  ## Run
9
9
 
@@ -14,6 +14,6 @@ npm run dev
14
14
 
15
15
  ## Notes
16
16
 
17
- - This template depends on `@lessonkit/react`.
18
- - It’s copied by `@lessonkit/cli` when you run `lessonkit init`.
19
-
17
+ - Depends on [`@lessonkit/react`](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/components-and-hooks.html).
18
+ - Copied by [`@lessonkit/cli`](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) when you run `lessonkit init`.
19
+ - Package for an LMS with the [packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html).