@boopkit/cli 0.1.0 → 0.1.2

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.
@@ -85,11 +85,19 @@ function runInit(name, options) {
85
85
  }
86
86
  }
87
87
  // --- Pre-flight: detect script conflicts before writing anything ---
88
+ // When running from npm (not the monorepo), capabilities can't be resolved
89
+ // until after npm install. Defer conflict check to after install in that case.
88
90
  const capabilityNames = profile.defaultCapabilities;
89
91
  const preflightScripts = {};
92
+ let preflight = true;
90
93
  for (const capName of capabilityNames) {
91
94
  const cap = (0, capability_1.getCapability)(capName);
92
- if (!cap?.scripts)
95
+ if (!cap) {
96
+ // Can't resolve capabilities yet — will install first, then check
97
+ preflight = false;
98
+ break;
99
+ }
100
+ if (!cap.scripts)
93
101
  continue;
94
102
  for (const scriptName of Object.keys(cap.scripts)) {
95
103
  if (scriptName in preflightScripts) {
@@ -102,15 +110,19 @@ function runInit(name, options) {
102
110
  }
103
111
  // --- Create directory ---
104
112
  fs.mkdirSync(targetDir, { recursive: true });
105
- // --- Collect hooks from capabilities ---
106
- const hookEntries = [];
107
- for (const capName of capabilityNames) {
108
- const cap = (0, capability_1.getCapability)(capName);
109
- if (cap?.hooks) {
110
- hookEntries.push({ name: capName, hooks: cap.hooks });
113
+ // --- Collect hooks from capabilities (deferred if not resolvable yet) ---
114
+ let hookVars = (0, scaffold_1.collectHooks)([]);
115
+ const canResolveLocally = capabilityNames.every(n => (0, capability_1.getCapability)(n) !== null);
116
+ if (canResolveLocally) {
117
+ const hookEntries = [];
118
+ for (const capName of capabilityNames) {
119
+ const cap = (0, capability_1.getCapability)(capName);
120
+ if (cap?.hooks) {
121
+ hookEntries.push({ name: capName, hooks: cap.hooks });
122
+ }
111
123
  }
124
+ hookVars = (0, scaffold_1.collectHooks)(hookEntries);
112
125
  }
113
- const hookVars = (0, scaffold_1.collectHooks)(hookEntries);
114
126
  // --- Scaffold kernel ---
115
127
  const variables = {
116
128
  projectName: name,
@@ -130,6 +142,65 @@ function runInit(name, options) {
130
142
  capabilities: [],
131
143
  createdAt: new Date().toISOString(),
132
144
  });
145
+ // --- Add capability package deps and install before resolving ---
146
+ // When running from npm (not the monorepo), capability packages aren't in
147
+ // the CLI's node_modules. We add them to the project's package.json first,
148
+ // run npm install, then resolve from the project's node_modules.
149
+ const capDeps = {};
150
+ for (const capName of capabilityNames) {
151
+ const pkgName = `@boopkit/cap-${capName}`;
152
+ capDeps[pkgName] = (0, scaffold_1.getBoopkitVersion)(pkgName);
153
+ }
154
+ (0, scaffold_1.mergePackageJson)(targetDir, undefined, undefined, capDeps, { skipBackup: true });
155
+ // Run npm install early so capability packages are available for resolution
156
+ if (shouldInstall) {
157
+ try {
158
+ console.log('\nInstalling dependencies...');
159
+ (0, child_process_1.execSync)('npm install', { cwd: targetDir, stdio: 'inherit' });
160
+ }
161
+ catch {
162
+ console.error('Warning: npm install failed. You can run it manually later.');
163
+ }
164
+ }
165
+ // Search paths for capability resolution: project dir first, then CLI root
166
+ const capSearchPaths = [targetDir];
167
+ // If hooks couldn't be collected before kernel scaffold (remote install),
168
+ // collect them now and re-scaffold kernel templates that use hook variables.
169
+ if (!canResolveLocally) {
170
+ const hookEntries = [];
171
+ for (const capName of capabilityNames) {
172
+ const cap = (0, capability_1.getCapability)(capName, capSearchPaths);
173
+ if (cap?.hooks) {
174
+ hookEntries.push({ name: capName, hooks: cap.hooks });
175
+ }
176
+ }
177
+ hookVars = (0, scaffold_1.collectHooks)(hookEntries);
178
+ // Re-scaffold kernel with correct hook variables
179
+ variables.hookImports = hookVars.hookImports;
180
+ variables.hookInit = hookVars.hookInit;
181
+ variables.hookPreRoutes = hookVars.hookPreRoutes;
182
+ variables.hookPostRoutes = hookVars.hookPostRoutes;
183
+ (0, scaffold_1.scaffoldKernel)(targetDir, variables, true);
184
+ }
185
+ // Post-install conflict check (when preflight was deferred)
186
+ if (!preflight) {
187
+ const postScripts = {};
188
+ for (const capName of capabilityNames) {
189
+ const cap = (0, capability_1.getCapability)(capName, capSearchPaths);
190
+ if (!cap?.scripts)
191
+ continue;
192
+ for (const scriptName of Object.keys(cap.scripts)) {
193
+ if (scriptName in postScripts) {
194
+ console.error(`Error: Script "${scriptName}" conflicts between capabilities "${postScripts[scriptName]}" and "${capName}".`);
195
+ // Already created the directory, but the project is incomplete — warn user
196
+ console.error('The project directory has been partially created and may need cleanup.');
197
+ process.exitCode = 1;
198
+ return;
199
+ }
200
+ postScripts[scriptName] = capName;
201
+ }
202
+ }
203
+ }
133
204
  // --- Resolve and install capabilities ---
134
205
  const installedCapabilities = [];
135
206
  const allScripts = {};
@@ -137,13 +208,13 @@ function runInit(name, options) {
137
208
  const capabilityFiles = [];
138
209
  const cssCapMap = {};
139
210
  for (const capName of capabilityNames) {
140
- const cap = (0, capability_1.getCapability)(capName);
211
+ const cap = (0, capability_1.getCapability)(capName, capSearchPaths);
141
212
  if (!cap) {
142
213
  console.error(`Warning: Capability "${capName}" not found, skipping.`);
143
214
  continue;
144
215
  }
145
216
  // Copy capability files (skills, scaffolded files, etc.)
146
- const copied = (0, scaffold_1.copyCapabilityFiles)(targetDir, capName);
217
+ const copied = (0, scaffold_1.copyCapabilityFiles)(targetDir, capName, capSearchPaths);
147
218
  capabilityFiles.push(...copied);
148
219
  // Track CSS partials for annotation
149
220
  for (const f of copied) {
@@ -194,7 +265,7 @@ function runInit(name, options) {
194
265
  const capabilityConfig = {};
195
266
  const envEntries = [];
196
267
  for (const capName of installedCapabilities) {
197
- const cap = (0, capability_1.getCapability)(capName);
268
+ const cap = (0, capability_1.getCapability)(capName, capSearchPaths);
198
269
  if (cap?.env && cap.env.length > 0) {
199
270
  const envConfig = {};
200
271
  for (const e of cap.env) {
@@ -239,7 +310,7 @@ function runInit(name, options) {
239
310
  const typeOrder = ['application', 'development', 'operations', 'demo'];
240
311
  const grouped = new Map();
241
312
  for (const capName of installedCapabilities) {
242
- const cap = (0, capability_1.getCapability)(capName);
313
+ const cap = (0, capability_1.getCapability)(capName, capSearchPaths);
243
314
  const key = cap?.type ?? 'other';
244
315
  if (!grouped.has(key))
245
316
  grouped.set(key, []);
@@ -253,15 +324,8 @@ function runInit(name, options) {
253
324
  const label = typeLabels[type] ?? type;
254
325
  console.log(` ${label}: ${caps.join(', ')}`);
255
326
  }
256
- // --- npm install (after summary so output is clear) ---
327
+ // --- Post-install setup (CSS build, Prisma generate) ---
257
328
  if (shouldInstall) {
258
- try {
259
- console.log('\nInstalling dependencies...');
260
- (0, child_process_1.execSync)('npm install', { cwd: targetDir, stdio: 'inherit' });
261
- }
262
- catch {
263
- console.error('Warning: npm install failed. You can run it manually later.');
264
- }
265
329
  // Build CSS if tailwind:build script exists
266
330
  const pkgPath = path.join(targetDir, 'package.json');
267
331
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
@@ -284,6 +348,13 @@ function runInit(name, options) {
284
348
  console.error('Warning: Prisma generate failed. Run `npm run db:generate` manually.');
285
349
  }
286
350
  }
351
+ // Re-run npm install to pick up any new deps added by capability scaffolding
352
+ try {
353
+ (0, child_process_1.execSync)('npm install', { cwd: targetDir, stdio: 'inherit' });
354
+ }
355
+ catch {
356
+ // Already warned earlier if npm install has issues
357
+ }
287
358
  }
288
359
  console.log(`\nNext steps:`);
289
360
  console.log(` cd ${name}`);
@@ -1,9 +1,10 @@
1
1
  import { CapabilityMetadata } from './config';
2
2
  /**
3
3
  * Resolve the directory of an installed @boopkit/cap-{name} package.
4
+ * Searches in the provided paths first, then falls back to boopRoot.
4
5
  * Returns null if not found.
5
6
  */
6
- export declare function getPackageDir(name: string): string | null;
7
+ export declare function getPackageDir(name: string, searchPaths?: string[]): string | null;
7
8
  /**
8
9
  * List all capabilities by scanning package directory and capabilities/ source tree (test fixtures only).
9
10
  * Package resolution takes priority. Returns metadata for each valid capability.
@@ -12,6 +13,7 @@ export declare function listCapabilities(): CapabilityMetadata[];
12
13
  /**
13
14
  * Get a single capability's metadata by name.
14
15
  * Tries package resolution first, falls back to source tree.
16
+ * Pass searchPaths to resolve from a project's node_modules (e.g. during init).
15
17
  * Returns null if not found in either location.
16
18
  */
17
- export declare function getCapability(name: string): CapabilityMetadata | null;
19
+ export declare function getCapability(name: string, searchPaths?: string[]): CapabilityMetadata | null;
@@ -41,29 +41,31 @@ const path = __importStar(require("path"));
41
41
  const config_1 = require("./config");
42
42
  /**
43
43
  * Resolve the directory of an installed @boopkit/cap-{name} package.
44
+ * Searches in the provided paths first, then falls back to boopRoot.
44
45
  * Returns null if not found.
45
46
  */
46
- function getPackageDir(name) {
47
+ function getPackageDir(name, searchPaths) {
47
48
  const pkgName = `@boopkit/cap-${name}`;
49
+ const paths = [...(searchPaths ?? []), (0, config_1.getBoopRoot)()];
48
50
  try {
49
51
  const pkgJsonPath = require.resolve(`${pkgName}/package.json`, {
50
- paths: [(0, config_1.getBoopRoot)()],
52
+ paths,
51
53
  });
52
54
  return path.dirname(pkgJsonPath);
53
55
  }
54
56
  catch {
55
57
  // Fall back to scanning for a package whose capability.json has this name
56
- return findPackageDirByCapabilityName(name);
58
+ return findPackageDirByCapabilityName(name, paths);
57
59
  }
58
60
  }
59
61
  /**
60
62
  * Read capability metadata from a package's capability.json.
61
63
  * Returns null if the package is not found or has no capability.json.
62
64
  */
63
- function getCapabilityFromPackage(name) {
65
+ function getCapabilityFromPackage(name, searchPaths) {
64
66
  // getPackageDir already falls back to findPackageDirByCapabilityName on
65
67
  // require.resolve failure, so no secondary scan needed here.
66
- const pkgDir = getPackageDir(name);
68
+ const pkgDir = getPackageDir(name, searchPaths);
67
69
  if (!pkgDir)
68
70
  return null;
69
71
  const capJsonPath = path.join(pkgDir, 'capability.json');
@@ -84,25 +86,27 @@ function getCapabilityFromPackage(name) {
84
86
  * has the given capability name. Handles mismatches between capability name
85
87
  * and package name (e.g. "users-and-accounts" → @boopkit/cap-users).
86
88
  */
87
- function findPackageDirByCapabilityName(name) {
88
- const nodeModulesBoopkit = path.join((0, config_1.getBoopRoot)(), 'node_modules', '@boopkit');
89
- if (!fs.existsSync(nodeModulesBoopkit))
90
- return null;
91
- const entries = fs.readdirSync(nodeModulesBoopkit, { withFileTypes: true });
92
- for (const entry of entries) {
93
- if ((!entry.isDirectory() && !entry.isSymbolicLink()) || !entry.name.startsWith('cap-'))
94
- continue;
95
- const dir = path.join(nodeModulesBoopkit, entry.name);
96
- const capJsonPath = path.join(dir, 'capability.json');
97
- if (!fs.existsSync(capJsonPath))
98
- continue;
99
- try {
100
- const capJson = JSON.parse(fs.readFileSync(capJsonPath, 'utf-8'));
101
- if (capJson.name === name)
102
- return dir;
103
- }
104
- catch {
89
+ function findPackageDirByCapabilityName(name, searchPaths) {
90
+ for (const basePath of searchPaths) {
91
+ const nodeModulesBoopkit = path.join(basePath, 'node_modules', '@boopkit');
92
+ if (!fs.existsSync(nodeModulesBoopkit))
105
93
  continue;
94
+ const entries = fs.readdirSync(nodeModulesBoopkit, { withFileTypes: true });
95
+ for (const entry of entries) {
96
+ if ((!entry.isDirectory() && !entry.isSymbolicLink()) || !entry.name.startsWith('cap-'))
97
+ continue;
98
+ const dir = path.join(nodeModulesBoopkit, entry.name);
99
+ const capJsonPath = path.join(dir, 'capability.json');
100
+ if (!fs.existsSync(capJsonPath))
101
+ continue;
102
+ try {
103
+ const capJson = JSON.parse(fs.readFileSync(capJsonPath, 'utf-8'));
104
+ if (capJson.name === name)
105
+ return dir;
106
+ }
107
+ catch {
108
+ continue;
109
+ }
106
110
  }
107
111
  }
108
112
  return null;
@@ -149,11 +153,12 @@ function listCapabilities() {
149
153
  /**
150
154
  * Get a single capability's metadata by name.
151
155
  * Tries package resolution first, falls back to source tree.
156
+ * Pass searchPaths to resolve from a project's node_modules (e.g. during init).
152
157
  * Returns null if not found in either location.
153
158
  */
154
- function getCapability(name) {
159
+ function getCapability(name, searchPaths) {
155
160
  // Try package first
156
- const fromPackage = getCapabilityFromPackage(name);
161
+ const fromPackage = getCapabilityFromPackage(name, searchPaths);
157
162
  if (fromPackage)
158
163
  return fromPackage;
159
164
  // Fall back to source tree
@@ -35,7 +35,7 @@ export declare function hashFile(filePath: string): string;
35
35
  * Tries package resolution first (scaffold/ and skills/ dirs), falls back to source tree.
36
36
  * Returns list of copied file paths relative to targetDir, and file hashes.
37
37
  */
38
- export declare function copyCapabilityFiles(targetDir: string, capabilityName: string): string[];
38
+ export declare function copyCapabilityFiles(targetDir: string, capabilityName: string, searchPaths?: string[]): string[];
39
39
  /**
40
40
  * Merge dependencies into an existing package.json.
41
41
  * Does not overwrite existing dependency versions.
@@ -45,7 +45,9 @@ export declare function copyCapabilityFiles(targetDir: string, capabilityName: s
45
45
  */
46
46
  /**
47
47
  * Read the version from a @boopkit/ package's package.json and return ^version.
48
- * Falls back to '*' if the package.json cannot be read.
48
+ * First tries the monorepo packages/ dir, then falls back to resolving the
49
+ * installed package via require.resolve (for published CLI installs).
50
+ * Falls back to '*' if neither location works.
49
51
  */
50
52
  export declare function getBoopkitVersion(packageName: string): string;
51
53
  export declare function mergePackageJson(targetDir: string, dependencies?: string[], devDependencies?: string[], packages?: Record<string, string>, options?: {
@@ -125,8 +125,8 @@ function hashFile(filePath) {
125
125
  * Tries package resolution first (scaffold/ and skills/ dirs), falls back to source tree.
126
126
  * Returns list of copied file paths relative to targetDir, and file hashes.
127
127
  */
128
- function copyCapabilityFiles(targetDir, capabilityName) {
129
- const pkgDir = (0, capability_1.getPackageDir)(capabilityName);
128
+ function copyCapabilityFiles(targetDir, capabilityName, searchPaths) {
129
+ const pkgDir = (0, capability_1.getPackageDir)(capabilityName, searchPaths);
130
130
  if (pkgDir) {
131
131
  // Read package.json for boopkit config
132
132
  const pkgJsonPath = path.join(pkgDir, 'package.json');
@@ -276,18 +276,43 @@ function scaffoldCapabilityFiles(targetDir, filesDir) {
276
276
  */
277
277
  /**
278
278
  * Read the version from a @boopkit/ package's package.json and return ^version.
279
- * Falls back to '*' if the package.json cannot be read.
279
+ * First tries the monorepo packages/ dir, then falls back to resolving the
280
+ * installed package via require.resolve (for published CLI installs).
281
+ * Falls back to '*' if neither location works.
280
282
  */
281
283
  function getBoopkitVersion(packageName) {
282
284
  const pkgName = packageName.replace(/^@boopkit\//, '');
283
- const pkgJsonPath = path.join((0, config_1.getBoopRoot)(), 'packages', pkgName, 'package.json');
285
+ // Try monorepo packages/ directory first (development)
286
+ const monorepoPkgJsonPath = path.join((0, config_1.getBoopRoot)(), 'packages', pkgName, 'package.json');
284
287
  try {
285
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
288
+ const pkg = JSON.parse(fs.readFileSync(monorepoPkgJsonPath, 'utf-8'));
286
289
  return `^${pkg.version}`;
287
290
  }
288
291
  catch {
289
- return '*';
292
+ // Not in monorepo — try require.resolve (published CLI install)
290
293
  }
294
+ // Try resolving the installed package
295
+ try {
296
+ const resolvedPath = require.resolve(`${packageName}/package.json`, {
297
+ paths: [(0, config_1.getBoopRoot)()],
298
+ });
299
+ const pkg = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
300
+ return `^${pkg.version}`;
301
+ }
302
+ catch {
303
+ // Not found — fall back to CLI's own version as a reasonable default
304
+ }
305
+ // Use CLI's own version as fallback (all @boopkit packages versioned in same range)
306
+ try {
307
+ const cliPkgJsonPath = path.join((0, config_1.getBoopRoot)(), 'package.json');
308
+ const cliPkg = JSON.parse(fs.readFileSync(cliPkgJsonPath, 'utf-8'));
309
+ if (cliPkg.version)
310
+ return `^${cliPkg.version}`;
311
+ }
312
+ catch {
313
+ // give up
314
+ }
315
+ return '*';
291
316
  }
292
317
  function parseDep(dep) {
293
318
  // Handle scoped packages: @scope/name@version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boopkit/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tool to scaffold new projects from boop profiles and capabilities",
5
5
  "main": "dist/cli.js",
6
6
  "workspaces": [
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "dist/",
14
- "profiles/"
14
+ "profiles/",
15
+ "src/templates/"
15
16
  ],
16
17
  "publishConfig": {
17
18
  "access": "public"
@@ -0,0 +1,3 @@
1
+ Read the [AGENTS.md](../AGENTS.md) file and follow those instructions.
2
+
3
+ Before writing any code, run `boop status` to see what capabilities are configured and what needs setup.
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ *.log
5
+ .DS_Store
6
+ .boop/*
7
+ !.boop/config.json
@@ -0,0 +1,28 @@
1
+ # {{projectName}}
2
+
3
+ This is a **boop**-managed project. Use boop commands — don't reinvent what boop already provides.
4
+
5
+ ## Getting Started
6
+
7
+ 1. Run `boop status` to see what needs configuration
8
+ 2. Follow the setup hints to configure your environment
9
+ 3. Run `npm run dev` to start the development server
10
+
11
+ ## How to work in this project
12
+
13
+ 1. **Check skills first.** Before coding, reviewing, testing, or designing, check [skills/index.md](skills/index.md) for a matching skill and follow its process. Do not rely on general knowledge when a project skill exists.
14
+ 2. **Use boop for capabilities.** To add features like database, auth, or other infrastructure, run `boop add <capability>` — don't install or wire things manually. Run `boop caps` to see what's available.
15
+ 3. **Check project status.** Run `boop status` to see what's configured and what needs attention.
16
+ 4. **Run `boop --help`** for the full command list.
17
+
18
+ ## Boop extension model
19
+
20
+ Read [reference/boop-guide.ref.md](reference/boop-guide.ref.md) to understand how this project's services, controllers, and styles are structured — service tiers, factory patterns, the adoption curve (use → compose → adopt), and CSS extension.
21
+
22
+ ## Project layout
23
+ - `src/` — source code
24
+ - `skills/` — process skills for agents (read `skills/index.md`)
25
+ - `reference/` — architecture and coding standards ("what exists now")
26
+ - `specs/` — design docs and specifications ("future state")
27
+ - `tests/` — `npm test` (Jest)
28
+ - `.boop/` — project manifest (managed by boop)
@@ -0,0 +1,3 @@
1
+ # {{projectName}}
2
+
3
+ Read [AGENTS.md](AGENTS.md) and follow all instructions there.
@@ -0,0 +1 @@
1
+ web: npm start
@@ -0,0 +1,17 @@
1
+ # {{projectName}}
2
+
3
+ A project scaffolded with [boop](https://github.com/boop).
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ ## Scripts
13
+
14
+ - `npm start` — Start the application
15
+ - `npm run dev` — Start in development mode
16
+ - `npm run build` — Build the project
17
+ - `npm test` — Run tests
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "engines": {
6
+ "node": "{{nodeVersion}}"
7
+ },
8
+ "scripts": {
9
+ "start": "{{scripts.start}}",
10
+ "dev": "{{scripts.dev}}",
11
+ "build": "{{scripts.build}}",
12
+ "test": "{{scripts.test}}"
13
+ },
14
+ "dependencies": {},
15
+ "devDependencies": {}
16
+ }
@@ -0,0 +1,160 @@
1
+ # Busy Agent's Guide to Boop
2
+
3
+ Quick orientation for AI agents working in a boop-generated project. Read this before modifying service code, controller logic, or styles.
4
+
5
+ **Deep dive**: If this project has `reference/application-services.ref.md`, it covers service tiers, factory internals, and controller patterns in full detail.
6
+
7
+ ---
8
+
9
+ ## How Boop Projects Work
10
+
11
+ Boop projects are built from **capabilities** — each one adds skills, npm packages, scaffolded files, or all three. The developer owns scaffolded files from day one. Packages (`@boopkit/*`) are managed dependencies that can be updated via npm — or adopted into the project when the developer outgrows them.
12
+
13
+ Key commands:
14
+ - `boop caps` — list available capabilities
15
+ - `boop add <capability>` — add a capability to the project
16
+ - `boop adopt <capability>` — copy package source into the project and take ownership
17
+ - `boop status` — check project configuration and what needs attention
18
+
19
+ ---
20
+
21
+ ## Service Tiers
22
+
23
+ Application logic is organized into four tiers. Dependency flows downward only.
24
+
25
+ ```
26
+ Developer code ← project-owned, fully custom
27
+ → Business services ← multi-step orchestration (e.g. postNotice)
28
+ → Data services ← typed CRUD + lifecycle events (e.g. createNoticeService)
29
+ → Entity primitives ← createEntityService, createPropertyService (@boopkit/cap-services)
30
+ → Primitives ← middleware, DB helpers, validators
31
+ ```
32
+
33
+ | Tier | In package? | npm updatable? | Typical relationship |
34
+ |------|-------------|----------------|---------------------|
35
+ | Primitives | Yes | Yes | Stable utilities — rarely replaced |
36
+ | Data services | Yes | Yes | Long-lived composable building blocks |
37
+ | Business services | Yes | Yes | Use until you outgrow, then adopt |
38
+ | Developer code | No | N/A | Fully owned from the start |
39
+
40
+ ---
41
+
42
+ ## Factory Pattern
43
+
44
+ Boop services export **factory functions** that return a bag of typed methods — not classes, not singletons.
45
+
46
+ ```typescript
47
+ const noticeService = createNoticeService<MyNoticeInfo>(store);
48
+ const { create, findById, list } = noticeService; // destructuring is safe
49
+ ```
50
+
51
+ **Generic type params** let you extend the base entity shape. Set the type once at factory creation; type safety flows to every returned method:
52
+
53
+ ```typescript
54
+ interface MyNoticeInfo extends BaseNoticeInfo {
55
+ priority: 'low' | 'medium' | 'high';
56
+ tags: string[];
57
+ }
58
+ const noticeService = createNoticeService<MyNoticeInfo>(store);
59
+ // All methods are now typed with MyNoticeInfo
60
+ ```
61
+
62
+ **Lifecycle events** are optional callbacks passed to the factory. When provided, they fire on mutations. When omitted, they're no-ops:
63
+
64
+ ```typescript
65
+ const noticeService = createNoticeService<MyNoticeInfo>(store, {
66
+ onCreated: async (notice) => { /* publish, log, index, etc. */ },
67
+ });
68
+ ```
69
+
70
+ ---
71
+
72
+ ## The Adoption Curve: Use → Compose → Adopt
73
+
74
+ Every boop service supports three levels of engagement. Move between them as needs evolve.
75
+
76
+ ### 1. Use the box
77
+ Call the high-level method. Accept the defaults.
78
+ ```typescript
79
+ const notice = await noticeBoardService.postNotice(input);
80
+ ```
81
+
82
+ ### 2. Compose the steps
83
+ Business services expose their sub-operations as public methods. Orchestrate them yourself, inserting custom logic between steps:
84
+ ```typescript
85
+ noticeBoardService.validateContent(input);
86
+ const notice = await noticeBoardService.createNotice(input);
87
+ await myCustomApprovalWorkflow(notice); // your step
88
+ await noticeBoardService.notifySubscribers(notice);
89
+ ```
90
+
91
+ **How to find decomposed operations**: Look at the convenience method's implementation — it calls the individual steps in sequence. Each step is a public method on the same service. JSDoc on each method explains what it does.
92
+
93
+ ### 3. Adopt
94
+ Copy the package source into the project and own it:
95
+ ```bash
96
+ boop adopt noticeboard
97
+ ```
98
+
99
+ This copies `@boopkit/cap-noticeboard` source into `src/adopted/cap-noticeboard/`, rewrites all imports, removes the package dependency, and updates the manifest. From here, the developer maintains the code directly.
100
+
101
+ > **"Adopt" not "eject."** Adoption implies natural maturation — taking ownership of code that served you well.
102
+
103
+ ---
104
+
105
+ ## Controller Handlers
106
+
107
+ Packages export **individual route handlers** as named functions, plus an optional **router factory**:
108
+
109
+ **Use the box** — mount the whole router:
110
+ ```typescript
111
+ app.use('/noticeboard', createNoticeBoardRouter({ noticeBoardService }));
112
+ ```
113
+
114
+ **Compose the steps** — pick individual handlers, add custom middleware:
115
+ ```typescript
116
+ const handlers = createNoticeBoardHandlers({ noticeBoardService });
117
+ app.get('/noticeboard', handlers.listNotices);
118
+ app.post('/noticeboard', rateLimit(5), handlers.postNotice);
119
+ app.delete('/noticeboard/:id', requireAdmin, handlers.deleteNotice);
120
+ ```
121
+
122
+ ---
123
+
124
+ ## CSS Extension
125
+
126
+ The project's `styles/styles.css` imports from packages in a layered cascade:
127
+
128
+ ```css
129
+ @import "tailwindcss"; /* 1. Framework */
130
+ @import "@boopkit/cap-web/styles/theme.css"; /* 2. Design tokens */
131
+ @import "@boopkit/cap-web/styles/base.css"; /* 3. Base styles */
132
+ @import "@boopkit/cap-noticeboard/styles/..."; /* 4. Capability styles */
133
+
134
+ @theme { --color-brand-600: #059669; } /* 5. Project overrides */
135
+ .btn-primary { @apply shadow-lg; } /* 6. Component overrides */
136
+ ```
137
+
138
+ **Design tokens** (`@theme`) generate Tailwind utilities automatically. Override a token → everything downstream updates.
139
+
140
+ **Package CSS is npm-updatable.** Boop ships new components that pick up the project's brand colors via tokens. Developers override any class by redeclaring it later in the file — CSS cascade handles precedence.
141
+
142
+ **The same adoption curve applies**: use the imports → override specific classes → drop the import and own the styles.
143
+
144
+ ---
145
+
146
+ ## Project Structure at a Glance
147
+
148
+ | Directory | Owned by | Purpose |
149
+ |-----------|----------|---------|
150
+ | `src/services/` | Developer | Service wiring files (bridge factories to ServiceMap) |
151
+ | `src/routes/` | Developer | Route mounting (thin glue over handler sets) |
152
+ | `src/controllers/` | Developer | Project-specific controllers |
153
+ | `src/adopted/` | Developer | Adopted package source (created by `boop adopt`) |
154
+ | `styles/` | Developer | Project CSS (imports from packages + overrides) |
155
+ | `styles/adopted/` | Developer | Adopted package styles |
156
+ | `views/` | Developer | Handlebars templates (layouts, pages, partials) |
157
+ | `node_modules/@boopkit/*` | Package | Managed dependencies (updatable via npm) |
158
+ | `skills/` | Scaffolded | Agent skills (process guidance) |
159
+ | `reference/` | Scaffolded | Architecture docs |
160
+ | `.boop/config.json` | Boop | Project manifest (capabilities, adopted packages) |
@@ -0,0 +1,12 @@
1
+ # Specs
2
+
3
+ Design documents and specifications for planned features and changes. Specs describe the **future state** — what we intend to build — in contrast to `reference/` which documents the **current state**.
4
+
5
+ ## Workflow
6
+
7
+ 1. Write a spec for a new feature or significant change
8
+ 2. Review and approve the spec
9
+ 3. Create implementation tasks (in beads) from the approved spec
10
+ 4. After shipping, update `reference/` docs to reflect the new reality
11
+
12
+ Specs remain as a record of design intent even after implementation.
@@ -0,0 +1,23 @@
1
+ // @boopkit:env
2
+ import express from 'express';
3
+ {{{hookImports}}}
4
+
5
+ const app = express();
6
+ const PORT = process.env.PORT || {{port}};
7
+ app.locals.projectName = '{{projectName}}';
8
+
9
+ app.use(express.json());
10
+ app.use(express.urlencoded({ extended: true }));
11
+ {{{hookPreRoutes}}}
12
+
13
+ {{{hookPostRoutes}}}
14
+
15
+ export async function boot() {
16
+ {{{hookInit}}}
17
+
18
+ app.listen(PORT, () => {
19
+ console.log(`{{projectName}} listening on port ${PORT}`);
20
+ });
21
+ }
22
+
23
+ export default app;
File without changes
@@ -0,0 +1,3 @@
1
+ import { boot } from './app';
2
+ boot();
3
+
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "outDir": "dist",
6
+ "rootDir": "src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }