@boopkit/cli 0.1.1 → 0.1.3

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,17 @@
1
+ {
2
+ "@boopkit/cap-api": "0.1.0",
3
+ "@boopkit/cap-cloud-storage": "0.1.0",
4
+ "@boopkit/cap-coding": "0.1.0",
5
+ "@boopkit/cap-database": "0.1.0",
6
+ "@boopkit/cap-email": "0.1.0",
7
+ "@boopkit/cap-hello": "0.1.0",
8
+ "@boopkit/cap-noticeboard": "0.1.0",
9
+ "@boopkit/cap-product-design": "0.1.0",
10
+ "@boopkit/cap-project-mgmt": "0.1.0",
11
+ "@boopkit/cap-services": "0.1.0",
12
+ "@boopkit/cap-skills": "0.1.0",
13
+ "@boopkit/cap-testing": "0.1.0",
14
+ "@boopkit/cap-users": "0.1.0",
15
+ "@boopkit/cap-web": "0.1.0",
16
+ "@boopkit/logger": "0.1.0"
17
+ }
@@ -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,11 @@ 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
+ * Fallback chain:
49
+ * 1. Monorepo packages/ dir (development)
50
+ * 2. require.resolve from CLI's node_modules
51
+ * 3. dist/capability-versions.json (baked at build time for npm installs)
52
+ * 4. '*' (last resort)
49
53
  */
50
54
  export declare function getBoopkitVersion(packageName: string): string;
51
55
  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,47 @@ 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
+ * Fallback chain:
280
+ * 1. Monorepo packages/ dir (development)
281
+ * 2. require.resolve from CLI's node_modules
282
+ * 3. dist/capability-versions.json (baked at build time for npm installs)
283
+ * 4. '*' (last resort)
280
284
  */
281
285
  function getBoopkitVersion(packageName) {
282
286
  const pkgName = packageName.replace(/^@boopkit\//, '');
283
- const pkgJsonPath = path.join((0, config_1.getBoopRoot)(), 'packages', pkgName, 'package.json');
287
+ // Try monorepo packages/ directory first (development)
288
+ const monorepoPkgJsonPath = path.join((0, config_1.getBoopRoot)(), 'packages', pkgName, 'package.json');
284
289
  try {
285
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
290
+ const pkg = JSON.parse(fs.readFileSync(monorepoPkgJsonPath, 'utf-8'));
286
291
  return `^${pkg.version}`;
287
292
  }
288
293
  catch {
289
- return '*';
294
+ // Not in monorepo — try require.resolve (published CLI install)
290
295
  }
296
+ // Try resolving the installed package
297
+ try {
298
+ const resolvedPath = require.resolve(`${packageName}/package.json`, {
299
+ paths: [(0, config_1.getBoopRoot)()],
300
+ });
301
+ const pkg = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
302
+ return `^${pkg.version}`;
303
+ }
304
+ catch {
305
+ // Not found via require.resolve
306
+ }
307
+ // Try build-time version manifest (baked into dist/ at publish time)
308
+ try {
309
+ const manifestPath = path.join((0, config_1.getBoopRoot)(), 'dist', 'capability-versions.json');
310
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
311
+ const fullName = packageName.startsWith('@') ? packageName : `@boopkit/${pkgName}`;
312
+ if (manifest[fullName]) {
313
+ return `^${manifest[fullName]}`;
314
+ }
315
+ }
316
+ catch {
317
+ // No manifest available
318
+ }
319
+ return '*';
291
320
  }
292
321
  function parseDep(dep) {
293
322
  // 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.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI tool to scaffold new projects from boop profiles and capabilities",
5
5
  "main": "dist/cli.js",
6
6
  "workspaces": [
@@ -18,7 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "scripts": {
21
- "build": "tsc",
21
+ "build": "tsc && node tools/generate-version-manifest.js",
22
22
  "dev": "tsx src/cli.ts",
23
23
  "start": "node dist/cli.js",
24
24
  "test": "jest"