@beignet/cli 0.0.9 → 0.0.11

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +70 -21
  3. package/dist/choices.d.ts +32 -3
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +54 -3
  6. package/dist/choices.js.map +1 -1
  7. package/dist/create-prompts.d.ts +5 -4
  8. package/dist/create-prompts.d.ts.map +1 -1
  9. package/dist/create-prompts.js +22 -4
  10. package/dist/create-prompts.js.map +1 -1
  11. package/dist/create.d.ts +7 -1
  12. package/dist/create.d.ts.map +1 -1
  13. package/dist/create.js +13 -3
  14. package/dist/create.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +47 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/make.d.ts +21 -1
  19. package/dist/make.d.ts.map +1 -1
  20. package/dist/make.js +210 -37
  21. package/dist/make.js.map +1 -1
  22. package/dist/mcp.d.ts +19 -0
  23. package/dist/mcp.d.ts.map +1 -0
  24. package/dist/mcp.js +250 -0
  25. package/dist/mcp.js.map +1 -0
  26. package/dist/templates/agents.d.ts +14 -0
  27. package/dist/templates/agents.d.ts.map +1 -0
  28. package/dist/templates/agents.js +98 -0
  29. package/dist/templates/agents.js.map +1 -0
  30. package/dist/templates/base.d.ts.map +1 -1
  31. package/dist/templates/base.js +79 -9
  32. package/dist/templates/base.js.map +1 -1
  33. package/dist/templates/db/index.d.ts +21 -0
  34. package/dist/templates/db/index.d.ts.map +1 -0
  35. package/dist/templates/db/index.js +14 -0
  36. package/dist/templates/db/index.js.map +1 -0
  37. package/dist/templates/db/mysql.d.ts +4 -0
  38. package/dist/templates/db/mysql.d.ts.map +1 -0
  39. package/dist/templates/db/mysql.js +972 -0
  40. package/dist/templates/db/mysql.js.map +1 -0
  41. package/dist/templates/db/postgres.d.ts +4 -0
  42. package/dist/templates/db/postgres.d.ts.map +1 -0
  43. package/dist/templates/db/postgres.js +863 -0
  44. package/dist/templates/db/postgres.js.map +1 -0
  45. package/dist/templates/db/sqlite.d.ts +3 -0
  46. package/dist/templates/db/sqlite.d.ts.map +1 -0
  47. package/dist/templates/db/sqlite.js +878 -0
  48. package/dist/templates/db/sqlite.js.map +1 -0
  49. package/dist/templates/index.d.ts +5 -5
  50. package/dist/templates/index.d.ts.map +1 -1
  51. package/dist/templates/index.js +24 -20
  52. package/dist/templates/index.js.map +1 -1
  53. package/dist/templates/server.d.ts +3 -3
  54. package/dist/templates/server.d.ts.map +1 -1
  55. package/dist/templates/server.js +149 -97
  56. package/dist/templates/server.js.map +1 -1
  57. package/dist/templates/shared.d.ts +34 -1
  58. package/dist/templates/shared.d.ts.map +1 -1
  59. package/dist/templates/shared.js +45 -0
  60. package/dist/templates/shared.js.map +1 -1
  61. package/package.json +5 -3
  62. package/src/choices.ts +70 -3
  63. package/src/create-prompts.ts +25 -3
  64. package/src/create.ts +24 -2
  65. package/src/index.ts +58 -5
  66. package/src/make.ts +265 -34
  67. package/src/mcp.ts +367 -0
  68. package/src/templates/agents.ts +103 -0
  69. package/src/templates/base.ts +95 -9
  70. package/src/templates/db/index.ts +34 -0
  71. package/src/templates/db/mysql.ts +976 -0
  72. package/src/templates/db/postgres.ts +867 -0
  73. package/src/templates/{db.ts → db/sqlite.ts} +31 -152
  74. package/src/templates/index.ts +27 -19
  75. package/src/templates/server.ts +161 -97
  76. package/src/templates/shared.ts +90 -1
