@boopkit/cli 0.3.0 → 0.3.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.
@@ -1,18 +1,18 @@
1
1
  {
2
- "@boopkit/cap-api": "0.3.0",
3
- "@boopkit/cap-background-jobs": "0.3.0",
4
- "@boopkit/cap-cloud-storage": "0.3.0",
5
- "@boopkit/cap-coding": "0.3.0",
6
- "@boopkit/cap-database": "0.3.0",
7
- "@boopkit/cap-email": "0.3.0",
8
- "@boopkit/cap-hello": "0.3.0",
9
- "@boopkit/cap-noticeboard": "0.3.0",
10
- "@boopkit/cap-product-design": "0.3.0",
11
- "@boopkit/cap-project-mgmt": "0.3.0",
12
- "@boopkit/cap-services": "0.3.0",
13
- "@boopkit/cap-skills": "0.3.0",
14
- "@boopkit/cap-testing": "0.3.0",
15
- "@boopkit/cap-users": "0.3.0",
16
- "@boopkit/cap-web": "0.3.0",
17
- "@boopkit/logger": "0.3.0"
2
+ "@boopkit/cap-api": "0.3.1",
3
+ "@boopkit/cap-background-jobs": "0.3.1",
4
+ "@boopkit/cap-cloud-storage": "0.3.1",
5
+ "@boopkit/cap-coding": "0.3.1",
6
+ "@boopkit/cap-database": "0.3.1",
7
+ "@boopkit/cap-email": "0.3.1",
8
+ "@boopkit/cap-hello": "0.3.1",
9
+ "@boopkit/cap-noticeboard": "0.3.1",
10
+ "@boopkit/cap-product-design": "0.3.1",
11
+ "@boopkit/cap-project-mgmt": "0.3.1",
12
+ "@boopkit/cap-services": "0.3.1",
13
+ "@boopkit/cap-skills": "0.3.1",
14
+ "@boopkit/cap-testing": "0.3.1",
15
+ "@boopkit/cap-users": "0.3.1",
16
+ "@boopkit/cap-web": "0.3.1",
17
+ "@boopkit/logger": "0.3.1"
18
18
  }
@@ -181,7 +181,9 @@ async function enrichWithSetupChecks(data, envValues, projectDir, projectName) {
181
181
  label: (0, interpolate_1.interpolate)(c.label, vars),
182
182
  hint: c.hint ? (0, interpolate_1.interpolate)(c.hint, vars) : undefined,
183
183
  }));
184
- const steps = await (0, db_status_1.runSetupChecks)(interpolatedChecks, envValues, projectDir, projectName);
184
+ // Extract this capability's .prisma files for scoped prisma-migrate checks
185
+ const prismaFiles = capDef?.files?.filter(f => f.endsWith('.prisma'));
186
+ const steps = await (0, db_status_1.runSetupChecks)(interpolatedChecks, envValues, projectDir, projectName, prismaFiles);
185
187
  const completedSteps = steps.filter(s => s.done).map(s => s.label);
186
188
  const nextSteps = steps.filter(s => !s.done).map(s => s.hint ? `${s.label}: ${s.hint}` : s.label);
187
189
  if (nextSteps.length === 0) {
@@ -26,6 +26,16 @@ export declare function checkMigrationsTable(dbUrl: string): Promise<boolean | n
26
26
  * Returns true if prisma/migrations/ contains at least one migration subdirectory.
27
27
  */
28
28
  export declare function hasPrismaMigrations(projectDir: string): boolean;
29
+ /**
30
+ * Parse a .prisma file and extract table names from model declarations.
31
+ * Returns the @@map() value if present, otherwise the model name.
32
+ */
33
+ export declare function parsePrismaTableNames(filePath: string): string[];
34
+ /**
35
+ * Check if specific tables exist in the database.
36
+ * Returns true if ALL tables exist, false if any are missing, null if psql unavailable.
37
+ */
38
+ export declare function checkTablesExist(dbUrl: string, tableNames: string[]): Promise<boolean | null>;
29
39
  /**
30
40
  * Check if any .prisma files in the project define models.
31
41
  * Returns true if at least one file contains a `model` declaration.
@@ -52,6 +62,8 @@ export interface CheckContext {
52
62
  projectName: string;
53
63
  /** Accumulated results from earlier checks — keyed by check type. */
54
64
  results: Map<string, boolean>;
65
+ /** Capability's .prisma file paths relative to projectDir — enables scoped prisma-migrate checks. */
66
+ prismaFiles?: string[];
55
67
  }
