@beignet/cli 0.0.24 → 0.0.26
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/CHANGELOG.md +21 -0
- package/README.md +48 -5
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +30 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +201 -1
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +157 -235
- package/dist/inspect.js.map +1 -1
- package/dist/provider-audit.d.ts +131 -0
- package/dist/provider-audit.d.ts.map +1 -0
- package/dist/provider-audit.js +1013 -0
- package/dist/provider-audit.js.map +1 -0
- package/dist/templates/agents.js +2 -2
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +8 -1
- package/dist/templates/base.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +27 -4
- package/src/config.ts +99 -0
- package/src/db.ts +316 -1
- package/src/index.ts +180 -1
- package/src/inspect.ts +298 -369
- package/src/provider-audit.ts +1681 -0
- package/src/templates/agents.ts +2 -2
- package/src/templates/base.ts +8 -1
package/src/db.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -33,6 +33,31 @@ export type RunDatabaseCommandResult = {
|
|
|
33
33
|
exitCode: number;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export type DatabaseSchemaDialect = "sqlite" | "postgres" | "mysql";
|
|
37
|
+
export type DatabaseSchemaTable = "audit" | "idempotency" | "outbox";
|
|
38
|
+
|
|
39
|
+
export type GenerateDatabaseSchemaOptions = {
|
|
40
|
+
cwd?: string;
|
|
41
|
+
dialect?: DatabaseSchemaDialect;
|
|
42
|
+
tables?: readonly DatabaseSchemaTable[];
|
|
43
|
+
output?: string;
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type GenerateDatabaseSchemaResult = {
|
|
48
|
+
schemaVersion: 1;
|
|
49
|
+
command: "schema:generate";
|
|
50
|
+
cwd: string;
|
|
51
|
+
dialect: DatabaseSchemaDialect;
|
|
52
|
+
tables: DatabaseSchemaTable[];
|
|
53
|
+
output: string;
|
|
54
|
+
indexFile: string;
|
|
55
|
+
createdFiles: string[];
|
|
56
|
+
updatedFiles: string[];
|
|
57
|
+
skippedFiles: string[];
|
|
58
|
+
dryRun: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
36
61
|
type PackageJson = {
|
|
37
62
|
scripts?: Record<string, string>;
|
|
38
63
|
};
|
|
@@ -44,6 +69,12 @@ const databaseScripts: Record<DatabaseCommand, string> = {
|
|
|
44
69
|
reset: "db:reset",
|
|
45
70
|
};
|
|
46
71
|
|
|
72
|
+
const allDatabaseSchemaTables = [
|
|
73
|
+
"audit",
|
|
74
|
+
"idempotency",
|
|
75
|
+
"outbox",
|
|
76
|
+
] as const satisfies readonly DatabaseSchemaTable[];
|
|
77
|
+
|
|
47
78
|
/**
|
|
48
79
|
* Run an app-owned database lifecycle script.
|
|
49
80
|
*
|
|
@@ -100,6 +131,67 @@ export async function runDatabaseCommand(
|
|
|
100
131
|
};
|
|
101
132
|
}
|
|
102
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Generate the app-owned Drizzle schema entrypoint for Beignet provider tables.
|
|
136
|
+
*
|
|
137
|
+
* This only writes schema source. The app still owns SQL migration generation
|
|
138
|
+
* through `beignet db generate` and migration application through
|
|
139
|
+
* `beignet db migrate`.
|
|
140
|
+
*/
|
|
141
|
+
export async function generateDatabaseSchema(
|
|
142
|
+
options: GenerateDatabaseSchemaOptions = {},
|
|
143
|
+
): Promise<GenerateDatabaseSchemaResult> {
|
|
144
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
145
|
+
const dialect = await resolveDatabaseSchemaDialect(cwd, options.dialect);
|
|
146
|
+
const tables = uniqueSchemaTables(options.tables ?? allDatabaseSchemaTables);
|
|
147
|
+
const output = normalizeAppRelativePath(
|
|
148
|
+
cwd,
|
|
149
|
+
options.output ?? "infra/db/schema/beignet.ts",
|
|
150
|
+
"schema output",
|
|
151
|
+
);
|
|
152
|
+
const indexFile = normalizeAppRelativePath(
|
|
153
|
+
cwd,
|
|
154
|
+
path.join(path.dirname(output), "index.ts"),
|
|
155
|
+
"schema index",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (output === indexFile) {
|
|
159
|
+
throw new Error("Schema output must not be the schema index file.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const result: GenerateDatabaseSchemaResult = {
|
|
163
|
+
schemaVersion: 1,
|
|
164
|
+
command: "schema:generate",
|
|
165
|
+
cwd,
|
|
166
|
+
dialect,
|
|
167
|
+
tables,
|
|
168
|
+
output,
|
|
169
|
+
indexFile,
|
|
170
|
+
createdFiles: [],
|
|
171
|
+
updatedFiles: [],
|
|
172
|
+
skippedFiles: [],
|
|
173
|
+
dryRun: Boolean(options.dryRun),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const schemaStatus = await writeProjectFile(
|
|
177
|
+
cwd,
|
|
178
|
+
output,
|
|
179
|
+
renderDatabaseSchema(dialect, tables),
|
|
180
|
+
result.dryRun,
|
|
181
|
+
);
|
|
182
|
+
recordFileStatus(result, output, schemaStatus);
|
|
183
|
+
|
|
184
|
+
const indexStatus = await updateSchemaIndex(
|
|
185
|
+
cwd,
|
|
186
|
+
indexFile,
|
|
187
|
+
output,
|
|
188
|
+
result.dryRun,
|
|
189
|
+
);
|
|
190
|
+
recordFileStatus(result, indexFile, indexStatus);
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
103
195
|
async function assertDatabaseCommandPreflight(
|
|
104
196
|
cwd: string,
|
|
105
197
|
command: DatabaseCommand,
|
|
@@ -135,6 +227,229 @@ async function assertDatabaseCommandPreflight(
|
|
|
135
227
|
}
|
|
136
228
|
}
|
|
137
229
|
|
|
230
|
+
async function resolveDatabaseSchemaDialect(
|
|
231
|
+
cwd: string,
|
|
232
|
+
explicitDialect: DatabaseSchemaDialect | undefined,
|
|
233
|
+
): Promise<DatabaseSchemaDialect> {
|
|
234
|
+
if (explicitDialect !== undefined) return explicitDialect;
|
|
235
|
+
|
|
236
|
+
const providerDialect = await inferDialectFromProviders(cwd);
|
|
237
|
+
if (providerDialect !== undefined) return providerDialect;
|
|
238
|
+
|
|
239
|
+
const drizzleDialect = await inferDialectFromDrizzleConfig(cwd);
|
|
240
|
+
if (drizzleDialect !== undefined) return drizzleDialect;
|
|
241
|
+
|
|
242
|
+
throw new Error(
|
|
243
|
+
"Could not infer the Drizzle dialect. Pass --dialect sqlite, --dialect postgres, or --dialect mysql.",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function inferDialectFromProviders(
|
|
248
|
+
cwd: string,
|
|
249
|
+
): Promise<DatabaseSchemaDialect | undefined> {
|
|
250
|
+
const sources = await Promise.all(
|
|
251
|
+
["server/providers.ts", "server/index.ts"].map(async (file) => {
|
|
252
|
+
try {
|
|
253
|
+
return await readFile(path.join(cwd, file), "utf8");
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "";
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
const source = sources.join("\n");
|
|
261
|
+
const matches = new Set<DatabaseSchemaDialect>();
|
|
262
|
+
|
|
263
|
+
if (
|
|
264
|
+
/\b(?:drizzleSqliteProvider|createDrizzleSqliteProvider)\b/.test(source)
|
|
265
|
+
) {
|
|
266
|
+
matches.add("sqlite");
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
/\b(?:drizzlePostgresProvider|createDrizzlePostgresProvider)\b/.test(source)
|
|
270
|
+
) {
|
|
271
|
+
matches.add("postgres");
|
|
272
|
+
}
|
|
273
|
+
if (/\b(?:drizzleMysqlProvider|createDrizzleMysqlProvider)\b/.test(source)) {
|
|
274
|
+
matches.add("mysql");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return singleInferredDialect(matches, "registered Drizzle providers");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function inferDialectFromDrizzleConfig(
|
|
281
|
+
cwd: string,
|
|
282
|
+
): Promise<DatabaseSchemaDialect | undefined> {
|
|
283
|
+
for (const file of [
|
|
284
|
+
"drizzle.config.ts",
|
|
285
|
+
"drizzle.config.mts",
|
|
286
|
+
"drizzle.config.js",
|
|
287
|
+
"drizzle.config.mjs",
|
|
288
|
+
]) {
|
|
289
|
+
let source: string;
|
|
290
|
+
try {
|
|
291
|
+
source = await readFile(path.join(cwd, file), "utf8");
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const matches = new Set<DatabaseSchemaDialect>();
|
|
298
|
+
if (/\bdialect\s*:\s*["'](?:sqlite|turso)["']/.test(source)) {
|
|
299
|
+
matches.add("sqlite");
|
|
300
|
+
}
|
|
301
|
+
if (/\bdialect\s*:\s*["']postgresql["']/.test(source)) {
|
|
302
|
+
matches.add("postgres");
|
|
303
|
+
}
|
|
304
|
+
if (/\bdialect\s*:\s*["']mysql["']/.test(source)) {
|
|
305
|
+
matches.add("mysql");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const inferred = singleInferredDialect(matches, file);
|
|
309
|
+
if (inferred !== undefined) return inferred;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function singleInferredDialect(
|
|
316
|
+
matches: Set<DatabaseSchemaDialect>,
|
|
317
|
+
source: string,
|
|
318
|
+
): DatabaseSchemaDialect | undefined {
|
|
319
|
+
if (matches.size === 0) return undefined;
|
|
320
|
+
if (matches.size === 1) return [...matches][0];
|
|
321
|
+
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Could not infer a single Drizzle dialect from ${source}. Pass --dialect sqlite, --dialect postgres, or --dialect mysql.`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function normalizeAppRelativePath(
|
|
328
|
+
cwd: string,
|
|
329
|
+
input: string,
|
|
330
|
+
label: string,
|
|
331
|
+
): string {
|
|
332
|
+
const absolute = path.resolve(cwd, input);
|
|
333
|
+
const relative = path.relative(cwd, absolute);
|
|
334
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
335
|
+
throw new Error(`The ${label} path must be inside the app directory.`);
|
|
336
|
+
}
|
|
337
|
+
return relative.split(path.sep).join("/");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function renderDatabaseSchema(
|
|
341
|
+
dialect: DatabaseSchemaDialect,
|
|
342
|
+
tables: readonly DatabaseSchemaTable[],
|
|
343
|
+
): string {
|
|
344
|
+
const selectedTables = tables.map((table) => schemaTableDefinitions[table]);
|
|
345
|
+
const tableNames = selectedTables.map((table) => table.tableName);
|
|
346
|
+
const exportNames = selectedTables.map((table) => table.exportName).sort();
|
|
347
|
+
|
|
348
|
+
return `// Generated by beignet db schema generate.
|
|
349
|
+
// Required tables: ${tableNames.join(", ")}.
|
|
350
|
+
// Re-run this command after upgrading @beignet/provider-db-drizzle.
|
|
351
|
+
|
|
352
|
+
export {
|
|
353
|
+
${exportNames.map((name) => ` ${name},`).join("\n")}
|
|
354
|
+
} from "@beignet/provider-db-drizzle/${dialect}/schema";
|
|
355
|
+
`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const schemaTableDefinitions: Record<
|
|
359
|
+
DatabaseSchemaTable,
|
|
360
|
+
{ exportName: string; tableName: string }
|
|
361
|
+
> = {
|
|
362
|
+
audit: { exportName: "auditLog", tableName: "audit_log" },
|
|
363
|
+
idempotency: {
|
|
364
|
+
exportName: "idempotencyRecords",
|
|
365
|
+
tableName: "idempotency_records",
|
|
366
|
+
},
|
|
367
|
+
outbox: { exportName: "outboxMessages", tableName: "outbox_messages" },
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
function uniqueSchemaTables(
|
|
371
|
+
tables: readonly DatabaseSchemaTable[],
|
|
372
|
+
): DatabaseSchemaTable[] {
|
|
373
|
+
const unique = allDatabaseSchemaTables.filter((table) =>
|
|
374
|
+
tables.includes(table),
|
|
375
|
+
);
|
|
376
|
+
if (unique.length === 0) {
|
|
377
|
+
throw new Error("At least one schema table must be selected.");
|
|
378
|
+
}
|
|
379
|
+
return unique;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
type FileWriteStatus = "created" | "updated" | "unchanged";
|
|
383
|
+
|
|
384
|
+
async function writeProjectFile(
|
|
385
|
+
cwd: string,
|
|
386
|
+
relativePath: string,
|
|
387
|
+
contents: string,
|
|
388
|
+
dryRun: boolean,
|
|
389
|
+
): Promise<FileWriteStatus> {
|
|
390
|
+
const absolute = path.join(cwd, relativePath);
|
|
391
|
+
const existing = await readTextIfExists(absolute);
|
|
392
|
+
if (existing === contents) return "unchanged";
|
|
393
|
+
|
|
394
|
+
if (!dryRun) {
|
|
395
|
+
await mkdir(path.dirname(absolute), { recursive: true });
|
|
396
|
+
await writeFile(absolute, contents);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return existing === undefined ? "created" : "updated";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function updateSchemaIndex(
|
|
403
|
+
cwd: string,
|
|
404
|
+
indexFile: string,
|
|
405
|
+
output: string,
|
|
406
|
+
dryRun: boolean,
|
|
407
|
+
): Promise<FileWriteStatus> {
|
|
408
|
+
const indexAbsolute = path.join(cwd, indexFile);
|
|
409
|
+
const existing = await readTextIfExists(indexAbsolute);
|
|
410
|
+
const exportLine = `export * from "./${schemaIndexSpecifier(indexFile, output)}";`;
|
|
411
|
+
const contents =
|
|
412
|
+
existing === undefined
|
|
413
|
+
? `${exportLine}\n`
|
|
414
|
+
: existing.includes(exportLine)
|
|
415
|
+
? existing
|
|
416
|
+
: `${existing.trimEnd()}\n${exportLine}\n`;
|
|
417
|
+
|
|
418
|
+
return writeProjectFile(cwd, indexFile, contents, dryRun);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function schemaIndexSpecifier(indexFile: string, output: string): string {
|
|
422
|
+
const relative = path.relative(path.dirname(indexFile), output);
|
|
423
|
+
return stripTsExtension(relative).split(path.sep).join("/");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function stripTsExtension(file: string): string {
|
|
427
|
+
return file.replace(/\.(?:ts|mts|cts)$/, "");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function readTextIfExists(file: string): Promise<string | undefined> {
|
|
431
|
+
try {
|
|
432
|
+
return await readFile(file, "utf8");
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function recordFileStatus(
|
|
440
|
+
result: GenerateDatabaseSchemaResult,
|
|
441
|
+
file: string,
|
|
442
|
+
status: FileWriteStatus,
|
|
443
|
+
): void {
|
|
444
|
+
if (status === "created") {
|
|
445
|
+
result.createdFiles.push(file);
|
|
446
|
+
} else if (status === "updated") {
|
|
447
|
+
result.updatedFiles.push(file);
|
|
448
|
+
} else {
|
|
449
|
+
result.skippedFiles.push(file);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
138
453
|
function hasExplicitDrizzleConfigFlag(scriptCommand: string): boolean {
|
|
139
454
|
return /(?:^|\s)--config(?:=|\s)/.test(scriptCommand);
|
|
140
455
|
}
|
package/src/index.ts
CHANGED
|
@@ -107,6 +107,27 @@ type DbFlags = {
|
|
|
107
107
|
dryRun?: boolean;
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
type DatabaseSchemaDialect = "sqlite" | "postgres" | "mysql";
|
|
111
|
+
type DatabaseSchemaTable = "audit" | "idempotency" | "outbox";
|
|
112
|
+
|
|
113
|
+
const databaseSchemaDialectChoices = [
|
|
114
|
+
"sqlite",
|
|
115
|
+
"postgres",
|
|
116
|
+
"mysql",
|
|
117
|
+
] as const satisfies readonly DatabaseSchemaDialect[];
|
|
118
|
+
|
|
119
|
+
const databaseSchemaTableChoices = [
|
|
120
|
+
"audit",
|
|
121
|
+
"idempotency",
|
|
122
|
+
"outbox",
|
|
123
|
+
] as const satisfies readonly DatabaseSchemaTable[];
|
|
124
|
+
|
|
125
|
+
type DbSchemaGenerateFlags = DbFlags & {
|
|
126
|
+
dialect?: DatabaseSchemaDialect;
|
|
127
|
+
tables?: readonly DatabaseSchemaTable[];
|
|
128
|
+
output?: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
110
131
|
type TaskRunFlags = {
|
|
111
132
|
json?: boolean;
|
|
112
133
|
input?: string;
|
|
@@ -142,6 +163,11 @@ type DoctorFlags = {
|
|
|
142
163
|
format?: OutputFormat;
|
|
143
164
|
};
|
|
144
165
|
|
|
166
|
+
type ProviderAuditFlags = {
|
|
167
|
+
json?: boolean;
|
|
168
|
+
cwd?: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
145
171
|
const parseString = (input: string): string => input;
|
|
146
172
|
const parsePositiveInteger = (input: string): number => {
|
|
147
173
|
const value = Number(input);
|
|
@@ -209,11 +235,38 @@ const lintFlagParameters = {
|
|
|
209
235
|
format: formatFlag,
|
|
210
236
|
} satisfies FlagParametersForType<LintFlags, CliContext>;
|
|
211
237
|
|
|
238
|
+
const providerAuditFlagParameters = {
|
|
239
|
+
json: jsonFlag,
|
|
240
|
+
cwd: cwdFlag,
|
|
241
|
+
} satisfies FlagParametersForType<ProviderAuditFlags, CliContext>;
|
|
242
|
+
|
|
212
243
|
const dbFlagParameters = {
|
|
213
244
|
json: jsonFlag,
|
|
214
245
|
dryRun: dryRunFlag,
|
|
215
246
|
} satisfies FlagParametersForType<DbFlags, CliContext>;
|
|
216
247
|
|
|
248
|
+
const dbSchemaGenerateFlagParameters = {
|
|
249
|
+
...dbFlagParameters,
|
|
250
|
+
dialect: {
|
|
251
|
+
kind: "enum",
|
|
252
|
+
values: databaseSchemaDialectChoices,
|
|
253
|
+
optional: true,
|
|
254
|
+
brief:
|
|
255
|
+
"Drizzle dialect to generate for. Inferred from server/providers.ts when omitted.",
|
|
256
|
+
},
|
|
257
|
+
tables: {
|
|
258
|
+
kind: "enum",
|
|
259
|
+
values: databaseSchemaTableChoices,
|
|
260
|
+
optional: true,
|
|
261
|
+
variadic: ",",
|
|
262
|
+
brief:
|
|
263
|
+
"Provider tables to generate as a comma-separated list. Defaults to audit,idempotency,outbox.",
|
|
264
|
+
},
|
|
265
|
+
output: parsedStringFlag(
|
|
266
|
+
"Generated schema file. Defaults to infra/db/schema/beignet.ts.",
|
|
267
|
+
),
|
|
268
|
+
} satisfies FlagParametersForType<DbSchemaGenerateFlags, CliContext>;
|
|
269
|
+
|
|
217
270
|
const taskRunFlagParameters = {
|
|
218
271
|
json: jsonFlag,
|
|
219
272
|
input: parsedStringFlag("JSON input passed to the task schema."),
|
|
@@ -557,6 +610,44 @@ const mcpCommand = buildCommand<McpFlags, [], CliContext>({
|
|
|
557
610
|
},
|
|
558
611
|
});
|
|
559
612
|
|
|
613
|
+
const providerAuditCommand = buildCommand<ProviderAuditFlags, [], CliContext>({
|
|
614
|
+
docs: {
|
|
615
|
+
brief: "Audit installed Beignet provider setup.",
|
|
616
|
+
fullDescription:
|
|
617
|
+
"Reports installed provider packages, metadata validity, registration, provider env, required tables, app ports, and variants. This command is report-only; doctor remains the enforcement surface.",
|
|
618
|
+
},
|
|
619
|
+
parameters: {
|
|
620
|
+
flags: providerAuditFlagParameters,
|
|
621
|
+
},
|
|
622
|
+
loader: async () => {
|
|
623
|
+
const { auditProviders, formatProviderAudit } = await import(
|
|
624
|
+
"./provider-audit.js"
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
return async function runProviderAudit(
|
|
628
|
+
this: CliContext,
|
|
629
|
+
flags: ProviderAuditFlags,
|
|
630
|
+
) {
|
|
631
|
+
const result = await auditProviders({ cwd: flags.cwd });
|
|
632
|
+
writeOutput(
|
|
633
|
+
this,
|
|
634
|
+
flags.json
|
|
635
|
+
? JSON.stringify(result, null, 2)
|
|
636
|
+
: formatProviderAudit(result),
|
|
637
|
+
);
|
|
638
|
+
};
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const providerRoutes = buildRouteMap({
|
|
643
|
+
docs: {
|
|
644
|
+
brief: "Inspect Beignet provider setup.",
|
|
645
|
+
},
|
|
646
|
+
routes: {
|
|
647
|
+
audit: providerAuditCommand,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
560
651
|
type DatabaseCommandName = "generate" | "migrate" | "reset" | "seed";
|
|
561
652
|
|
|
562
653
|
function databaseCommand(command: DatabaseCommandName) {
|
|
@@ -591,6 +682,52 @@ function databaseCommand(command: DatabaseCommandName) {
|
|
|
591
682
|
});
|
|
592
683
|
}
|
|
593
684
|
|
|
685
|
+
const dbSchemaGenerateCommand = buildCommand<
|
|
686
|
+
DbSchemaGenerateFlags,
|
|
687
|
+
[],
|
|
688
|
+
CliContext
|
|
689
|
+
>({
|
|
690
|
+
docs: {
|
|
691
|
+
brief: "Generate provider-owned Drizzle schema exports.",
|
|
692
|
+
fullDescription:
|
|
693
|
+
"Writes an app-owned schema file that re-exports Beignet provider tables, then updates the schema index. Run beignet db generate afterward to let the app's Drizzle Kit script create migrations.",
|
|
694
|
+
},
|
|
695
|
+
parameters: {
|
|
696
|
+
flags: dbSchemaGenerateFlagParameters,
|
|
697
|
+
},
|
|
698
|
+
loader: async () => {
|
|
699
|
+
const { generateDatabaseSchema } = await import("./db.js");
|
|
700
|
+
|
|
701
|
+
return async function runDbSchemaGenerate(
|
|
702
|
+
this: CliContext,
|
|
703
|
+
flags: DbSchemaGenerateFlags,
|
|
704
|
+
) {
|
|
705
|
+
const result = await generateDatabaseSchema({
|
|
706
|
+
dialect: flags.dialect,
|
|
707
|
+
tables: flags.tables,
|
|
708
|
+
output: flags.output,
|
|
709
|
+
dryRun: Boolean(flags.dryRun),
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
writeOutput(
|
|
713
|
+
this,
|
|
714
|
+
flags.json
|
|
715
|
+
? JSON.stringify(result, null, 2)
|
|
716
|
+
: databaseSchemaGenerateNextSteps(result),
|
|
717
|
+
);
|
|
718
|
+
};
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const dbSchemaRoutes = buildRouteMap({
|
|
723
|
+
docs: {
|
|
724
|
+
brief: "Generate Beignet database schema helpers.",
|
|
725
|
+
},
|
|
726
|
+
routes: {
|
|
727
|
+
generate: dbSchemaGenerateCommand,
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
|
|
594
731
|
const dbRoutes = buildRouteMap({
|
|
595
732
|
docs: {
|
|
596
733
|
brief: "Run Beignet database lifecycle commands.",
|
|
@@ -599,6 +736,7 @@ const dbRoutes = buildRouteMap({
|
|
|
599
736
|
generate: databaseCommand("generate"),
|
|
600
737
|
migrate: databaseCommand("migrate"),
|
|
601
738
|
reset: databaseCommand("reset"),
|
|
739
|
+
schema: dbSchemaRoutes,
|
|
602
740
|
seed: databaseCommand("seed"),
|
|
603
741
|
},
|
|
604
742
|
});
|
|
@@ -1214,6 +1352,7 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
|
1214
1352
|
make: makeRoutes,
|
|
1215
1353
|
mcp: mcpCommand,
|
|
1216
1354
|
outbox: outboxRoutes,
|
|
1355
|
+
providers: providerRoutes,
|
|
1217
1356
|
routes: routesCommand,
|
|
1218
1357
|
schedule: scheduleRoutes,
|
|
1219
1358
|
task: taskRoutes,
|
|
@@ -1278,6 +1417,18 @@ type DatabaseCommandNextStepsResult = {
|
|
|
1278
1417
|
dryRun: boolean;
|
|
1279
1418
|
};
|
|
1280
1419
|
|
|
1420
|
+
type DatabaseSchemaGenerateNextStepsResult = {
|
|
1421
|
+
cwd: string;
|
|
1422
|
+
dialect: DatabaseSchemaDialect;
|
|
1423
|
+
tables: DatabaseSchemaTable[];
|
|
1424
|
+
output: string;
|
|
1425
|
+
indexFile: string;
|
|
1426
|
+
createdFiles: string[];
|
|
1427
|
+
updatedFiles: string[];
|
|
1428
|
+
skippedFiles: string[];
|
|
1429
|
+
dryRun: boolean;
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1281
1432
|
type AppTaskRunNextStepsResult = {
|
|
1282
1433
|
name: string;
|
|
1283
1434
|
durationMs: number;
|
|
@@ -1432,7 +1583,12 @@ function isEnvRequiredProviderIntegration(
|
|
|
1432
1583
|
);
|
|
1433
1584
|
}
|
|
1434
1585
|
|
|
1435
|
-
function changedFileLines(
|
|
1586
|
+
function changedFileLines(
|
|
1587
|
+
result: Pick<
|
|
1588
|
+
MakeNextStepsResult,
|
|
1589
|
+
"createdFiles" | "updatedFiles" | "skippedFiles"
|
|
1590
|
+
>,
|
|
1591
|
+
): {
|
|
1436
1592
|
changedFiles: string;
|
|
1437
1593
|
skippedFiles: string;
|
|
1438
1594
|
} {
|
|
@@ -1482,6 +1638,29 @@ Command:
|
|
|
1482
1638
|
${command}`;
|
|
1483
1639
|
}
|
|
1484
1640
|
|
|
1641
|
+
function databaseSchemaGenerateNextSteps(
|
|
1642
|
+
result: DatabaseSchemaGenerateNextStepsResult,
|
|
1643
|
+
): string {
|
|
1644
|
+
const { changedFiles, skippedFiles } = changedFileLines({
|
|
1645
|
+
createdFiles: result.createdFiles,
|
|
1646
|
+
updatedFiles: result.updatedFiles,
|
|
1647
|
+
skippedFiles: result.skippedFiles,
|
|
1648
|
+
});
|
|
1649
|
+
const prefix = result.dryRun ? "Would generate" : "Generated";
|
|
1650
|
+
const tables = result.tables.join(", ");
|
|
1651
|
+
|
|
1652
|
+
return `${prefix} Beignet ${result.dialect} database schema (${tables}) in ${result.cwd}
|
|
1653
|
+
|
|
1654
|
+
Changed files:
|
|
1655
|
+
${changedFiles || " none"}
|
|
1656
|
+
${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
|
|
1657
|
+
|
|
1658
|
+
Next steps:
|
|
1659
|
+
Run beignet db generate to produce app-owned Drizzle migrations.
|
|
1660
|
+
Run beignet db migrate to apply the migration.
|
|
1661
|
+
Run beignet doctor --strict to check provider setup.`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1485
1664
|
function makeFeatureNextSteps(result: MakeNextStepsResult): string {
|
|
1486
1665
|
return makeNextSteps(result, "feature slice", [
|
|
1487
1666
|
"Use make feature for product capabilities and workflows. Use make resource when the concept is mostly CRUD-shaped.",
|