package/dist/make.js CHANGED
@@ -4,6 +4,41 @@ import path from "node:path";
4
4
  import { directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
5
5
  import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, } from "./registry-edits.js";
6
6
  export { makeFeatureAddonChoices } from "./choices.js";
7
+ const drizzleDialects = {
8
+ sqlite: {
9
+ subpath: "sqlite",
10
+ dbTypeName: "DrizzleSqliteDatabase",
11
+ columnModule: "drizzle-orm/sqlite-core",
12
+ tableFunction: "sqliteTable",
13
+ schemaImports: ["integer", "sqliteTable", "text"],
14
+ supportsReturning: true,
15
+ idColumn: (columnName) => `text("${columnName}")`,
16
+ timestampColumn: (columnName) => `text("${columnName}")`,
17
+ integerColumn: (columnName) => `integer("${columnName}")`,
18
+ },
19
+ postgres: {
20
+ subpath: "postgres",
21
+ dbTypeName: "DrizzlePostgresDatabase",
22
+ columnModule: "drizzle-orm/pg-core",
23
+ tableFunction: "pgTable",
24
+ schemaImports: ["integer", "pgTable", "text"],
25
+ supportsReturning: true,
26
+ idColumn: (columnName) => `text("${columnName}")`,
27
+ timestampColumn: (columnName) => `text("${columnName}")`,
28
+ integerColumn: (columnName) => `integer("${columnName}")`,
29
+ },
30
+ mysql: {
31
+ subpath: "mysql",
32
+ dbTypeName: "DrizzleMysqlDatabase",
33
+ columnModule: "drizzle-orm/mysql-core",
34
+ tableFunction: "mysqlTable",
35
+ schemaImports: ["int", "mysqlTable", "text", "varchar"],
36
+ supportsReturning: false,
37
+ idColumn: (columnName) => `varchar("${columnName}", { length: 36 })`,
38
+ timestampColumn: (columnName) => `varchar("${columnName}", { length: 32 })`,
39
+ integerColumn: (columnName) => `int("${columnName}")`,
40
+ },
41
+ };
7
42
  export async function makeResource(options) {
8
43
  return makeResourceSlice(options, "resource");
9
44
  }
@@ -41,8 +76,11 @@ async function makeResourceSlice(options, mode) {
41
76
  throw new Error(`beignet make resource expects an app error catalog. Missing ${resourceSharedErrorsPath(config)}.`);
42
77
  }
43
78
  const persistence = await detectResourcePersistence(targetDir, config);
79
+ const database = persistence === "drizzle"
80
+ ? await detectResourceDatabase(targetDir, config)
81
+ : "sqlite";
44
82
  const generationOptions = resourceGenerationOptions(options, mode);
45
- const generatedFiles = resourceFiles(names, config, persistence, mode, generationOptions);
83
+ const generatedFiles = resourceFiles(names, config, persistence, mode, generationOptions, database);
46
84
  const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
47
85
  force: Boolean(options.force),
48
86
  });
@@ -2590,7 +2628,7 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
2590
2628
  tenant: false,
2591
2629
  events: false,
2592
2630
  softDelete: false,
2593
- }) {
2631
+ }, database = "sqlite") {
2594
2632
  const useCaseDir = resourceUseCaseDir(names, config);
2595
2633
  const infraDir = infrastructureDir(config);
2596
2634
  const featureDir = resourceFeatureDir(names, config);
@@ -2669,12 +2707,13 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
2669
2707
  },
2670
2708
  ];
2671
2709
  if (persistence === "drizzle") {
2710
+ const dialect = drizzleDialects[database];
2672
2711
  files.push({
2673
2712
  path: drizzleResourceSchemaFilePath(names, config),
2674
- content: drizzleSchemaFile(names, options),
2713
+ content: drizzleSchemaFile(names, options, dialect),
2675
2714
  }, {
2676
2715
  path: drizzleResourceRepositoryFilePath(names, config),
2677
- content: drizzleRepositoryFile(names, config, mode, options),
2716
+ content: drizzleRepositoryFile(names, config, mode, options, dialect),
2678
2717
  });
2679
2718
  }
