@lernapps/tooling 0.0.0-stage → 0.1.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/LICENSE +22 -0
- package/README.md +67 -2
- package/archetypes/shared/a11y.ts +38 -0
- package/archetypes/shared/hooks/pre-commit +22 -0
- package/archetypes/shared/hooks/pre-push +4 -0
- package/archetypes/shared/i18n.ts +25 -0
- package/archetypes/shared/install-hooks.ts +63 -0
- package/archetypes/shared/playwright.ts +54 -0
- package/archetypes/shared/preset.ts +81 -0
- package/archetypes/shared/storage.ts +167 -0
- package/archetypes/shared/tsconfig.json +27 -0
- package/check/validation-report.v1.schema.json +116 -0
- package/dist/a11y.mjs +30 -0
- package/dist/check/rules/accessible.mjs +18 -0
- package/dist/check/rules/app-content-guide.mjs +69 -0
- package/dist/check/rules/dependency-licence.mjs +82 -0
- package/dist/check/rules/entry-fitness-matches.mjs +22 -0
- package/dist/check/rules/entry-links-resolve.mjs +44 -0
- package/dist/check/rules/explainer-readable-without-js.mjs +66 -0
- package/dist/check/rules/links-resolve.mjs +10 -0
- package/dist/check/rules/lockfile-matches.mjs +57 -0
- package/dist/check/rules/no-request-before-click.mjs +17 -0
- package/dist/check/rules/no-tracking.mjs +17 -0
- package/dist/check/rules/own-data-only.mjs +66 -0
- package/dist/check/rules/pages-reachable.mjs +25 -0
- package/dist/check/rules/usable-at-360px.mjs +19 -0
- package/dist/check-cv4I6CCH.mjs +28 -0
- package/dist/cli.mjs +3118 -0
- package/dist/i18n.mjs +11 -0
- package/dist/lint.mjs +528 -0
- package/dist/plan-DWKayEnY.mjs +75 -0
- package/dist/playwright.mjs +47 -0
- package/dist/preset.mjs +60 -0
- package/dist/skills/lernapps-app/SKILL.md +49 -0
- package/dist/skills/lernapps-cli/SKILL.md +156 -0
- package/dist/skills/lernapps-cli/references/platform-rules.md +258 -0
- package/dist/skills/lernapps-commit/SKILL.md +39 -0
- package/dist/skills/lernapps-content-growth/SKILL.md +33 -0
- package/dist/skills/lernapps-content-questions/SKILL.md +133 -0
- package/dist/skills/lernapps-content-questions/references/question-types.md +69 -0
- package/dist/skills/lernapps-content-research/SKILL.md +50 -0
- package/dist/skills/lernapps-content-research/references/sources.md +16 -0
- package/dist/skills/lernapps-exercise-patterns/SKILL.md +74 -0
- package/dist/skills/lernapps-exercise-patterns/references/biology-geography.md +13 -0
- package/dist/skills/lernapps-exercise-patterns/references/chemistry.md +16 -0
- package/dist/skills/lernapps-exercise-patterns/references/computer-science.md +13 -0
- package/dist/skills/lernapps-exercise-patterns/references/figures.md +10 -0
- package/dist/skills/lernapps-exercise-patterns/references/history.md +13 -0
- package/dist/skills/lernapps-exercise-patterns/references/mathematics.md +19 -0
- package/dist/skills/lernapps-exercise-patterns/references/page-structures.md +8 -0
- package/dist/skills/lernapps-exercise-patterns/references/physics.md +18 -0
- package/dist/skills/lernapps-exercise-patterns/references/success.md +28 -0
- package/dist/skills/lernapps-explainer/SKILL.md +33 -0
- package/dist/skills/lernapps-explainer/references/blocks.md +83 -0
- package/dist/skills/lernapps-explainer/references/components.md +66 -0
- package/dist/skills/lernapps-explainer/references/exercises.md +115 -0
- package/dist/skills/lernapps-explainer/references/rules.md +285 -0
- package/dist/skills/lernapps-explainer/references/runtime.md +79 -0
- package/dist/skills/lernapps-explainer/references/structure.md +24 -0
- package/dist/skills/lernapps-explainer-design/SKILL.md +57 -0
- package/dist/skills/lernapps-explainer-design/references/rules.md +75 -0
- package/dist/skills/lernapps-explainer-self-assessment/SKILL.md +93 -0
- package/dist/skills/lernapps-github/SKILL.md +102 -0
- package/dist/skills/lernapps-interactive/SKILL.md +141 -0
- package/dist/skills/lernapps-interactive/references/frame.md +30 -0
- package/dist/skills/lernapps-interactive-design/SKILL.md +58 -0
- package/dist/skills/lernapps-listing/SKILL.md +44 -0
- package/dist/skills/lernapps-listing/references/keeping-updated.md +33 -0
- package/dist/skills/lernapps-quiz/SKILL.md +100 -0
- package/dist/skills/lernapps-quiz-design/SKILL.md +48 -0
- package/dist/status-CXoQBgfU.mjs +76 -0
- package/dist/storage.mjs +113 -0
- package/generator/app/.agents/skills/app-content/SKILL.md +63 -0
- package/generator/app/.github/ISSUE_TEMPLATE/inhaltsfehler.yml +25 -0
- package/generator/app/.github/workflows/pages.yml +61 -0
- package/generator/app/LICENSE +21 -0
- package/generator/app/renovate.json +4 -0
- package/generator/architecture.ts +212 -0
- package/generator/create.ts +462 -0
- package/generator/init.ts +186 -0
- package/generator/update.ts +429 -0
- package/guidance/AGENTS.md +26 -0
- package/guidance/increment-template.md +194 -0
- package/guidance/plan-front-matter.v1.schema.json +36 -0
- package/guidance/plan-template.md +233 -0
- package/guidance/subagent-brief.md +25 -0
- package/guidance/workflow-rules.md +109 -0
- package/package.json +135 -4
- package/review/verdict.v1.schema.json +134 -0
- package/skills/archetypes/lernapps-explainer/SKILL.md +33 -0
- package/skills/archetypes/lernapps-explainer/references/blocks.md +83 -0
- package/skills/archetypes/lernapps-explainer/references/components.md +66 -0
- package/skills/archetypes/lernapps-explainer/references/exercises.md +115 -0
- package/skills/archetypes/lernapps-explainer/references/rules.md +285 -0
- package/skills/archetypes/lernapps-explainer/references/runtime.md +79 -0
- package/skills/archetypes/lernapps-explainer/references/structure.md +24 -0
- package/skills/archetypes/lernapps-explainer-design/SKILL.md +57 -0
- package/skills/archetypes/lernapps-explainer-design/references/rules.md +75 -0
- package/skills/archetypes/lernapps-explainer-self-assessment/SKILL.md +93 -0
- package/skills/archetypes/lernapps-interactive/SKILL.md +141 -0
- package/skills/archetypes/lernapps-interactive/references/frame.md +30 -0
- package/skills/archetypes/lernapps-interactive-design/SKILL.md +58 -0
- package/skills/archetypes/lernapps-quiz/SKILL.md +100 -0
- package/skills/archetypes/lernapps-quiz-design/SKILL.md +48 -0
- package/skills/content/lernapps-content-questions/SKILL.md +133 -0
- package/skills/content/lernapps-content-questions/references/question-types.md +69 -0
- package/skills/content/lernapps-content-research/SKILL.md +50 -0
- package/skills/content/lernapps-content-research/references/sources.md +16 -0
- package/skills/content/lernapps-exercise-patterns/SKILL.md +74 -0
- package/skills/content/lernapps-exercise-patterns/references/biology-geography.md +13 -0
- package/skills/content/lernapps-exercise-patterns/references/chemistry.md +16 -0
- package/skills/content/lernapps-exercise-patterns/references/computer-science.md +13 -0
- package/skills/content/lernapps-exercise-patterns/references/figures.md +10 -0
- package/skills/content/lernapps-exercise-patterns/references/history.md +13 -0
- package/skills/content/lernapps-exercise-patterns/references/mathematics.md +19 -0
- package/skills/content/lernapps-exercise-patterns/references/page-structures.md +8 -0
- package/skills/content/lernapps-exercise-patterns/references/physics.md +18 -0
- package/skills/content/lernapps-exercise-patterns/references/success.md +28 -0
- package/skills/process/lernapps-app/SKILL.md +49 -0
- package/skills/process/lernapps-commit/SKILL.md +39 -0
- package/skills/process/lernapps-content-growth/SKILL.md +33 -0
- package/skills/process/lernapps-github/SKILL.md +102 -0
- package/skills/process/lernapps-listing/SKILL.md +44 -0
- package/skills/process/lernapps-listing/references/keeping-updated.md +33 -0
- package/skills/tooling/lernapps-cli/SKILL.md +156 -0
- package/skills/tooling/lernapps-cli/references/platform-rules.md +258 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// `lernapps init <short name>` and `lernapps skills --archetype <name>`: the start of an app, from the npm registry
|
|
2
|
+
// alone. `init` runs in an empty folder: it writes a package.json that depends on this tooling (and on an add-on such as
|
|
3
|
+
// @lernapps/practice), installs them, writes the plan template of the package, complete and unchanged, as
|
|
4
|
+
// .vibe/plans/01-<short name>.md, and syncs the skills of the installed packages into the agent directories with
|
|
5
|
+
// `skills experimental_sync` (the package `skills` supports many agents). The skills of the archetypes wait until the
|
|
6
|
+
// plan has chosen one: `skills --archetype <name>` then adds that archetype's. `lernapps create` later takes over the
|
|
7
|
+
// package.json (the marker lernapps.init) and the installed packages.
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
|
+
import { parseArgs, type ParseArgsOptionsConfig } from "node:util";
|
|
13
|
+
import { UsageError } from "../src/usage.ts";
|
|
14
|
+
import { PLANS } from "../src/plan.ts";
|
|
15
|
+
import { CreateError, fromPackage, ownVersion, readJson, TOOLING, writeJson } from "./create.ts";
|
|
16
|
+
|
|
17
|
+
/** The options of `lernapps init`, as node:util parseArgs takes them. */
|
|
18
|
+
export const INIT_OPTIONS = {
|
|
19
|
+
addon: { type: "string" },
|
|
20
|
+
help: { type: "boolean", short: "h", default: false },
|
|
21
|
+
} as const satisfies ParseArgsOptionsConfig;
|
|
22
|
+
|
|
23
|
+
export const INIT_USAGE = `Usage: lernapps init <short name> [--addon <package>]
|
|
24
|
+
|
|
25
|
+
Starts an app in the current folder, which must be empty except for .git. It needs git, Node and the npm registry; nothing
|
|
26
|
+
else. In order:
|
|
27
|
+
package.json names the folder, depends on @lernapps/tooling (this version) and the add-on
|
|
28
|
+
npm install installs them
|
|
29
|
+
.vibe/plans/01-<short name>.md
|
|
30
|
+
the plan template of the add-on, else of this tooling, copied complete and unchanged
|
|
31
|
+
.agents/skills/, ... the skills of the installed packages, for every agent \`skills\` finds, except those of
|
|
32
|
+
the archetypes: after the plan chose one, run \`lernapps skills --archetype <name>\`
|
|
33
|
+
|
|
34
|
+
Then follow the plan: it says what to do, and when to wait for the creator.
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--addon <package> a package that extends the tooling, e.g. @lernapps/practice: its plan template and its skills
|
|
38
|
+
-h, --help print this help
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
/** The options of `lernapps skills`. */
|
|
42
|
+
export const SKILLS_OPTIONS = {
|
|
43
|
+
archetype: { type: "string" },
|
|
44
|
+
help: { type: "boolean", short: "h", default: false },
|
|
45
|
+
} as const satisfies ParseArgsOptionsConfig;
|
|
46
|
+
|
|
47
|
+
export const SKILLS_USAGE = `Usage: lernapps skills --archetype <name>
|
|
48
|
+
|
|
49
|
+
Adds the skills of the archetype the plan chose to the agent directories (.agents/skills/ and those of the agents
|
|
50
|
+
\`skills\` finds), next to those lernapps init synced. The skills of the other archetypes stay out.
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--archetype <name> the archetype of the plan
|
|
54
|
+
-h, --help print this help
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
/** The version of the package `skills` that does the sync, as this tooling depends on it. */
|
|
58
|
+
const SKILLS_BIN = () => {
|
|
59
|
+
const manifest = createRequire(import.meta.url).resolve("skills/package.json");
|
|
60
|
+
const bin = readJson(manifest)["bin"] as Record<string, string> | undefined;
|
|
61
|
+
return join(dirname(manifest), bin?.["skills"] ?? "bin/cli.mjs");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** The names of the skills of the archetypes, with the archetype each belongs to (lernapps-<archetype>[-…]). */
|
|
65
|
+
export function archetypeSkills(): { name: string; archetype: string }[] {
|
|
66
|
+
const dir = fromPackage("skills", "archetypes");
|
|
67
|
+
if (!existsSync(dir)) return [];
|
|
68
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
69
|
+
.filter((entry) => entry.isDirectory())
|
|
70
|
+
.map((entry) => ({
|
|
71
|
+
name: entry.name,
|
|
72
|
+
archetype: /^lernapps-([a-z]+)(?:-|$)/.exec(entry.name)?.[1] ?? entry.name,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The script of a command that is run; its failure is an error of `fail`. */
|
|
77
|
+
function run(command: string, args: string[], fail: (message: string) => Error, cwd = "."): void {
|
|
78
|
+
const result = spawnSync(command, args, { cwd, stdio: ["ignore", 2, 2] });
|
|
79
|
+
if (result.error) throw fail(`${command} cannot run: ${result.error.message}`);
|
|
80
|
+
if (result.status !== 0)
|
|
81
|
+
throw fail(`${command} ${args.join(" ")} failed (exit ${result.status}); its output is above`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `skills experimental_sync` in `dir`, without the skills of the archetypes in `exclude`. The skills are copied, not
|
|
86
|
+
* linked into node_modules: the app commits them, and a fresh clone has them before its first npm install.
|
|
87
|
+
*/
|
|
88
|
+
function syncSkills(dir: string, exclude: string[]): void {
|
|
89
|
+
const filters = exclude.flatMap((name) => [`${TOOLING}#${name}`]);
|
|
90
|
+
run(
|
|
91
|
+
process.execPath,
|
|
92
|
+
[
|
|
93
|
+
SKILLS_BIN(),
|
|
94
|
+
"experimental_sync",
|
|
95
|
+
"-y",
|
|
96
|
+
"--no-remote",
|
|
97
|
+
"--copy",
|
|
98
|
+
...(filters.length > 0 ? ["--exclude", ...filters] : []),
|
|
99
|
+
],
|
|
100
|
+
(message) => new CreateError(message),
|
|
101
|
+
dir,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A package name from the folder's name: lower case, letters, digits and hyphens. */
|
|
106
|
+
const slug = (dir: string) =>
|
|
107
|
+
basename(dir)
|
|
108
|
+
.toLowerCase()
|
|
109
|
+
.normalize("NFKD")
|
|
110
|
+
.replace(/[̀-ͯ]/g, "")
|
|
111
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
112
|
+
.replace(/^-+|-+$/g, "") || "app";
|
|
113
|
+
|
|
114
|
+
export function init(args: string[]): number {
|
|
115
|
+
const { values, positionals } = parseArgs({ args, options: INIT_OPTIONS, strict: true, allowPositionals: true });
|
|
116
|
+
if (values.help) {
|
|
117
|
+
process.stdout.write(INIT_USAGE);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
if (positionals.length !== 1 || !/^[a-z0-9][a-z0-9-]*$/.test(positionals[0] ?? "")) {
|
|
121
|
+
throw new UsageError("init needs one <short name> for the plan: lower case letters, digits and hyphens");
|
|
122
|
+
}
|
|
123
|
+
const name = positionals[0] ?? "plan";
|
|
124
|
+
const dir = resolve(".");
|
|
125
|
+
const present = readdirSync(dir).filter((entry) => entry !== ".git");
|
|
126
|
+
if (present.length > 0) {
|
|
127
|
+
throw new CreateError(
|
|
128
|
+
`${dir} is not empty (${present.slice(0, 5).join(", ")}${present.length > 5 ? ", ..." : ""}): start in a new, empty folder`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const addon = values.addon;
|
|
132
|
+
writeJson(join(dir, "package.json"), {
|
|
133
|
+
name: slug(dir),
|
|
134
|
+
version: "0.0.0",
|
|
135
|
+
private: true,
|
|
136
|
+
lernapps: { init: true },
|
|
137
|
+
});
|
|
138
|
+
const fail = (message: string) => new CreateError(message);
|
|
139
|
+
run(
|
|
140
|
+
"npm",
|
|
141
|
+
["install", "--save-dev", "--no-audit", "--no-fund", `${TOOLING}@${ownVersion()}`, ...(addon ? [addon] : [])],
|
|
142
|
+
fail,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const source = (packageName: string, ...path: string[]) => join(dir, "node_modules", packageName, ...path);
|
|
146
|
+
let template = source(TOOLING, "guidance", "plan-template.md");
|
|
147
|
+
if (addon !== undefined) {
|
|
148
|
+
const manifest = readJson(source(addon, "package.json"));
|
|
149
|
+
const relative = (manifest.lernapps as { planTemplate?: string } | undefined)?.planTemplate;
|
|
150
|
+
if (relative !== undefined) template = source(addon, relative);
|
|
151
|
+
}
|
|
152
|
+
const target = join(dir, PLANS, `01-${name}.md`);
|
|
153
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
154
|
+
writeFileSync(target, readFileSync(template));
|
|
155
|
+
syncSkills(
|
|
156
|
+
dir,
|
|
157
|
+
archetypeSkills().map((skill) => skill.name),
|
|
158
|
+
);
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
`lernapps init: ${relative(dir, target)} from ${relativeTemplate(template, dir)}; the skills of the packages are installed\nNext: read ${relative(dir, target)} completely and follow it\n`,
|
|
161
|
+
);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const relative = (from: string, to: string) => (to.startsWith(from) ? to.slice(from.length + 1) : to);
|
|
166
|
+
const relativeTemplate = (template: string, dir: string) => relative(join(dir, "node_modules"), template);
|
|
167
|
+
|
|
168
|
+
export function skills(args: string[]): number {
|
|
169
|
+
const { values, positionals } = parseArgs({ args, options: SKILLS_OPTIONS, strict: true, allowPositionals: true });
|
|
170
|
+
if (values.help) {
|
|
171
|
+
process.stdout.write(SKILLS_USAGE);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
if (positionals.length > 0) throw new UsageError(`skills takes no arguments, not ${positionals.join(" ")}`);
|
|
175
|
+
const known = [...new Set(archetypeSkills().map((skill) => skill.archetype))].sort();
|
|
176
|
+
if (values.archetype === undefined) throw new UsageError(`skills needs --archetype <name> (${known.join(", ")})`);
|
|
177
|
+
if (!known.includes(values.archetype)) {
|
|
178
|
+
throw new UsageError(`unknown archetype: ${values.archetype} (known: ${known.join(", ")})`);
|
|
179
|
+
}
|
|
180
|
+
const others = archetypeSkills()
|
|
181
|
+
.filter((skill) => skill.archetype !== values.archetype)
|
|
182
|
+
.map((skill) => skill.name);
|
|
183
|
+
syncSkills(resolve("."), others);
|
|
184
|
+
process.stdout.write(`lernapps skills: the skills of the archetype ${values.archetype} are installed\n`);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// `lernapps update`: moves an existing app to the versions of this @lernapps/tooling. Run from the latest tooling
|
|
2
|
+
// (`npx --yes @lernapps/tooling@latest update`), it sets the app's runtime @lernapps/app-templates to the version this
|
|
3
|
+
// tooling depends on (the runtime its tests ran with) and the tooling to its own version, and the app's workflows to
|
|
4
|
+
// the actions of that version's tag; then it runs npm install, installs the hooks again and says what moved. It takes
|
|
5
|
+
// the tooling's version, not the newest runtime: the generator's tests run the templates and the runtime of that
|
|
6
|
+
// version with this tooling, so the pair is known to work together. An app made when apps pinned git commits has
|
|
7
|
+
// them replaced with versions.
|
|
8
|
+
// It never moves the runtime back: a newer version (Renovate moves the runtime too) stays. An app made before the
|
|
9
|
+
// generator wrote the content guide gets its skeleton (generator/create.ts), and one made before the plans had a
|
|
10
|
+
// folder gets its plan .vibe/plan.md moved to .vibe/plans/01-first-version.md (src/plan.ts). The app's lernapps-*
|
|
11
|
+
// skills, which the app commits, are replaced with those of this tooling (its skills/), in the layout of
|
|
12
|
+
// `npx skills add`; the app's own skills (app-content, any other) stay as they are. An interactive app's architecture
|
|
13
|
+
// is replaced outside its app sections, which stay as they are (generator/architecture.ts).
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import {
|
|
17
|
+
cpSync,
|
|
18
|
+
existsSync,
|
|
19
|
+
lstatSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readlinkSync,
|
|
24
|
+
renameSync,
|
|
25
|
+
rmSync,
|
|
26
|
+
symlinkSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
30
|
+
import { parseArgs, type ParseArgsOptionsConfig } from "node:util";
|
|
31
|
+
import { installHooks } from "../archetypes/shared/install-hooks.ts";
|
|
32
|
+
import { currentPlan, LEGACY_PLAN, MIGRATED_PLAN, planFrontMatter, plans } from "../src/plan.ts";
|
|
33
|
+
import { ARCHITECTURE, architectureArchetype, ArchitectureError, writeArchitecture } from "./architecture.ts";
|
|
34
|
+
import { UsageError } from "../src/usage.ts";
|
|
35
|
+
import {
|
|
36
|
+
addContentGuide,
|
|
37
|
+
CreateError,
|
|
38
|
+
fromPackage,
|
|
39
|
+
IGNORED_SKILLS_LINES,
|
|
40
|
+
ownVersion,
|
|
41
|
+
readJson,
|
|
42
|
+
RUNTIME,
|
|
43
|
+
SKILLS_LOCK,
|
|
44
|
+
templatesPackage,
|
|
45
|
+
TOOLING,
|
|
46
|
+
} from "./create.ts";
|
|
47
|
+
|
|
48
|
+
/** The options of `lernapps update`, as node:util parseArgs takes them. */
|
|
49
|
+
export const UPDATE_OPTIONS = {
|
|
50
|
+
help: { type: "boolean", short: "h", default: false },
|
|
51
|
+
} as const satisfies ParseArgsOptionsConfig;
|
|
52
|
+
|
|
53
|
+
const LATEST = "npx --yes @lernapps/tooling@latest update";
|
|
54
|
+
|
|
55
|
+
export const UPDATE_USAGE = `Usage: lernapps update
|
|
56
|
+
|
|
57
|
+
Moves the app in the current folder to the versions of this @lernapps/tooling, then runs npm install. npm update
|
|
58
|
+
moves the app within its version ranges; this moves the ranges, and what npm does not know:
|
|
59
|
+
@lernapps/app-templates to the version this tooling depends on: the runtime its tests ran with. A newer version
|
|
60
|
+
(e.g. from Renovate) is kept. A git commit, as apps once pinned, becomes a version
|
|
61
|
+
@lernapps/tooling to the version of this tooling (unchanged when run from the app)
|
|
62
|
+
.github/workflows/ the actions of lernapps/tooling at the tag of that version
|
|
63
|
+
.agents/skills/app-content/SKILL.md
|
|
64
|
+
the content guide's skeleton, where the app has none yet (and its link for Claude Code)
|
|
65
|
+
.vibe/plan.md the plan of an app made before the plans had a folder: moved to
|
|
66
|
+
.vibe/plans/01-first-version.md, the first of its plans (one per piece of work)
|
|
67
|
+
.agents/skills/lernapps-*, .claude/skills/lernapps-*, skills-lock.json
|
|
68
|
+
replaced with the skills of this tooling, as npx skills add lays them out: those of
|
|
69
|
+
every phase, and of the archetypes only those of the current plan's archetype; every
|
|
70
|
+
other lernapps-* skill is removed. The app's own skills (app-content, any other) stay. The lines of an earlier generator that had git ignore them are removed
|
|
71
|
+
from .gitignore: the app commits its skills
|
|
72
|
+
docs/arc42/ of an interactive app: replaced with the generic part and the interactive extension of
|
|
73
|
+
app-templates at the version of the app's runtime, except the app sections (between
|
|
74
|
+
<!-- app: begin … --> and <!-- app: end … -->), which stay as they are
|
|
75
|
+
|
|
76
|
+
Run it from the latest tooling, in the app's folder, e.g. to pick up a fixed runtime:
|
|
77
|
+
${LATEST}
|
|
78
|
+
Run as the app's own tooling (npx lernapps update), it knows only the versions the app already has.
|
|
79
|
+
Then run npx lernapps check, and commit package.json, package-lock.json, .github/workflows/ and what else it names.
|
|
80
|
+
|
|
81
|
+
Exit codes: 0 when the app is updated (or was up to date), 1 when it cannot be or only partly (the message says
|
|
82
|
+
what was not moved and why), 2 on a usage error.
|
|
83
|
+
|
|
84
|
+
Options:
|
|
85
|
+
-h, --help print this help
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
/** The app cannot be updated; the message says why and what to do. */
|
|
89
|
+
export class UpdateError extends Error {}
|
|
90
|
+
|
|
91
|
+
/** The version a dependency range names at its lowest: `^1.2.3`, `~1.2.3`, `1.2.3`; undefined for a git spec or a tag. */
|
|
92
|
+
function lowest(range: string): [number, number, number] | undefined {
|
|
93
|
+
const match = /^[\^~>=\s]*(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(range);
|
|
94
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether `a` is older than `b`. */
|
|
98
|
+
const older = (a: [number, number, number], b: [number, number, number]) =>
|
|
99
|
+
a[0] !== b[0] ? a[0] < b[0] : a[1] !== b[1] ? a[1] < b[1] : a[2] < b[2];
|
|
100
|
+
|
|
101
|
+
/** The next range of a dependency at `version`, and what the output says about it. */
|
|
102
|
+
export function nextRange(name: string, current: string | undefined, version: string): { spec: string; line: string } {
|
|
103
|
+
const target = `^${version}`;
|
|
104
|
+
// a local copy (a folder or a tarball of the package, as an app under development uses) is the creator's choice
|
|
105
|
+
if (current !== undefined && /^(file:|link:|\.{0,2}\/)/.test(current)) {
|
|
106
|
+
return { spec: current, line: `${name}: kept ${current}, a local copy` };
|
|
107
|
+
}
|
|
108
|
+
const now = current === undefined ? undefined : lowest(current);
|
|
109
|
+
const next = lowest(version);
|
|
110
|
+
if (current === target) return { spec: current, line: `${name}: unchanged (${current})` };
|
|
111
|
+
// a newer version than this tooling's stays: it must never move back
|
|
112
|
+
if (now !== undefined && next !== undefined && !older(now, next)) {
|
|
113
|
+
return { spec: current ?? target, line: `${name}: kept ${current}, not moved to ${target}: it is not older` };
|
|
114
|
+
}
|
|
115
|
+
return { spec: target, line: `${name}: ${current ?? "none"} → ${target}` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The workflows' `uses: lernapps/tooling/actions/<name>@<ref>`, set to `ref` (a commit or a tag); the files it changed. */
|
|
119
|
+
function moveActions(ref: string): string[] {
|
|
120
|
+
const changed: string[] = [];
|
|
121
|
+
for (const file of workflows()) {
|
|
122
|
+
const text = readFileSync(file, "utf8");
|
|
123
|
+
const next = text.replace(/(lernapps\/tooling\/actions\/[\w-]+@)(?:[0-9a-f]{40}|v\d+\.\d+\.\d+)\b/g, `$1${ref}`);
|
|
124
|
+
if (next !== text) {
|
|
125
|
+
writeFileSync(file, next);
|
|
126
|
+
changed.push(file);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return changed;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Moves the plan of an app made before the plans had a folder, .vibe/plan.md, to .vibe/plans/01-first-version.md,
|
|
134
|
+
* where there is no plan 01 yet; what it did, as lines of the output. A move only: git sees the rename when the
|
|
135
|
+
* change is committed.
|
|
136
|
+
*/
|
|
137
|
+
function migratePlan(): string[] {
|
|
138
|
+
if (!existsSync(LEGACY_PLAN)) return [];
|
|
139
|
+
if (plans().some((plan) => /\/01-[^/]+$/.test(plan))) {
|
|
140
|
+
return [
|
|
141
|
+
`plan: kept ${LEGACY_PLAN}: .vibe/plans/ already has a plan 01. Compare the two and keep one as the plan 01`,
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
mkdirSync(dirname(MIGRATED_PLAN), { recursive: true });
|
|
145
|
+
renameSync(LEGACY_PLAN, MIGRATED_PLAN);
|
|
146
|
+
return [
|
|
147
|
+
`plan: moved ${LEGACY_PLAN} to ${MIGRATED_PLAN}, unchanged: each piece of work has its own plan in .vibe/plans/`,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The skills of this tooling, as it ships them (skills/<group>/<name>/), and as the app keeps them (flat, by name).
|
|
153
|
+
* Every app has those of the groups process, tooling and content; of the group archetypes only those of its own
|
|
154
|
+
* archetype (lernapps-<archetype> and lernapps-<archetype>-<part>, e.g. lernapps-explainer-design), which the plan
|
|
155
|
+
* installs in the phase that needs it.
|
|
156
|
+
*/
|
|
157
|
+
const SKILL_PREFIX = "lernapps-";
|
|
158
|
+
const TOOLING_SOURCE = "lernapps/tooling";
|
|
159
|
+
const ARCHETYPE_GROUP = "archetypes";
|
|
160
|
+
const SKILLS = join(".agents", "skills");
|
|
161
|
+
const SKILL_LINKS = join(".claude", "skills");
|
|
162
|
+
|
|
163
|
+
/** The hash `npx skills` keeps in skills-lock.json (computedHash): every file of the folder, path and content. */
|
|
164
|
+
function skillHash(dir: string): string {
|
|
165
|
+
const files: string[] = [];
|
|
166
|
+
const walk = (folder: string) => {
|
|
167
|
+
for (const entry of readdirSync(folder, { withFileTypes: true })) {
|
|
168
|
+
if (entry.isDirectory() && entry.name !== ".git" && entry.name !== "node_modules") walk(join(folder, entry.name));
|
|
169
|
+
else if (entry.isFile()) files.push(relative(dir, join(folder, entry.name)).split("\\").join("/"));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
walk(dir);
|
|
173
|
+
const hash = createHash("sha256");
|
|
174
|
+
for (const file of files.sort((a, b) => a.localeCompare(b))) {
|
|
175
|
+
hash.update(file);
|
|
176
|
+
hash.update(readFileSync(join(dir, file)));
|
|
177
|
+
}
|
|
178
|
+
return hash.digest("hex");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface SkillsLock {
|
|
182
|
+
version?: number;
|
|
183
|
+
skills?: Record<string, Record<string, unknown>>;
|
|
184
|
+
[key: string]: unknown;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The lernapps-* skills below `source`: each folder with a SKILL.md, its group and its path relative to `source`. */
|
|
188
|
+
function shippedSkills(source: string): { name: string; group: string; path: string }[] {
|
|
189
|
+
const found: { name: string; group: string; path: string }[] = [];
|
|
190
|
+
for (const group of readdirSync(source, { withFileTypes: true })) {
|
|
191
|
+
if (!group.isDirectory()) continue;
|
|
192
|
+
for (const skill of readdirSync(join(source, group.name), { withFileTypes: true })) {
|
|
193
|
+
if (
|
|
194
|
+
skill.isDirectory() &&
|
|
195
|
+
skill.name.startsWith(SKILL_PREFIX) &&
|
|
196
|
+
existsSync(join(source, group.name, skill.name, "SKILL.md"))
|
|
197
|
+
) {
|
|
198
|
+
found.push({ name: skill.name, group: group.name, path: `${group.name}/${skill.name}` });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return found;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Replaces the app's lernapps-* skills with those of this tooling, as `npx skills add lernapps/tooling` lays them out:
|
|
207
|
+
* the folder in .agents/skills/, a relative link to it in .claude/skills/ (a copy where the system allows no link), and
|
|
208
|
+
* its entry in skills-lock.json with the hash of the new files. Of the archetypes' skills only those of the archetype
|
|
209
|
+
* the current plan names; without one, those the app has. Removes every other lernapps-* skill, unless its entry in
|
|
210
|
+
* skills-lock.json names another source than this tooling (lernapps-practice-* come from lernapps/practice).
|
|
211
|
+
* Never touches another skill. What it did, as lines of the output.
|
|
212
|
+
*/
|
|
213
|
+
export function replaceSkills(dir = ".", source = fromPackage("skills")): string[] {
|
|
214
|
+
const skillsDir = join(dir, SKILLS);
|
|
215
|
+
const linksDir = join(dir, SKILL_LINKS);
|
|
216
|
+
const lockFile = join(dir, SKILLS_LOCK);
|
|
217
|
+
let lock: SkillsLock = { version: 1, skills: {} };
|
|
218
|
+
try {
|
|
219
|
+
lock = JSON.parse(readFileSync(lockFile, "utf8")) as SkillsLock;
|
|
220
|
+
} catch {
|
|
221
|
+
// none yet, or unreadable: written anew
|
|
222
|
+
}
|
|
223
|
+
// a skill whose lock entry names another source (e.g. lernapps-practice-* of lernapps/practice) is not ours: it stays
|
|
224
|
+
const foreign = (name: string) => {
|
|
225
|
+
const source = lock.skills?.[name]?.["source"];
|
|
226
|
+
return source !== undefined && source !== TOOLING_SOURCE;
|
|
227
|
+
};
|
|
228
|
+
const present = existsSync(skillsDir)
|
|
229
|
+
? readdirSync(skillsDir).filter((name) => name.startsWith(SKILL_PREFIX) && !foreign(name))
|
|
230
|
+
: [];
|
|
231
|
+
const plan = currentPlan(dir);
|
|
232
|
+
const archetype = plan === undefined ? undefined : planFrontMatter(join(dir, plan))["archetype"];
|
|
233
|
+
const own = (name: string) =>
|
|
234
|
+
typeof archetype === "string"
|
|
235
|
+
? name === `${SKILL_PREFIX}${archetype}` || name.startsWith(`${SKILL_PREFIX}${archetype}-`)
|
|
236
|
+
: present.includes(name);
|
|
237
|
+
const shipped = shippedSkills(source).filter(({ name, group }) => group !== ARCHETYPE_GROUP || own(name));
|
|
238
|
+
const folder = new Map(shipped.map(({ name, path }) => [name, path]));
|
|
239
|
+
const ours = [...folder.keys()].sort();
|
|
240
|
+
const entries: Record<string, Record<string, unknown>> = { ...lock.skills };
|
|
241
|
+
const added: string[] = [];
|
|
242
|
+
const changed: string[] = [];
|
|
243
|
+
const removed = present.filter((name) => !ours.includes(name));
|
|
244
|
+
for (const name of removed) {
|
|
245
|
+
rmSync(join(skillsDir, name), { recursive: true, force: true });
|
|
246
|
+
rmSync(join(linksDir, name), { recursive: true, force: true });
|
|
247
|
+
delete entries[name];
|
|
248
|
+
}
|
|
249
|
+
for (const name of ours) {
|
|
250
|
+
const target = join(skillsDir, name);
|
|
251
|
+
const before = existsSync(target) ? skillHash(target) : undefined;
|
|
252
|
+
rmSync(target, { recursive: true, force: true });
|
|
253
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
254
|
+
cpSync(join(source, folder.get(name) ?? name), target, { recursive: true });
|
|
255
|
+
const hash = skillHash(target);
|
|
256
|
+
if (before === undefined) added.push(name);
|
|
257
|
+
else if (before !== hash) changed.push(name);
|
|
258
|
+
const link = join(linksDir, name);
|
|
259
|
+
const to = `../../.agents/skills/${name}`;
|
|
260
|
+
let linked = false;
|
|
261
|
+
try {
|
|
262
|
+
linked = lstatSync(link).isSymbolicLink() && readlinkSync(link) === to;
|
|
263
|
+
} catch {
|
|
264
|
+
// no link yet
|
|
265
|
+
}
|
|
266
|
+
if (!linked) {
|
|
267
|
+
rmSync(link, { recursive: true, force: true });
|
|
268
|
+
mkdirSync(linksDir, { recursive: true });
|
|
269
|
+
try {
|
|
270
|
+
symlinkSync(to, link, "dir");
|
|
271
|
+
} catch {
|
|
272
|
+
cpSync(target, link, { recursive: true }); // no symbolic links on this system: a copy, as npx skills makes
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
entries[name] = {
|
|
276
|
+
source: TOOLING_SOURCE,
|
|
277
|
+
sourceType: "github",
|
|
278
|
+
...entries[name],
|
|
279
|
+
skillPath: `skills/${folder.get(name) ?? name}/SKILL.md`,
|
|
280
|
+
computedHash: hash,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const sorted = Object.fromEntries(
|
|
284
|
+
Object.keys(entries)
|
|
285
|
+
.sort()
|
|
286
|
+
.map((name) => [name, entries[name]]),
|
|
287
|
+
);
|
|
288
|
+
const next = `${JSON.stringify({ ...lock, version: lock.version ?? 1, skills: sorted }, null, 2)}\n`;
|
|
289
|
+
const lockChanged = !existsSync(lockFile) || readFileSync(lockFile, "utf8") !== next;
|
|
290
|
+
if (lockChanged) writeFileSync(lockFile, next);
|
|
291
|
+
const lines: string[] = [];
|
|
292
|
+
const list = (names: string[]) => names.join(", ");
|
|
293
|
+
if (added.length + changed.length + removed.length === 0) {
|
|
294
|
+
lines.push(
|
|
295
|
+
lockChanged
|
|
296
|
+
? `skills: the ${SKILL_PREFIX}* skills are those of this tooling; their entries in ${SKILLS_LOCK} updated`
|
|
297
|
+
: `skills: the ${SKILL_PREFIX}* skills are those of this tooling already (${ours.length})`,
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
const parts = [
|
|
301
|
+
...(changed.length > 0 ? [`replaced ${list(changed)}`] : []),
|
|
302
|
+
...(added.length > 0 ? [`added ${list(added)}`] : []),
|
|
303
|
+
...(removed.length > 0 ? [`removed ${list(removed)}: not of this tooling, or of another archetype`] : []),
|
|
304
|
+
];
|
|
305
|
+
lines.push(
|
|
306
|
+
`skills: ${parts.join("; ")}: the ${SKILL_PREFIX}* skills of this tooling, in ${SKILLS}/ and ${SKILL_LINKS}/`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return lines;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Removes the lines an earlier generator appended to .gitignore so that git ignored the installed skills: the app
|
|
314
|
+
* commits them now. What it did, as lines of the output.
|
|
315
|
+
*/
|
|
316
|
+
export function unignoreSkills(dir = "."): string[] {
|
|
317
|
+
const ignore = join(dir, ".gitignore");
|
|
318
|
+
if (!existsSync(ignore)) return [];
|
|
319
|
+
const lines = readFileSync(ignore, "utf8").split("\n");
|
|
320
|
+
const kept = lines.filter((line) => !IGNORED_SKILLS_LINES.includes(line));
|
|
321
|
+
if (kept.length === lines.length) return [];
|
|
322
|
+
writeFileSync(ignore, `${kept.join("\n").replace(/\n+$/, "")}\n`);
|
|
323
|
+
return ["skills: removed the lines from .gitignore that had git ignore them: the app commits its skills"];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Replaces the reference files of an interactive app's architecture with the generic part and the extension of
|
|
328
|
+
* the installed app-templates; the app's own files stay. What it did, as lines of the output, and whether it failed.
|
|
329
|
+
*/
|
|
330
|
+
function updateArchitecture(): { lines: string[]; failed: boolean } {
|
|
331
|
+
const archetype = architectureArchetype();
|
|
332
|
+
if (archetype === undefined) return { lines: [], failed: false };
|
|
333
|
+
try {
|
|
334
|
+
const templates = templatesPackage();
|
|
335
|
+
return { lines: writeArchitecture(".", templates.root, archetype, templates.version), failed: false };
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (!(error instanceof ArchitectureError) && !(error instanceof CreateError)) throw error;
|
|
338
|
+
return { lines: [`architecture: not replaced: ${error.message}`], failed: true };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The app's workflow files. */
|
|
343
|
+
function workflows(): string[] {
|
|
344
|
+
const dir = join(".github", "workflows");
|
|
345
|
+
if (!existsSync(dir)) return [];
|
|
346
|
+
return readdirSync(dir)
|
|
347
|
+
.filter((file) => /\.ya?ml$/.test(file))
|
|
348
|
+
.map((name) => join(dir, name));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function update(args: string[]): number {
|
|
352
|
+
const { values, positionals } = parseArgs({ args, options: UPDATE_OPTIONS, strict: true, allowPositionals: true });
|
|
353
|
+
if (values.help) {
|
|
354
|
+
process.stdout.write(UPDATE_USAGE);
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
if (positionals.length > 0) throw new UsageError(`update takes no arguments, not ${positionals.join(" ")}`);
|
|
358
|
+
if (!existsSync("package.json")) {
|
|
359
|
+
throw new UpdateError("no package.json here: run it in the app's folder");
|
|
360
|
+
}
|
|
361
|
+
const app = readJson("package.json");
|
|
362
|
+
const devDependencies = app.devDependencies ?? {};
|
|
363
|
+
const current = devDependencies[TOOLING];
|
|
364
|
+
if (current === undefined) {
|
|
365
|
+
throw new UpdateError(`this is not an app from lernapps create: package.json has no devDependency ${TOOLING}`);
|
|
366
|
+
}
|
|
367
|
+
const templates = templatesPackage();
|
|
368
|
+
|
|
369
|
+
const lines: string[] = [];
|
|
370
|
+
const specs: string[] = [];
|
|
371
|
+
const notMoved: string[] = [];
|
|
372
|
+
const runtime = devDependencies[RUNTIME];
|
|
373
|
+
if (runtime !== undefined) {
|
|
374
|
+
const next = nextRange(RUNTIME, runtime, templates.version);
|
|
375
|
+
if (next.spec !== runtime) specs.push(`${RUNTIME}@${next.spec}`);
|
|
376
|
+
lines.push(next.line);
|
|
377
|
+
}
|
|
378
|
+
const version = ownVersion();
|
|
379
|
+
const nextTooling = nextRange(TOOLING, current, version);
|
|
380
|
+
if (nextTooling.spec !== current) specs.push(`${TOOLING}@${nextTooling.spec}`);
|
|
381
|
+
lines.push(nextTooling.line);
|
|
382
|
+
// the app's own tooling knows the version the app already has: nothing newer
|
|
383
|
+
if (nextTooling.spec === current && resolve(fromPackage()) === resolve("node_modules", TOOLING)) {
|
|
384
|
+
lines.push(`this is the app's own tooling, which knows only its own version; for the latest run: ${LATEST}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (specs.length > 0) {
|
|
388
|
+
const install = spawnSync("npm", ["install", "--no-audit", "--no-fund", "--save-dev", ...specs], {
|
|
389
|
+
stdio: ["ignore", 2, 2],
|
|
390
|
+
});
|
|
391
|
+
if (install.error || install.status !== 0) {
|
|
392
|
+
throw new UpdateError(
|
|
393
|
+
`npm install ${specs.join(" ")} failed${install.error ? `: ${install.error.message}` : ` (exit ${install.status})`}; its output is above. Fix the cause, then run lernapps update again`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
// npm install <spec> runs no prepare script: the hooks of the new tooling, as npm install would install them
|
|
397
|
+
if (installHooks([]) !== 0)
|
|
398
|
+
throw new UpdateError("lernapps hooks failed (see above); run npx lernapps hooks again");
|
|
399
|
+
}
|
|
400
|
+
const changed = moveActions(`v${version}`);
|
|
401
|
+
if (changed.length > 0) lines.push(`the actions of lernapps/tooling in ${changed.join(", ")}: v${version}`);
|
|
402
|
+
// an app made before the generator wrote the content guide gets its skeleton
|
|
403
|
+
const guide = addContentGuide(".");
|
|
404
|
+
lines.push(...guide.map((line) => `content guide: ${line}`));
|
|
405
|
+
const plan = migratePlan();
|
|
406
|
+
lines.push(...plan);
|
|
407
|
+
const skills = [...replaceSkills(), ...unignoreSkills()];
|
|
408
|
+
lines.push(...skills);
|
|
409
|
+
const architecture = updateArchitecture();
|
|
410
|
+
lines.push(...architecture.lines);
|
|
411
|
+
if (architecture.failed) notMoved.push("the architecture");
|
|
412
|
+
process.stdout.write(`${lines.map((line) => `lernapps update: ${line}`).join("\n")}\n`);
|
|
413
|
+
if (notMoved.length > 0) {
|
|
414
|
+
throw new UpdateError(
|
|
415
|
+
`partly updated: ${notMoved.join(" and ")} not moved (see above). Run it from the latest tooling, installed by npx: ${LATEST}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const files = ["package.json", "package-lock.json", ".github/workflows/"];
|
|
419
|
+
const skillsMoved = skills.some((line) => !line.includes("those of this tooling already"));
|
|
420
|
+
if (guide.length > 0 || skillsMoved) files.push(".agents/", ".claude/");
|
|
421
|
+
if (skillsMoved) files.push(SKILLS_LOCK);
|
|
422
|
+
if (skills.some((line) => line.includes(".gitignore"))) files.push(".gitignore");
|
|
423
|
+
if (architecture.lines.length > 0) files.push(`${ARCHITECTURE}/`);
|
|
424
|
+
if (plan.some((line) => line.startsWith("plan: moved"))) files.push(".vibe/ (git add -A .vibe, to keep the move)");
|
|
425
|
+
process.stdout.write(
|
|
426
|
+
`Next: npx lernapps check, then commit ${files.slice(0, -1).join(", ")} and ${files.at(-1) ?? ""}\n`,
|
|
427
|
+
);
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Learning app for lernapps.net: static, runs in the browser, collects nothing, accessible. Built with its creator.
|
|
4
|
+
|
|
5
|
+
## Start here
|
|
6
|
+
|
|
7
|
+
1. Current plan: `.vibe/plans/<NN>-<short name>.md` with the highest NN.
|
|
8
|
+
2. No plan, or its Commit is completed: start a new plan. Copy a template complete and unchanged from
|
|
9
|
+
`@lernapps/tooling/guidance/` (in `node_modules/`):
|
|
10
|
+
- `plan-template.md`: the first version;
|
|
11
|
+
- `increment-template.md`: every later change.
|
|
12
|
+
3. Read the plan before each step. Do its tasks in order. Each phase names its skills.
|
|
13
|
+
|
|
14
|
+
## Always
|
|
15
|
+
|
|
16
|
+
- Read guidance and skills from the file, in full. Never use a fetch tool that summarises.
|
|
17
|
+
- Push only the branch the current plan names. A push to `main` publishes.
|
|
18
|
+
- Never `--no-verify`. Never edit `prePushFailures` or `failedChecks`.
|
|
19
|
+
- Never edit a `lernapps-*` skill: `lernapps update` replaces them. The app's own skills (`app-content`, `app-*`)
|
|
20
|
+
take precedence; app rules go there.
|
|
21
|
+
- Nothing about a real learner in any file or commit message (`no-learner-data-in-texts`).
|
|
22
|
+
|
|
23
|
+
## Language
|
|
24
|
+
|
|
25
|
+
- Learners, teachers, parents: German, plain language.
|
|
26
|
+
- Creator: their language, plain words, no forms.
|