@lernapps/tooling 0.1.2 → 0.2.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/dist/cli.mjs CHANGED
@@ -809,7 +809,7 @@ const SCREENSHOTS_OPTIONS = {
809
809
  },
810
810
  width: {
811
811
  type: "string",
812
- default: "390"
812
+ default: "360"
813
813
  },
814
814
  desktop: {
815
815
  type: "boolean",
@@ -846,8 +846,8 @@ which shows the figures with their page, in the folder --out. The folder is empt
846
846
 
847
847
  Options:
848
848
  --out <dir> the folder of the images (default screenshots)
849
- --width <px> the width of the images in pixels (default 390, a phone)
850
- --desktop also images 1280 px wide; every image is then named with its width (01-index-390.png,
849
+ --width <px> the width of the images in pixels (default 360, a phone)
850
+ --desktop also images 1280 px wide; every image is then named with its width (01-index-360.png,
851
851
  01-index-1280.png, 01-index-figure-1-1280.png)
852
852
  -h, --help print this help
853
853
  `;
@@ -1445,7 +1445,7 @@ Run it in the folder lernapps init prepared, from the tooling init installed:
1445
1445
  It needs git, and the npm registry for the npm install that follows. The app then depends on the version of
1446
1446
  @lernapps/tooling that created it, and its workflows use the actions of that version's tag.
1447
1447
 
1448
- It writes, besides the template: package.json named after the folder (its prepare script, lernapps hooks,
1448
+ It writes, besides the template: package.json named after the short name given to init (the folder's name without init) (its prepare script, lernapps hooks,
1449
1449
  installs the git hooks; for an explainer and an interactive app also vue-tsc and typescript, with which lernapps
1450
1450
  check type-checks its Vue components; for an interactive app also @doctc/arc42, which checks its architecture),
1451
1451
  for an interactive app its architecture docs/arc42/ (the generic part and the interactive extension of app-templates,
@@ -1527,7 +1527,7 @@ function contents(dir) {
1527
1527
  if (name === ".git" || name === "skills-lock.json" && skills.size > 0) return [];
1528
1528
  if (name === "node_modules" || name === "package-lock.json") return [];
1529
1529
  if (name === "package.json" && readJson(join(dir, name)).lernapps?.init === true) return [];
1530
- if (name === ".vibe") return within(name, (file) => file === "plan.md" || file === "plans").concat(existsSync(join(dir, ".vibe/plans")) ? within(PLANS, (file) => PLAN_NAME.test(file)) : []);
1530
+ if (name === ".vibe") return existsSync(join(dir, ".vibe/plans")) ? within(PLANS, (file) => PLAN_NAME.test(file)) : [];
1531
1531
  if ((name === ".agents" || name === ".claude") && skills.size > 0) return within(name, (folder) => folder === "skills").concat(existsSync(join(dir, name, "skills")) ? within(`${name}/skills`, (skill) => skills.has(skill)) : []);
1532
1532
  return [name];
1533
1533
  });
@@ -1691,7 +1691,7 @@ function create(args) {
1691
1691
  if (positionals.length > 1) throw new UsageError(`one <dir> at most, not ${positionals.join(" ")}`);
1692
1692
  const dir = resolve(positionals[0] ?? ".");
1693
1693
  const found = contents(dir);
1694
- if (found.length > 0) throw new CreateError(`${dir} is not empty (${found.slice(0, 5).join(", ")}${found.length > 5 ? ", ..." : ""}): create the app in an empty folder; only .git, the first version's plan .vibe/plans/01-<short name>.md and what lernapps init wrote (its skills, package.json, package-lock.json and node_modules) may be there`);
1694
+ if (found.length > 0) throw new CreateError(`${dir} is not empty (${found.slice(0, 5).join(", ")}${found.length > 5 ? ", ..." : ""}): create the app in an empty folder; only .git, .vibe/ (the first version's plan .vibe/plans/01-<short name>.md, and its sketches) and what lernapps init wrote (its skills, package.json, package-lock.json and node_modules) may be there`);
1695
1695
  const plan = confirmedPlan(dir, archetype, values.plan);
1696
1696
  const architecture = ARCHITECTURE_ARCHETYPES.includes(archetype);
1697
1697
  if (!existsSync(join(templates.root, archetype, "package.json"))) throw new CreateError(`${templates.root} has no template ${archetype}/ (version ${templates.version})`);
@@ -1711,7 +1711,7 @@ function create(args) {
1711
1711
  if (architecture) writeArchitecture(dir, templates.root, archetype, templates.version);
1712
1712
  else rmSync(join(dir, ".arc42ignore"), { force: true });
1713
1713
  const app = readJson(join(dir, "package.json"));
1714
- const name = packageName(dir);
1714
+ const name = initialised?.lernapps?.init === true && typeof initialised.name === "string" ? initialised.name : packageName(dir);
1715
1715
  app.name = name;
1716
1716
  app.scripts = {
1717
1717
  ...app.scripts,
@@ -1860,8 +1860,6 @@ function syncSkills(dir, exclude) {
1860
1860
  ...filters.length > 0 ? ["--exclude", ...filters] : []
1861
1861
  ], (message) => new CreateError(message), dir);
1862
1862
  }
1863
- /** A package name from the folder's name: lower case, letters, digits and hyphens. */
1864
- const slug = (dir) => basename(dir).toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
1865
1863
  function init(args) {
1866
1864
  const { values, positionals } = parseArgs({
1867
1865
  args,
@@ -1881,7 +1879,7 @@ function init(args) {
1881
1879
  const addon = values.addon;
1882
1880
  const repository = ensureRepository(dir);
1883
1881
  writeJson(join(dir, "package.json"), {
1884
- name: slug(dir),
1882
+ name,
1885
1883
  version: "0.0.0",
1886
1884
  private: true,
1887
1885
  lernapps: { init: true }
@@ -44,7 +44,7 @@ a submission they declare something about.
44
44
  (`…-figure-<n>.png`): a page image is scaled down, its details are not readable. `--desktop` adds 1280 px images.
45
45
  Options: `npx --no -- lernapps screenshots --help`. The folder ignores itself in git, so a later check never sees it.
46
46
  - Interactive app: `screenshots` does not support it. Open the built app with the Playwright the tooling installs
47
- (`import { chromium } from "playwright"`), at 390 px width, and take `page.screenshot({ fullPage: true })` of each
47
+ (`import { chromium } from "playwright"`), at 360 px width, and take `page.screenshot({ fullPage: true })` of each
48
48
  state the creator should see, from a short script outside the app.
49
49
 
50
50
  ## A check fails
@@ -7,7 +7,7 @@
7
7
  | `true-false` | `answer` (true or false) |
8
8
  | `number` | `answer`, `tolerance` (0 = exact), `unit` if any; the learner may type a comma or a point |
9
9
  | `ordering` | `items` in the right order; the page mixes them |
10
- | `matching` | `pairs` as they belong; the page mixes the right side. `distractors` (1 to 4): wrong right entries that match no left one. `pairsFrom: material` instead of `pairs` takes them from the `labelled-figure` in `material` (marker left, text right; the figure needs `legend: false`) |
10
+ | `matching` | `pairs` as they belong, each pair an object `{ left, right }`; the page mixes the right side. `distractors` (1 to 4): wrong right entries that match no left one. `pairsFrom: material` instead of `pairs` takes them from the `labelled-figure` in `material` (marker left, text right; the figure needs `legend: false`) |
11
11
 
12
12
  - Every question: `id`, `text`, `explanation`, at least one link in `links`. Optional quiz `sources`.
13
13
  - Types without options carry their background in `explanation`.
@@ -5,6 +5,7 @@
5
5
  - Bundled; nothing from other servers before a click (`no-request-before-click`); others' images with source and
6
6
  licence (`third-party-licence`).
7
7
  - Works at 360 px: trees top to bottom; too wide → table, or split by stage.
8
+ - Check each in its own image of `npx --no -- lernapps screenshots`, not in the page image.
8
9
  - Same conventions (colours, labels, units, orientation) across the app; write them into the guide, "Figures".
9
10
  - The same figures work in a quiz question (`material`, skill `lernapps-content-questions`): an image of `src/images/` with
10
11
  `alt`, `caption`, `source` and `licence`.
@@ -28,6 +28,8 @@ license: MIT
28
28
 
29
29
  - A build problem is one line `file:line: place (language): what to do`. Fix where it points.
30
30
  - Headings are `section` blocks, never Markdown headings.
31
+ - YAML: quote a text that has `: ` in it or looks like a number (`"8."`, `"01000001"`, `"0"`). Unquoted, YAML reads a map or a
32
+ number: the build stops, or the digits change. A table cell, a header cell too, is a text and never empty.
31
33
  - A new chapter: a folder with `chapter.yaml`, listed in `src/app.yaml`, each page listed in `chapter.yaml`.
32
34
  - `selfAssessment` in a `chapter.yaml`: written in Commit, skill
33
35
  `lernapps-explainer-self-assessment`. An exercise it names renamed or moved → change the reference too.
@@ -57,7 +57,7 @@ Run it in the folder lernapps init prepared, from the tooling init installed:
57
57
  It needs git, and the npm registry for the npm install that follows. The app then depends on the version of
58
58
  @lernapps/tooling that created it, and its workflows use the actions of that version's tag.
59
59
 
60
- It writes, besides the template: package.json named after the folder (its prepare script, lernapps hooks,
60
+ It writes, besides the template: package.json named after the short name given to init (the folder's name without init) (its prepare script, lernapps hooks,
61
61
  installs the git hooks; for an explainer and an interactive app also vue-tsc and typescript, with which lernapps
62
62
  check type-checks its Vue components; for an interactive app also @doctc/arc42, which checks its architecture),
63
63
  for an interactive app its architecture docs/arc42/ (the generic part and the interactive extension of app-templates,
@@ -174,9 +174,9 @@ function contents(dir: string): string[] {
174
174
  if (name === "node_modules" || name === "package-lock.json") return [];
175
175
  if (name === "package.json" && readJson(join(dir, name)).lernapps?.init === true) return [];
176
176
  if (name === ".vibe") {
177
- return within(name, (file) => file === "plan.md" || file === "plans").concat(
178
- existsSync(join(dir, PLANS)) ? within(PLANS, (file) => PLAN_NAME.test(file)) : [],
179
- );
177
+ // the planning folder: the plans are checked (a first version has one), what the planning leaves beside them stays
178
+ // (the sketches of the plan, .vibe/sketches/)
179
+ return existsSync(join(dir, PLANS)) ? within(PLANS, (file) => PLAN_NAME.test(file)) : [];
180
180
  }
181
181
  if ((name === ".agents" || name === ".claude") && skills.size > 0) {
182
182
  return within(name, (folder) => folder === "skills").concat(
@@ -384,7 +384,7 @@ export function create(args: string[]): number {
384
384
  const found = contents(dir);
385
385
  if (found.length > 0) {
386
386
  throw new CreateError(
387
- `${dir} is not empty (${found.slice(0, 5).join(", ")}${found.length > 5 ? ", ..." : ""}): create the app in an empty folder; only .git, the first version's plan .vibe/plans/01-<short name>.md and what lernapps init wrote (its skills, package.json, package-lock.json and node_modules) may be there`,
387
+ `${dir} is not empty (${found.slice(0, 5).join(", ")}${found.length > 5 ? ", ..." : ""}): create the app in an empty folder; only .git, .vibe/ (the first version's plan .vibe/plans/01-<short name>.md, and its sketches) and what lernapps init wrote (its skills, package.json, package-lock.json and node_modules) may be there`,
388
388
  );
389
389
  }
390
390
  const plan = confirmedPlan(dir, archetype, values.plan);
@@ -416,7 +416,9 @@ export function create(args: string[]): number {
416
416
  else rmSync(join(dir, ".arc42ignore"), { force: true });
417
417
 
418
418
  const app = readJson(join(dir, "package.json"));
419
- const name = packageName(dir);
419
+ // the short name the creator gave `lernapps init` stays the app's name; without init, the folder names it
420
+ const name =
421
+ initialised?.lernapps?.init === true && typeof initialised.name === "string" ? initialised.name : packageName(dir);
420
422
  app.name = name;
421
423
  // npm install installs the hooks of this package, in the app's own repository only (lernapps hooks); the
422
424
  // template needs no prepare script of its own, which would install them in app-templates
package/generator/init.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
10
10
  import { createRequire } from "node:module";
11
- import { basename, dirname, join, resolve } from "node:path";
11
+ import { dirname, join, resolve } from "node:path";
12
12
  import { parseArgs, type ParseArgsOptionsConfig } from "node:util";
13
13
  import { UsageError } from "../src/usage.ts";
14
14
  import { PLANS } from "../src/plan.ts";
@@ -121,15 +121,6 @@ function syncSkills(dir: string, exclude: string[]): void {
121
121
  );
122
122
  }
123
123
 
124
- /** A package name from the folder's name: lower case, letters, digits and hyphens. */
125
- const slug = (dir: string) =>
126
- basename(dir)
127
- .toLowerCase()
128
- .normalize("NFKD")
129
- .replace(/[̀-ͯ]/g, "")
130
- .replace(/[^a-z0-9]+/g, "-")
131
- .replace(/^-+|-+$/g, "") || "app";
132
-
133
124
  export function init(args: string[]): number {
134
125
  const { values, positionals } = parseArgs({ args, options: INIT_OPTIONS, strict: true, allowPositionals: true });
135
126
  if (values.help) {
@@ -150,7 +141,7 @@ export function init(args: string[]): number {
150
141
  const addon = values.addon;
151
142
  const repository = ensureRepository(dir);
152
143
  writeJson(join(dir, "package.json"), {
153
- name: slug(dir),
144
+ name,
154
145
  version: "0.0.0",
155
146
  private: true,
156
147
  lernapps: { init: true },
@@ -15,6 +15,8 @@ Learning app for lernapps.net: static, runs in the browser, collects nothing, ac
15
15
 
16
16
  - Read guidance and skills from the file, in full. Never use a fetch tool that summarises.
17
17
  - Push only the branch the current plan names. A push to `main` publishes.
18
+ - Tick a box only after you did what it says, and only as far as it says. A box you did in part stays open, with a line
19
+ saying what is missing.
18
20
  - Never `--no-verify`. Never edit `prePushFailures` or `failedChecks`.
19
21
  - Never edit a `lernapps-*` skill: `lernapps update` replaces them. The app's own skills (`app-content`, `app-*`)
20
22
  take precedence; app rules go there.
@@ -100,7 +100,7 @@ questions. -->
100
100
  `lernapps-<archetype>-design` says, choose its pattern with skill `lernapps-exercise-patterns` (both loaded
101
101
  in Explore), and say why the guide's patterns do not fit.
102
102
  - [ ] Each new component or interactive exercise: a sketch (static SVG or image, or the steps). Words alone are not
103
- enough.
103
+ enough. Keep sketch files in `.vibe/sketches/`.
104
104
  - [ ] Show the creator, short: goal, new competencies by chapter, slices; in plain words what learners see and do,
105
105
  what is new. At most 2 questions.
106
106
  - [ ] **Checkpoint:** confirm the plan and the content method. Stop and wait for an explicit yes. Changes → change,
@@ -138,6 +138,7 @@ cannot. Something new goes into the guide in the slice that adds it. -->
138
138
  - [ ] The content guide describes the app as it is now (`app-content-matches`). `grep -c "FILL IN"
139
139
  .agents/skills/app-content/SKILL.md` prints 0.
140
140
  - [ ] `npx --no -- lernapps check` passes.
141
+ - [ ] Look at `npx --no -- lernapps screenshots --desktop`, each figure in its own image.
141
142
  - [ ] Every box of Explore, Plan and Code ticked.
142
143
  - [ ] Set `phase: commit`: change the value line (`grep '^phase:'` prints `phase: commit`).
143
144
  No checkpoint here: set it before you ask or report anything, then start Commit in the same message.
@@ -133,7 +133,7 @@ questions. -->
133
133
  - [ ] Write the Code tasks under "Code": pages or screens, their content, what each shows. Each ends in a commit.
134
134
  After the first task that shows real content: the task "Show the creator a first look (`early-preview`)".
135
135
  - [ ] Each screen, component or interactive exercise: a sketch (static SVG or image, or the steps with what
136
- learners see after each). Words alone are not enough.
136
+ learners see after each). Words alone are not enough. Keep sketch files in `.vibe/sketches/`, which `create` leaves alone.
137
137
  - [ ] Show the creator, short: goal, competencies by chapter, entry draft, tasks, sketches; the content method in
138
138
  plain words (what learners see and do, what it cannot do). At most 2 questions.
139
139
  - [ ] **Checkpoint:** confirm the plan and the content method, with the sketches shown. Stop and wait for an
@@ -175,7 +175,7 @@ checks and cannot; how a later agent adds a chapter. Becomes the content guide i
175
175
  - [ ] The content guide matches what was built (`app-content-matches`).
176
176
  - [ ] `npx --no -- lernapps check` passes.
177
177
  - [ ] Look at a screenshot of every screen and state (start, each mode, pause, end), desktop and 360 px. Compare
178
- each with the confirmed sketches.
178
+ each with the confirmed sketches; each figure in its own image (`npx --no -- lernapps screenshots --desktop`).
179
179
  - [ ] Every box of Explore, Plan and Code ticked.
180
180
  - [ ] Set `phase: commit`: change the value line (`grep '^phase:'` prints `phase: commit`).
181
181
  No checkpoint here: set it before you ask or report anything, then start Commit in the same message.
@@ -60,8 +60,8 @@ severity: hint
60
60
  ```
61
61
 
62
62
  - In Code, as soon as the first screen or page shows real content: send the creator a short "Zwischenstand".
63
- - Show screenshots, desktop and 360 px. On the creator's machine also the address of `npm run dev`, started in the
64
- background.
63
+ - Show screenshots, desktop and 360 px (`npx --no -- lernapps screenshots --desktop`). On the creator's machine also the
64
+ address of `npm run dev`, started in the background.
65
65
  - No checkpoint, no question: go on in the same message. The creator may stop you or ask for a change.
66
66
  - Example: "Zwischenstand: So sieht die erste Seite aus. Ich baue weiter; sag Bescheid, wenn etwas nicht passt."
67
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lernapps/tooling",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Shared tooling of the lernapps.net apps and sites: the CLI lernapps, site actions, Renovate preset, guidance for building apps; documentation at https://lernapps.net/tooling/",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -115,7 +115,7 @@
115
115
  },
116
116
  "devDependencies": {
117
117
  "@doctc/arc42": "0.38.0",
118
- "@lernapps/site": "github:lernapps/lernapps.github.io#96912a0b6691d90a538ac8336ac808152a7a2227",
118
+ "@lernapps/site": "github:lernapps/lernapps.github.io#15473cb8e782c69b2a38d45208bbe7eaf8a6c698",
119
119
  "@types/node": "26.6.5",
120
120
  "ajv": "8.20.0",
121
121
  "vite-plus": "1.4.0"
@@ -28,6 +28,8 @@ license: MIT
28
28
 
29
29
  - A build problem is one line `file:line: place (language): what to do`. Fix where it points.
30
30
  - Headings are `section` blocks, never Markdown headings.
31
+ - YAML: quote a text that has `: ` in it or looks like a number (`"8."`, `"01000001"`, `"0"`). Unquoted, YAML reads a map or a
32
+ number: the build stops, or the digits change. A table cell, a header cell too, is a text and never empty.
31
33
  - A new chapter: a folder with `chapter.yaml`, listed in `src/app.yaml`, each page listed in `chapter.yaml`.
32
34
  - `selfAssessment` in a `chapter.yaml`: written in Commit, skill
33
35
  `lernapps-explainer-self-assessment`. An exercise it names renamed or moved → change the reference too.
@@ -7,7 +7,7 @@
7
7
  | `true-false` | `answer` (true or false) |
8
8
  | `number` | `answer`, `tolerance` (0 = exact), `unit` if any; the learner may type a comma or a point |
9
9
  | `ordering` | `items` in the right order; the page mixes them |
10
- | `matching` | `pairs` as they belong; the page mixes the right side. `distractors` (1 to 4): wrong right entries that match no left one. `pairsFrom: material` instead of `pairs` takes them from the `labelled-figure` in `material` (marker left, text right; the figure needs `legend: false`) |
10
+ | `matching` | `pairs` as they belong, each pair an object `{ left, right }`; the page mixes the right side. `distractors` (1 to 4): wrong right entries that match no left one. `pairsFrom: material` instead of `pairs` takes them from the `labelled-figure` in `material` (marker left, text right; the figure needs `legend: false`) |
11
11
 
12
12
  - Every question: `id`, `text`, `explanation`, at least one link in `links`. Optional quiz `sources`.
13
13
  - Types without options carry their background in `explanation`.
@@ -5,6 +5,7 @@
5
5
  - Bundled; nothing from other servers before a click (`no-request-before-click`); others' images with source and
6
6
  licence (`third-party-licence`).
7
7
  - Works at 360 px: trees top to bottom; too wide → table, or split by stage.
8
+ - Check each in its own image of `npx --no -- lernapps screenshots`, not in the page image.
8
9
  - Same conventions (colours, labels, units, orientation) across the app; write them into the guide, "Figures".
9
10
  - The same figures work in a quiz question (`material`, skill `lernapps-content-questions`): an image of `src/images/` with
10
11
  `alt`, `caption`, `source` and `licence`.
@@ -44,7 +44,7 @@ a submission they declare something about.
44
44
  (`…-figure-<n>.png`): a page image is scaled down, its details are not readable. `--desktop` adds 1280 px images.
45
45
  Options: `npx --no -- lernapps screenshots --help`. The folder ignores itself in git, so a later check never sees it.
46
46
  - Interactive app: `screenshots` does not support it. Open the built app with the Playwright the tooling installs
47
- (`import { chromium } from "playwright"`), at 390 px width, and take `page.screenshot({ fullPage: true })` of each
47
+ (`import { chromium } from "playwright"`), at 360 px width, and take `page.screenshot({ fullPage: true })` of each
48
48
  state the creator should see, from a short script outside the app.
49
49
 
50
50
  ## A check fails