2680
2719
  return files;
@@ -2862,6 +2901,37 @@ async function detectResourcePersistence(targetDir, config) {
2862
2901
  ? "drizzle"
2863
2902
  : "memory";
2864
2903
  }
2904
+ const databaseNames = ["sqlite", "postgres", "mysql"];
2905
+ /**
2906
+ * Detect the SQL dialect of a Drizzle app from its generated wiring.
2907
+ *
2908
+ * Precedence:
2909
+ * 1. The provider import in the Drizzle repositories file
2910
+ * (`@beignet/provider-db-drizzle/<dialect>`), resolved through the
2911
+ * configured infrastructure paths so custom layouts keep working.
2912
+ * 2. The provider registration tokens in `server/providers.ts`
2913
+ * (`drizzle<Dialect>Provider` / `createDrizzle<Dialect>Provider`).
2914
+ * 3. `"sqlite"`, the starter default.
2915
+ *
2916
+ * Self-healing by design: there is no config field to drift, the detector
2917
+ * reads the same files the app boots from.
2918
+ */
2919
+ export async function detectResourceDatabase(targetDir, config) {
2920
+ const repositories = await readOptionalFile(path.join(targetDir, drizzleRepositoriesPath(config)));
2921
+ const providerImport = repositories?.match(/from\s+["']@beignet\/provider-db-drizzle\/(sqlite|postgres|mysql)["']/);
2922
+ if (providerImport)
2923
+ return providerImport[1];
2924
+ const providers = await readOptionalFile(path.join(targetDir, providersFilePath(config)));
2925
+ if (providers !== undefined) {
2926
+ for (const database of databaseNames) {
2927
+ const dialectPascal = database.charAt(0).toUpperCase() + database.slice(1);
2928
+ const tokens = new RegExp(`\\b(?:drizzle${dialectPascal}Provider|createDrizzle${dialectPascal}Provider)\\b`);
2929
+ if (tokens.test(providers))
2930
+ return database;
2931
+ }
2932
+ }
2933
+ return "sqlite";
2934
+ }
2865
2935
  function resourceFeatureDir(names, config) {
2866
2936
  return path.join(config.paths.features, names.pluralKebab);
2867
2937
  }
@@ -3804,16 +3874,16 @@ ${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter
3804
3874
  }
3805
3875
  `;
3806
3876
  }
3807
- function drizzleSchemaFile(names, options) {
3808
- return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
3877
+ function drizzleSchemaFile(names, options, dialect) {
3878
+ return `import { ${dialect.schemaImports.join(", ")} } from "${dialect.columnModule}";
3809
3879
 
3810
- export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
3811
- id: text("id").primaryKey(),
3812
- ${options.tenant ? `\ttenantId: text("tenant_id").notNull(),\n` : ""} name: text("name").notNull(),
3813
- version: integer("version").notNull(),
3814
- createdAt: text("created_at").notNull(),
3815
- updatedAt: text("updated_at").notNull(),
3816
- ${options.softDelete ? `\tdeletedAt: text("deleted_at"),\n` : ""}});
3880
+ export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
3881
+ id: ${dialect.idColumn("id")}.primaryKey(),
3882
+ ${options.tenant ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
3883
+ version: ${dialect.integerColumn("version")}.notNull(),
3884
+ createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
3885
+ updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
3886
+ ${options.softDelete ? `\tdeletedAt: ${dialect.timestampColumn("deleted_at")},\n` : ""}});
3817
3887
  `;
3818
3888
  }
3819
3889
  function drizzleResourceWhere(names, options, predicates) {
@@ -3829,7 +3899,7 @@ function drizzleResourceWhere(names, options, predicates) {
3829
3899
  return allPredicates[0] ?? "";
3830
3900
  return `and(${allPredicates.join(", ")})`;
3831
3901
  }
3832
- function drizzleRepositoryFile(names, config, mode, options) {
3902
+ function drizzleRepositoryFile(names, config, mode, options, dialect) {
3833
3903
  const repositoryPortPath = resourcePortFilePath(names, config);
3834
3904
  const schemaPath = drizzleSchemaIndexPath(config);
3835
3905
  const findWhere = drizzleResourceWhere(names, options, [
@@ -3863,9 +3933,131 @@ function drizzleRepositoryFile(names, config, mode, options) {
3863
3933
  "sql",
3864
3934
  "type SQL",
3865
3935
  ].filter((name) => Boolean(name));
3936
+ // After a version-guarded update succeeds, re-select by identity only —
3937
+ // matching what `.returning()` yields on the dialects that support it.
3938
+ const updateReselectWhere = options.tenant
3939
+ ? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, input.tenantId))`
3940
+ : `eq(schema.${names.pluralCamel}.id, input.id)`;
3941
+ // drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
3942
+ // `[ResultSetHeader, FieldPacket[]]` tuple typed as `unknown` through the
3943
+ // generic database seam, so generated MySQL code reads affectedRows
3944
+ // defensively and re-selects rows it needs back.
3945
+ const affectedRowsHelper = mode === "resource" && !dialect.supportsReturning
3946
+ ? `
3947
+ // drizzle-orm/mysql2 mutations resolve to [ResultSetHeader, FieldPacket[]],
3948
+ // typed as unknown through the generic database seam, so read affectedRows
3949
+ // defensively.
3950
+ function affectedRows(result: unknown): number {
3951
+ const head: unknown = Array.isArray(result) ? result[0] : undefined;
3952
+ if (head === null || typeof head !== "object") return 0;
3953
+ return "affectedRows" in head && typeof head.affectedRows === "number"
3954
+ ? head.affectedRows
3955
+ : 0;
3956
+ }
3957
+ `
3958
+ : "";
3959
+ const createMethod = dialect.supportsReturning
3960
+ ? ` async create(input) {
3961
+ const now = new Date().toISOString();
3962
+ const [row] = await db
3963
+ .insert(schema.${names.pluralCamel})
3964
+ .values({
3965
+ id: crypto.randomUUID(),
3966
+ ${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
3967
+ version: 1,
3968
+ createdAt: now,
3969
+ updatedAt: now,
3970
+ })
3971
+ .returning();
3972
+
3973
+ if (!row) {
3974
+ throw new Error("Failed to create ${names.singularKebab}.");
3975
+ }
3976
+
3977
+ return to${names.singularPascal}(row);
3978
+ },
3979
+ `
3980
+ : ` async create(input) {
3981
+ const now = new Date().toISOString();
3982
+ const ${names.singularCamel}: ${names.singularPascal} = {
3983
+ id: crypto.randomUUID(),
3984
+ ${options.tenant ? `\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
3985
+ version: 1,
3986
+ createdAt: now,
3987
+ updatedAt: now,
3988
+ };
3989
+ await db.insert(schema.${names.pluralCamel}).values(${names.singularCamel});
3990
+
3991
+ return ${names.singularCamel};
3992
+ },
3993
+ `;
3994
+ const resourceMethods = mode !== "resource"
3995
+ ? ""
3996
+ : dialect.supportsReturning
3997
+ ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {
3998
+ const [row] = await db
3999
+ .select()
4000
+ .from(schema.${names.pluralCamel})
4001
+ .where(${findWhere})
4002
+ .limit(1);
4003
+
4004
+ return row ? to${names.singularPascal}(row) : null;
4005
+ },
4006
+ async update(input) {
4007
+ const [row] = await db
4008
+ .update(schema.${names.pluralCamel})
4009
+ .set({
4010
+ name: input.name,
4011
+ version: input.version + 1,
4012
+ updatedAt: new Date().toISOString(),
4013
+ })
4014
+ .where(${updateWhere})
4015
+ .returning();
4016
+
4017
+ return row ? to${names.singularPascal}(row) : null;
4018
+ },
4019
+ async delete(id: string${options.tenant ? ", filter" : ""}) {
4020
+ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}
4021
+ return rows.length > 0;
4022
+ },
4023
+ `
4024
+ : ` async findById(id: string${options.tenant ? ", filter" : ""}) {
4025
+ const [row] = await db
4026
+ .select()
4027
+ .from(schema.${names.pluralCamel})
4028
+ .where(${findWhere})
4029
+ .limit(1);
4030
+
4031
+ return row ? to${names.singularPascal}(row) : null;
4032
+ },
4033
+ async update(input) {
4034
+ const result = await db
4035
+ .update(schema.${names.pluralCamel})
4036
+ .set({
4037
+ name: input.name,
4038
+ version: input.version + 1,
4039
+ updatedAt: new Date().toISOString(),
4040
+ })
4041
+ .where(${updateWhere});
4042
+
4043
+ if (affectedRows(result) === 0) return null;
4044
+
4045
+ const [row] = await db
4046
+ .select()
4047
+ .from(schema.${names.pluralCamel})
4048
+ .where(${updateReselectWhere})
4049
+ .limit(1);
4050
+
4051
+ return row ? to${names.singularPascal}(row) : null;
4052
+ },
4053
+ async delete(id: string${options.tenant ? ", filter" : ""}) {
4054
+ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst result = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere});\n` : `\t\t\tconst result = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere});\n`}
4055
+ return affectedRows(result) > 0;
4056
+ },
4057
+ `;
3866
4058
  return `import type { beignetServerOnly } from "@beignet/core/server-only";