56
68
  /**
57
69
  * Run all setup checks declared in a capability's setupChecks list.
@@ -61,4 +73,4 @@ export declare function runSetupChecks(checks: {
61
73
  type: string;
62
74
  label: string;
63
75
  hint?: string;
64
- }[], envValues: Map<string, string>, projectDir: string, projectName: string): Promise<SetupStep[]>;
76
+ }[], envValues: Map<string, string>, projectDir: string, projectName: string, prismaFiles?: string[]): Promise<SetupStep[]>;
@@ -38,6 +38,8 @@ exports.checkTcpReachable = checkTcpReachable;
38
38
  exports.checkDatabaseConnectable = checkDatabaseConnectable;
39
39
  exports.checkMigrationsTable = checkMigrationsTable;
40
40
  exports.hasPrismaMigrations = hasPrismaMigrations;
41
+ exports.parsePrismaTableNames = parsePrismaTableNames;
42
+ exports.checkTablesExist = checkTablesExist;
41
43
  exports.hasPrismaModels = hasPrismaModels;
42
44
  exports.isPrismaClientGenerated = isPrismaClientGenerated;
43
45
  exports.checkPrismaMigrateStatus = checkPrismaMigrateStatus;
@@ -140,6 +142,77 @@ function hasPrismaMigrations(projectDir) {
140
142
  const entries = fs.readdirSync(migrationsDir, { withFileTypes: true });
141
143
  return entries.some(e => e.isDirectory());
142
144
  }
145
+ /**
146
+ * Parse a .prisma file and extract table names from model declarations.
147
+ * Returns the @@map() value if present, otherwise the model name.
148
+ */
149
+ function parsePrismaTableNames(filePath) {
150
+ if (!fs.existsSync(filePath))
151
+ return [];
152
+ const content = fs.readFileSync(filePath, 'utf-8');
153
+ const tables = [];
154
+ const lines = content.split('\n');
155
+ let currentModel = null;
156
+ let braceDepth = 0;
157
+ let mapName = null;
158
+ for (const line of lines) {
159
+ const modelMatch = line.match(/^\s*model\s+(\w+)\s*\{/);
160
+ if (modelMatch && !currentModel) {
161
+ currentModel = modelMatch[1];
162
+ braceDepth = 1;
163
+ mapName = null;
164
+ continue;
165
+ }
166
+ if (currentModel) {
167
+ for (const ch of line) {
168
+ if (ch === '{')
169
+ braceDepth++;
170
+ if (ch === '}')
171
+ braceDepth--;
172
+ }
173
+ const mapMatch = line.match(/@@map\s*\(\s*"([^"]+)"\s*\)/);
174
+ if (mapMatch) {
175
+ mapName = mapMatch[1];
176
+ }
177
+ if (braceDepth === 0) {
178
+ tables.push(mapName || currentModel);
179
+ currentModel = null;
180
+ mapName = null;
181
+ }
182
+ }
183
+ }
184
+ return tables;
185
+ }
186
+ /**
187
+ * Check if specific tables exist in the database.
188
+ * Returns true if ALL tables exist, false if any are missing, null if psql unavailable.
189
+ */
190
+ function checkTablesExist(dbUrl, tableNames) {
191
+ if (tableNames.length === 0)
192
+ return Promise.resolve(true);
193
+ // Validate table names — only allow alphanumeric and underscores
194
+ if (!tableNames.every(t => /^[a-zA-Z_]\w*$/.test(t))) {
195
+ return Promise.resolve(false);
196
+ }
197
+ const quotedNames = tableNames.map(t => `'${t}'`).join(',');
198
+ const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN (${quotedNames})`;
199
+ return new Promise((resolve) => {
200
+ (0, child_process_1.execFile)('psql', [dbUrl, '-tAc', query], { timeout: 3000 }, (error, stdout) => {
201
+ if (error) {
202
+ if ('code' in error && error.code === 'ENOENT') {
203
+ resolve(null);
204
+ }
205
+ else {
206
+ resolve(false);
207
+ }
208
+ }
209
+ else {
210
+ const count = parseInt(stdout.trim(), 10);
211
+ resolve(count === tableNames.length);
212
+ }
213
+ });
214
+ });
215
+ }
143
216
  /**
144
217
  * Check if any .prisma files in the project define models.
145
218
  * Returns true if at least one file contains a `model` declaration.
@@ -216,7 +289,28 @@ async function runCheck(type, ctx) {
216
289
  // Skip only if a database check was explicitly run and failed
217
290
  if (ctx.results.has('database') && !ctx.results.get('database'))
218
291
  return false;
219
- // If schema has models but no migrations exist, migration hasn't been run
292
+ // Scoped check: when prismaFiles are provided, check this capability's tables instead of global status
293
+ if (ctx.prismaFiles?.length) {
294
+ const tableNames = [];
295
+ for (const file of ctx.prismaFiles) {
296
+ const filePath = path.join(ctx.projectDir, file);
297
+ tableNames.push(...parsePrismaTableNames(filePath));
298
+ }
299
+ if (tableNames.length > 0) {
300
+ // Capability defines models — check if its tables exist in the DB
301
+ const dbUrl = ctx.envValues.get('DATABASE_URL');
302
+ if (!dbUrl)
303
+ return false;
304
+ return (await checkTablesExist(dbUrl, tableNames)) === true;
305
+ }
306
+ // Capability has prisma files but no models (e.g., base schema) —
307
+ // check if _prisma_migrations table exists (initial migration was run)
308
+ const dbUrl = ctx.envValues.get('DATABASE_URL');
309
+ if (!dbUrl)
310
+ return false;
311
+ return (await checkMigrationsTable(dbUrl)) === true;
312
+ }
313
+ // Fallback: no capability context — use global check
220
314
  if (hasPrismaModels(ctx.projectDir) && !hasPrismaMigrations(ctx.projectDir))
221
315
  return false;
222
316
  const result = await checkPrismaMigrateStatus(ctx.projectDir);
@@ -233,8 +327,8 @@ async function runCheck(type, ctx) {
233
327
  * Run all setup checks declared in a capability's setupChecks list.
234
328
  * Returns an ordered list of steps with live done/pending status.
235
329
  */
236
- async function runSetupChecks(checks, envValues, projectDir, projectName) {
237
- const ctx = { envValues, projectDir, projectName, results: new Map() };
330
+ async function runSetupChecks(checks, envValues, projectDir, projectName, prismaFiles) {
331
+ const ctx = { envValues, projectDir, projectName, results: new Map(), prismaFiles };
238
332
  const steps = [];
239
333
  for (const check of checks) {
240
334
  const done = await runCheck(check.type, ctx);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@boopkit/cli",
3
- "version": "0.3.0",
4
- "description": "CLI tool to scaffold new projects from boop profiles and capabilities",
3
+ "version": "0.3.2",
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": [
7
7
  "packages/*"
@@ -24,7 +24,7 @@ await MessageQueueService.publish('batch-ready', [message1, message2]);
24
24
  - **Reuse**: The same connection and channel are reused for all subsequent publishes
25
25
  - **Auto-reconnect**: If the connection drops (`error` or `close` event), the cached reference is cleared and the next `publish()` call opens a fresh connection
26
26
  - **Error recovery**: If `publish()` catches an error (e.g., channel closed), it resets connection state and returns `false` — the next call will reconnect
27
- - **Shutdown**: The publish connection is tracked in `listenerConnections` and closed by the coordinated SIGINT handler alongside listener connections
27
+ - **Shutdown**: The publish connection is tracked and closed by `close()` alongside the listener connection
28
28
 
29
29
  ### Why persistent (not per-call)
30
30
 
@@ -36,6 +36,21 @@ The original implementation opened a new AMQP connection for every `publish()` c
36
36
  |--------|-------|-----------|
37
37
  | Scheduler jobs | various | 1-2 per 10-min cycle |
38
38
 
39
+ ## Listening
40
+
41
+ `MessageQueueService.listen()` uses a **shared AMQP connection** across all queue consumers. Each `listen()` call creates a separate **channel** on the shared connection. This keeps connection count at 2 (1 publish + 1 listen) regardless of how many queues are consumed.
42
+
43
+ ### Connection lifecycle
44
+
45
+ - **Lazy init**: No connection is opened until the first `listen()` call
46
+ - **Shared**: All listeners share the same connection; each gets its own channel with independent prefetch and consumer
47
+ - **Auto-reconnect**: If the connection drops (`error` or `close` event), the cached reference is cleared and the next `listen()` call opens a fresh connection
48
+ - **Shutdown**: The listener connection is closed by `close()` alongside the publish connection
49
+
50
+ ### Why shared (not per-queue)
51
+
52
+ CloudAMQP's shared plan has a 20-connection limit. The previous implementation opened a new AMQP connection per `listen()` call. AMQP supports multiple channels on a single connection — each channel can have its own prefetch and consumer — so sharing one connection scales to any number of queue consumers without hitting connection limits.
53
+
39
54
  ## Consumer Modes
40
55
 
41
56
  The `MessageQueueService.listen()` function supports two modes controlled by `opts.manualAck`: