@boopkit/cli 0.6.9 → 0.6.11

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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Detect package.json scripts that contradict the runner settings a capability
3
+ * now ships, and offer the edit that reconciles them.
4
+ *
5
+ * The case this exists for: `cap-testing` used to ship `"test": "jest --runInBand"`.
6
+ * It now ships a `jest.config.js` carrying `maxWorkers` and `workerIdleMemoryLimit`
7
+ * instead, because in-band execution runs every test file through one process that
8
+ * Jest never recycles, and a long suite dies as that process's heap grows.
9
+ *
10
+ * `boop update` cannot simply fix those projects. Scripts are merged only when
11
+ * absent (see `merge-scripts` in lib/execute.ts), which is the right default —
12
+ * clobbering a script somebody customised would be worse than leaving it stale.
13
+ * So the old command survives the update, and because a CLI `--runInBand` is the
14
+ * first and unconditional branch of Jest's `shouldRunInBand()`, it overrides the
15
+ * new config entirely. That is the state worth warning about: the project now has
16
+ * a config file that looks like the fix, reads like the fix in review, and does
17
+ * nothing at all.
18
+ *
19
+ * Warning rather than prompting is deliberate. `boop update` is usually run by an
20
+ * agent, often inside `boop loop`, where there is no terminal to answer a question
21
+ * and a blocking prompt would hang the run. The warning therefore has to carry
22
+ * everything needed to act: which script, what is wrong with it, and the exact
23
+ * command that fixes it.
24
+ */
25
+ export interface StaleScript {
26
+ /** The script's key in package.json, e.g. `test`. */
27
+ name: string;
28
+ /** The command as it stands today. */
29
+ command: string;
30
+ /** What the command becomes once the offending flags are removed. */
31
+ rewritten: string;
32
+ /** The flags that triggered the finding, in the order they appeared. */
33
+ flags: string[];
34
+ }
35
+ /**
36
+ * Pure: given a package.json `scripts` object, return the scripts whose command
37
+ * forces in-band Jest execution and therefore defeats the shipped config.
38
+ */
39
+ export declare function findStaleRunnerScripts(scripts: Record<string, string> | undefined): StaleScript[];
40
+ /**
41
+ * Apply the findings to the project's package.json.
42
+ *
43
+ * A finding is only applied when the script still holds exactly the command the
44
+ * finding was made against. Anything else means the file changed underneath us,
45
+ * and rewriting it then would discard somebody's edit. Returns the findings that
46
+ * were actually written.
47
+ */
48
+ export declare function rewriteStaleRunnerScripts(targetDir: string, stale: StaleScript[]): StaleScript[];
49
+ /**
50
+ * The warning shown after an update. Written to be actionable by an agent reading
51
+ * it in a transcript with no other context.
52
+ */
53
+ export declare function formatStaleScriptWarning(stale: StaleScript[]): string;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ /**
3
+ * Detect package.json scripts that contradict the runner settings a capability
4
+ * now ships, and offer the edit that reconciles them.
5
+ *
6
+ * The case this exists for: `cap-testing` used to ship `"test": "jest --runInBand"`.
7
+ * It now ships a `jest.config.js` carrying `maxWorkers` and `workerIdleMemoryLimit`
8
+ * instead, because in-band execution runs every test file through one process that
9
+ * Jest never recycles, and a long suite dies as that process's heap grows.
10
+ *
11
+ * `boop update` cannot simply fix those projects. Scripts are merged only when
12
+ * absent (see `merge-scripts` in lib/execute.ts), which is the right default —
13
+ * clobbering a script somebody customised would be worse than leaving it stale.
14
+ * So the old command survives the update, and because a CLI `--runInBand` is the
15
+ * first and unconditional branch of Jest's `shouldRunInBand()`, it overrides the
16
+ * new config entirely. That is the state worth warning about: the project now has
17
+ * a config file that looks like the fix, reads like the fix in review, and does
18
+ * nothing at all.
19
+ *
20
+ * Warning rather than prompting is deliberate. `boop update` is usually run by an
21
+ * agent, often inside `boop loop`, where there is no terminal to answer a question
22
+ * and a blocking prompt would hang the run. The warning therefore has to carry
23
+ * everything needed to act: which script, what is wrong with it, and the exact
24
+ * command that fixes it.
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.findStaleRunnerScripts = findStaleRunnerScripts;
61
+ exports.rewriteStaleRunnerScripts = rewriteStaleRunnerScripts;
62
+ exports.formatStaleScriptWarning = formatStaleScriptWarning;
63
+ const fs = __importStar(require("fs"));
64
+ const path = __importStar(require("path"));
65
+ /** Jest's in-band flag and its single-letter alias. */
66
+ const IN_BAND_FLAGS = new Set(['--runInBand', '--runInBand=true', '-i']);
67
+ /**
68
+ * Does this command invoke Jest? Matched on the token's basename so that `jest`,
69
+ * `npx jest` and `node_modules/.bin/jest` all count, while a command that merely
70
+ * contains the same characters somewhere does not.
71
+ */
72
+ function invokesJest(tokens) {
73
+ return tokens.some((token) => {
74
+ if (token.startsWith('-'))
75
+ return false;
76
+ const base = path.basename(token);
77
+ return base === 'jest' || base === 'jest.js';
78
+ });
79
+ }
80
+ /**
81
+ * Pure: given a package.json `scripts` object, return the scripts whose command
82
+ * forces in-band Jest execution and therefore defeats the shipped config.
83
+ */
84
+ function findStaleRunnerScripts(scripts) {
85
+ if (!scripts)
86
+ return [];
87
+ const findings = [];
88
+ for (const [name, command] of Object.entries(scripts)) {
89
+ if (typeof command !== 'string')
90
+ continue;
91
+ const tokens = command.split(/\s+/).filter(Boolean);
92
+ if (!invokesJest(tokens))
93
+ continue;
94
+ const flags = tokens.filter((t) => IN_BAND_FLAGS.has(t));
95
+ if (flags.length === 0)
96
+ continue;
97
+ findings.push({
98
+ name,
99
+ command,
100
+ rewritten: tokens.filter((t) => !IN_BAND_FLAGS.has(t)).join(' '),
101
+ flags,
102
+ });
103
+ }
104
+ return findings;
105
+ }
106
+ /**
107
+ * Apply the findings to the project's package.json.
108
+ *
109
+ * A finding is only applied when the script still holds exactly the command the
110
+ * finding was made against. Anything else means the file changed underneath us,
111
+ * and rewriting it then would discard somebody's edit. Returns the findings that
112
+ * were actually written.
113
+ */
114
+ function rewriteStaleRunnerScripts(targetDir, stale) {
115
+ if (stale.length === 0)
116
+ return [];
117
+ const pkgPath = path.join(targetDir, 'package.json');
118
+ if (!fs.existsSync(pkgPath))
119
+ return [];
120
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
121
+ if (!pkg.scripts)
122
+ return [];
123
+ const applied = [];
124
+ for (const finding of stale) {
125
+ if (pkg.scripts[finding.name] !== finding.command)
126
+ continue;
127
+ pkg.scripts[finding.name] = finding.rewritten;
128
+ applied.push(finding);
129
+ }
130
+ if (applied.length > 0) {
131
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
132
+ }
133
+ return applied;
134
+ }
135
+ /**
136
+ * The warning shown after an update. Written to be actionable by an agent reading
137
+ * it in a transcript with no other context.
138
+ */
139
+ function formatStaleScriptWarning(stale) {
140
+ if (stale.length === 0)
141
+ return '';
142
+ const lines = [];
143
+ lines.push(`\x1b[33mWARN\x1b[0m ${stale.length === 1 ? 'A package.json script forces' : `${stale.length} package.json scripts force`} Jest to run in band, which overrides the settings this capability ships.`);
144
+ for (const finding of stale) {
145
+ lines.push(` ${finding.name}: ${finding.command}`);
146
+ lines.push(` ${' '.repeat(finding.name.length)} \x1b[32m→ ${finding.rewritten}\x1b[0m`);
147
+ }
148
+ lines.push('');
149
+ lines.push(' Why it matters: in band, every test file runs through one process that Jest');
150
+ lines.push(' never recycles, so the heap grows for the whole run until a long suite dies.');
151
+ lines.push(' The scaffolded jest.config.js sets maxWorkers and workerIdleMemoryLimit to');
152
+ lines.push(' bound that, but a command-line --runInBand wins over both, so the config is');
153
+ lines.push(' present and inert until this script changes.');
154
+ lines.push('');
155
+ lines.push(' boop did not change it for you because it does not overwrite scripts you may');
156
+ lines.push(' have customised. To apply the edit above:');
157
+ lines.push('');
158
+ lines.push(' \x1b[36mboop update --fix-scripts\x1b[0m');
159
+ return lines.join('\n');
160
+ }
@@ -49,6 +49,7 @@ const scaffold_1 = require("./scaffold");
49
49
  const capability_1 = require("./capability");