3867
4059
  import { cursorPageResult } from "@beignet/core/pagination";
3868
- import type { DrizzleSqliteDatabase } from "@beignet/provider-db-drizzle/sqlite";
4060
+ import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
3869
4061
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
3870
4062
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
3871
4063
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -3886,7 +4078,7 @@ ${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
3886
4078
  updatedAt: row.updatedAt,
3887
4079
  };
3888
4080
  }
3889
-
4081
+ ${affectedRowsHelper}
3890
4082
  type List${names.pluralPascal}Query = Parameters<${names.singularPascal}Repository["list"]>[0];
3891
4083
 
3892
4084
  function ${names.singularCamel}SortColumn(query: List${names.pluralPascal}Query) {
@@ -3945,7 +4137,7 @@ function cursorFor${names.singularPascal}(
3945
4137
  }
3946
4138
 
3947
4139
  export function createDrizzle${names.singularPascal}Repository(
3948
- db: DrizzleSqliteDatabase<typeof schema>,
4140
+ db: ${dialect.dbTypeName}<typeof schema>,
3949
4141
  ): ${names.singularPascal}Repository {
3950
4142
  return {
3951
4143
  async list(query) {
@@ -3968,26 +4160,7 @@ export function createDrizzle${names.singularPascal}Repository(
3968
4160
  nextCursor,
3969
4161
  );
3970
4162
  },
3971
- async create(input) {
3972
- const now = new Date().toISOString();
3973
- const [row] = await db
3974
- .insert(schema.${names.pluralCamel})
3975
- .values({
3976
- id: crypto.randomUUID(),
3977
- ${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
3978
- version: 1,
3979
- createdAt: now,
3980
- updatedAt: now,
3981
- })
3982
- .returning();
3983
-
3984
- if (!row) {
3985
- throw new Error("Failed to create ${names.singularKebab}.");
3986
- }
3987
-
3988
- return to${names.singularPascal}(row);
3989
- },
3990
- ${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {\n const [row] = await db\n .select()\n .from(schema.${names.pluralCamel})\n .where(${findWhere})\n .limit(1);\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async update(input) {\n const [row] = await db\n .update(schema.${names.pluralCamel})\n .set({\n name: input.name,\n version: input.version + 1,\n updatedAt: new Date().toISOString(),\n })\n .where(${updateWhere})\n .returning();\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async delete(id: string${options.tenant ? ", filter" : ""}) {\n${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}\n return rows.length > 0;\n },\n` : ""}
4163
+ ${createMethod}${resourceMethods}
3991
4164
  };
3992
4165
  }
3993
4166
  `;