@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.
- package/dist/commands/init.js +91 -20
- package/dist/lib/capability.d.ts +4 -2
- package/dist/lib/capability.js +30 -25
- package/dist/lib/scaffold.d.ts +4 -2
- package/dist/lib/scaffold.js +31 -6
- package/package.json +3 -2
- package/src/templates/kernel/.github/copilot-instructions.md +3 -0
- package/src/templates/kernel/.gitignore.hbs +7 -0
- package/src/templates/kernel/AGENTS.md.hbs +28 -0
- package/src/templates/kernel/CLAUDE.md.hbs +3 -0
- package/src/templates/kernel/Procfile.hbs +1 -0
- package/src/templates/kernel/README.md.hbs +17 -0
- package/src/templates/kernel/package.json.hbs +16 -0
- package/src/templates/kernel/reference/boop-guide.ref.md +160 -0
- package/src/templates/kernel/specs/README.md +12 -0
- package/src/templates/kernel/src/app.ts.hbs +23 -0
- package/src/templates/kernel/src/lib/.gitkeep +0 -0
- package/src/templates/kernel/src/server.ts.hbs +3 -0
- package/src/templates/kernel/tsconfig.json.hbs +16 -0
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
// ---
|
|
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}`);
|
package/dist/lib/capability.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/capability.js
CHANGED
|
@@ -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
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
package/dist/lib/scaffold.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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?: {
|
package/dist/lib/scaffold.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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(
|
|
288
|
+
const pkg = JSON.parse(fs.readFileSync(monorepoPkgJsonPath, 'utf-8'));
|
|
286
289
|
return `^${pkg.version}`;
|
|
287
290
|
}
|
|
288
291
|
catch {
|
|
289
|
-
|
|
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.
|
|
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,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 @@
|
|
|
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,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
|
+
}
|