50
50
  const project_1 = require("./project");
51
51
  const safe_io_1 = require("./safe-io");
52
+ const install_layout_1 = require("./install-layout");
52
53
  /**
53
54
  * Compare a file's current hash against a stored hash.
54
55
  */
@@ -84,30 +85,11 @@ function planCapabilityUpdate(targetDir, capabilityName, projectManifest) {
84
85
  const storedHashes = meta?.fileHashes || {};
85
86
  const actions = [];
86
87
  let unchangedCount = 0;
87
- // Collect files from package scaffold dir
88
+ // Where each of the package's files lands — the same answer `boop add` used to
89
+ // put them there, so an update compares like with like.
88
90
  const packageFiles = new Map(); // relPath → absolute source path
89
- if (boopkit.scaffoldDir) {
90
- const scaffoldDir = path.join(pkgDir, boopkit.scaffoldDir);
91
- if (fs.existsSync(scaffoldDir)) {
92
- collectFiles(scaffoldDir, scaffoldDir, packageFiles);
93
- }
94
- }
95
- // Collect files from package skills dir (both flat files and folder-based skills)
96
- if (boopkit.skillsDir) {
97
- const skillsDir = path.join(pkgDir, boopkit.skillsDir);
98
- if (fs.existsSync(skillsDir)) {
99
- const entries = fs.readdirSync(skillsDir);
100
- for (const entry of entries) {
101
- const srcPath = path.join(skillsDir, entry);
102
- if (fs.statSync(srcPath).isDirectory()) {
103
- const subFiles = new Map();
104
- collectFiles(srcPath, srcPath, subFiles);
105
- for (const [subRel, subAbs] of subFiles) {
106
- packageFiles.set(path.join('skills', entry, subRel), subAbs);
107
- }
108
- }
109
- }
110
- }
91
+ for (const placement of (0, install_layout_1.capabilityPlacementsFromDir)(pkgDir)) {
92
+ packageFiles.set(placement.dest, placement.source);
111
93
  }
112
94
  // For each file in the package, determine action
113
95
  for (const [relPath, srcPath] of packageFiles) {
@@ -276,7 +258,7 @@ function executeUpdatePlan(plan, options = {}) {
276
258
  const destPath = path.join(plan.targetDir, action.relativePath);
277
259
  switch (action.action) {
278
260
  case 'overwrite': {
279
- const srcPath = resolveSourcePath(plan.packageDir, boopkit, action.relativePath);
261
+ const srcPath = resolveSourcePath(plan.packageDir, action.relativePath);
280
262
  if (srcPath) {
281
263
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
282
264
  fs.copyFileSync(srcPath, destPath);
@@ -289,7 +271,7 @@ function executeUpdatePlan(plan, options = {}) {
289
271
  const isLocallyDeleted = action.reason === 'Locally deleted';
290
272
  if (isLocallyDeleted && options.restore) {
291
273
  // Restore mode: re-add locally deleted files
292
- const srcPath = resolveSourcePath(plan.packageDir, boopkit, action.relativePath);
274
+ const srcPath = resolveSourcePath(plan.packageDir, action.relativePath);
293
275
  if (srcPath) {
294
276
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
295
277
  fs.copyFileSync(srcPath, destPath);
@@ -299,7 +281,7 @@ function executeUpdatePlan(plan, options = {}) {
299
281
  }
300
282
  else if (!isLocallyDeleted && options.force) {
301
283
  // Force mode: backup and overwrite modified files
302
- const srcPath = resolveSourcePath(plan.packageDir, boopkit, action.relativePath);
284
+ const srcPath = resolveSourcePath(plan.packageDir, action.relativePath);
303
285
  if (srcPath) {
304
286
  if (fs.existsSync(destPath)) {
305
287
  (0, safe_io_1.createBackup)(destPath);
@@ -322,7 +304,7 @@ function executeUpdatePlan(plan, options = {}) {
322
304
  break;
323
305
  }
324
306
  case 'add': {
325
- const srcPath = resolveSourcePath(plan.packageDir, boopkit, action.relativePath);
307
+ const srcPath = resolveSourcePath(plan.packageDir, action.relativePath);
326
308
  if (srcPath) {
327
309
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
328
310
  fs.copyFileSync(srcPath, destPath);
@@ -374,37 +356,12 @@ function executeUpdatePlan(plan, options = {}) {
374
356
  return result;
375
357
  }
376
358
  /**
377
- * Resolve the source path for a relative file path from the package.
378
- */
379
- function resolveSourcePath(pkgDir, boopkit, relativePath) {
380
- // Skills are stored as skills/<filename> or skills/<folder>/<file> in relPath
381
- if (relativePath.startsWith('skills/') && boopkit.skillsDir) {
382
- const subPath = relativePath.slice('skills/'.length);
383
- const srcPath = path.join(pkgDir, boopkit.skillsDir, subPath);
384
- return fs.existsSync(srcPath) ? srcPath : null;
385
- }
386
- // Scaffold files
387
- if (boopkit.scaffoldDir) {
388
- const srcPath = path.join(pkgDir, boopkit.scaffoldDir, relativePath);
389
- return fs.existsSync(srcPath) ? srcPath : null;
390
- }
391
- return null;
392
- }
393
- /**
394
- * Recursively collect files from a directory into a map of relPath → absPath.
359
+ * Resolve the source path a project-relative file was installed from — the
360
+ * placement list read backwards, so it cannot disagree with what was copied.
395
361
  */
396
- function collectFiles(baseDir, dir, result) {
397
- const entries = fs.readdirSync(dir, { withFileTypes: true });
398
- for (const entry of entries) {
399
- const fullPath = path.join(dir, entry.name);
400
- if (entry.isDirectory()) {
401
- collectFiles(baseDir, fullPath, result);
402
- }
403
- else {
404
- const relPath = path.relative(baseDir, fullPath);
405
- result.set(relPath, fullPath);
406
- }
407
- }
362
+ function resolveSourcePath(pkgDir, relativePath) {
363
+ const placement = (0, install_layout_1.capabilityPlacementsFromDir)(pkgDir).find(p => p.dest === relativePath);
364
+ return placement && fs.existsSync(placement.source) ? placement.source : null;
408
365
  }
409
366
  /**
410
367
  * Format an update result as a human-readable summary line.
package/docs/init.md CHANGED
@@ -33,7 +33,12 @@ Available profiles:
33
33
  runtime: node | deploy: heroku | port: 3000
34
34
  ```
35
35
 
36
- Right now the built-in profile is `web-app`.
36
+ `web-app` is the only profile `boop init` can scaffold from.
37
+ The other built-in profile, `development`, carries development capabilities and no application domain,
38
+ so it has no runtime, port or deploy target to scaffold with —
39
+ `boop init --profile development` is refused with a pointer to the two ways it is meant to be used:
40
+ naming it as the `profile` in an existing project's `.boop/config.json`,
41
+ or `boop add` for the capabilities you want.
37
42
 
38
43
  ## What It Creates
39
44
 
package/docs/skills.md CHANGED
@@ -37,6 +37,7 @@ Every capability ships **agent skills** — focused `SKILL.md` guides that `boop
37
37
  - **boop-skills-agent-surface-design** — When you design or audit tool surfaces (tools, schemas, APIs, conventions) that LLM agents will reach for, decide where to put the friction — adapt the system to the agent, or make the existing thing more discoverable. Last resort: prompt-engineer.
38
38
  - **boop-get-smart** — When there is a recurring fault in your own behaviour during a chat, run the five whys to find its root cause, and agree a change with the operator that makes it less likely.
39
39
  - **boop-so-what** — When the operator asks for the so-what, or says some version of 'thanks for the essay', compress what you just produced into three sentences about what they should care about.
40
+ - **boop-skills-hyperlinking** — When you need to add, review, or repair a `## Related` section that points one skill at another, follow these rules for what earns a link, how to phrase it, which way it points, how many are too many, and how to write a path that survives `boop add`.
40
41
 
41
42
  ### `testing` — Jest testing setup
42
43
 
@@ -62,6 +63,7 @@ Every capability ships **agent skills** — focused `SKILL.md` guides that `boop
62
63
  - **boop-website-dev-workflow** — When you are tasked with implementing website changes end-to-end (new pages, page rewrites, IA changes, content/CTA updates), follow this guidance.
63
64
  - **boop-design-handoff-workflow** — When the operator hands off a design package (a README plus a drop-in view template and CSS) for a redesigned surface, follow this process: read the README, VERIFY its claims against the real code, surface any buried decision to the operator, file an issue, run it as a sub-agent (branch for UAT or merge-on-clean-visual-check), then close out with knowledge integration and archival. The README is confident; confidence is not correctness — verify before you run.
64
65
  - **boop-knowledge-backfill** — When you need to integrate new knowledge from a set of specs or completed work that was not documented in reference at the time it shipped. This often happens when a feature is developed under a tight deadline and the team prioritizes shipping over documentation, or when work was done before the reference system was fully established.
66
+ - **boop-stray-disposition** — When an autonomous run (a loop, a sweep, a sub-agent session) has finished and filed follow-up issues along the way, settle each one before you write the closing summary. Every stray gets a recorded disposition — re-parented and reachable from the roadmap, folded into a spec and closed, or triaged away. Use this whenever a run ends with issues in the queue that nobody has decided to do.
65
67
 
66
68
  ## Application capabilities
67
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boopkit/cli",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "boop: the opinionated bootstrap for founder friendly, AI-first web apps - code plus agent skills, composable from day one.",
5
5
  "main": "dist/cli.js",
6
6
  "workspaces": [
@@ -22,13 +22,17 @@
22
22
  },
23
23
  "scripts": {
24
24
  "build": "tsc && node tools/generate-version-manifest.js",
25
+ "typecheck": "tsc -p tsconfig.test.json",
25
26
  "skills:index": "node tools/generate-skills-index.js",
27
+ "skills:layout": "tsx tools/generate-installed-layout.ts",
28
+ "skills:audit": "npm run --silent skills:layout -- --check && node tools/audit-skill-links.js",
26
29
  "build:caps": "./tools/build-caps.sh",
27
30
  "publish": "./tools/publish-caps.sh",
28
31
  "release": "./tools/publish-caps.sh --release",
29
32
  "dev": "tsx src/cli.ts",
30
33
  "start": "node dist/cli.js",
31
- "test": "jest"
34
+ "test": "jest",
35
+ "pretest": "npm run skills:layout"
32
36
  },
33
37
  "keywords": [],
34
38
  "license": "ISC",
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: development
3
+ description: Development skills only — for a project with no application domain (a CLI, a library, a data pipeline)
4
+ defaultCapabilities:
5
+ # Development
6
+ - coding
7
+ - testing
8
+ - project-mgmt
9
+ - product-design
10
+ - skills
11
+ - workflows
12
+ ---
13
+
14
+ # Development Profile
15
+
16
+ The development capabilities on their own,
17
+ with no application domain attached.
18
+
19
+ A project has two independent things going on:
20
+ what it *is* — a web app, a CLI, a library, a data pipeline —
21
+ and what its team *does with it* — write code, test it, plan it, design it, document it.
22
+ The `web-app` profile bundles both,
23
+ because a full-stack web application needs both.
24
+ This profile is the second half by itself,
25
+ for a project whose domain boop has nothing to say about.
26
+
27
+ That is what makes it reusable past boop.
28
+ A TypeScript CLI, a published library and an ETL job
29
+ share no application layers at all,
30
+ but they are all developed the same way.
31
+
32
+ ## What You Get
33
+
34
+ Six capabilities, all of them `type: development` —
35
+ pure skill bundles that guide agent behaviour,
36
+ plus the handful of dev dependencies those skills assume:
37
+
38
+ - **coding** — development conventions, code style, commit and debugging skills
39
+ - **testing** — Jest setup, test utilities, the TDD-Lite workflow
40
+ - **project-mgmt** — beads issue tracking and planning skills
41
+ - **product-design** — feature requirements, engineering design, documentation skills
42
+ - **skills** — skill authoring, discovery and maintenance
43
+ - **workflows** — the end-to-end development and writing workflows
44
+
45
+ No web layer, no API layer, no service layer, no database.
46
+ Add any of those with `boop add` if the project grows an application domain.
47
+
48
+ ## Two Deliberate Omissions
49
+
50
+ ### No kernel
51
+
52
+ `web-app` carries a `kernel` block: a node runtime, a Heroku deploy target,
53
+ port 3000, an Express dependency set and the scripts that start a server.
54
+ Every field in it exists to parameterise the scaffolding of a running web process.
55
+
56
+ This profile scaffolds no application,
57
+ so it has no runtime to name, no port to listen on and no deploy target.
58
+ Filling those in with plausible-looking values
59
+ would be inventing facts the profile does not have,
60
+ and it would make `boop init --profile development`
61
+ emit an Express `src/app.ts` that imports a dependency the profile never installs.
62
+
63
+ So `kernel` is absent, and `ProfileMetadata.kernel` is optional to allow it.
64
+ The consequence is deliberate: `boop init --profile development` is refused
65
+ with a message pointing at the two ways this profile is actually meant to be used —
66
+ name it in an existing project's `.boop/config.json`,
67
+ or `boop add` these capabilities one at a time.
68
+ The capabilities carry their own dependencies (`jest`, `ts-jest`, `beads`),
69
+ so nothing is lost by having no kernel to merge them from.
70
+
71
+ ### No reference/
72
+
73
+ `web-app` ships `reference/architecture.ref.md` and `reference/message-queue-patterns.ref.md`,
74
+ because it has an architecture to describe.
75
+ This profile has none — the reference pages it would want
76
+ already ship with the capabilities themselves.
77
+ There is no `profiles/development/reference/` directory rather than an empty one,
78
+ because the installed-layout generator walks every profile directory
79
+ and an empty directory is a promise of content that never arrives.
@@ -25,16 +25,16 @@ kernel:
25
25
  nodeVersion: ">=24.0.0"
26
26
  dependencies: []
27
27
  devDependencies:
28
- - typescript
29
- - tsx
30
- - nodemon
31
- - "@types/express"
32
- - "@types/node"
28
+ - typescript@^5
29
+ - tsx@^4
30
+ - nodemon@^3
31
+ - "@types/express@^5"
32
+ - "@types/node@^25"
33
33
  scripts:
34
34
  start: "tsx src/server.ts"
35
35
  dev: "nodemon --exec tsx src/server.ts"
36
36
  build: "tsc"
37
- test: "jest --runInBand"
37
+ test: "jest"
38
38
  ---
39
39
 
40
40
  # Web App Profile