@beignet/cli 0.0.22 → 0.0.24

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 (59) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +35 -5
  3. package/dist/choices.d.ts +8 -0
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +4 -0
  6. package/dist/choices.js.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +8 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/inspect.d.ts.map +1 -1
  11. package/dist/inspect.js +184 -0
  12. package/dist/inspect.js.map +1 -1
  13. package/dist/lib.d.ts +2 -2
  14. package/dist/lib.d.ts.map +1 -1
  15. package/dist/lib.js +1 -1
  16. package/dist/lib.js.map +1 -1
  17. package/dist/make.d.ts +4 -3
  18. package/dist/make.d.ts.map +1 -1
  19. package/dist/make.js +349 -45
  20. package/dist/make.js.map +1 -1
  21. package/dist/mcp.d.ts.map +1 -1
  22. package/dist/mcp.js +6 -2
  23. package/dist/mcp.js.map +1 -1
  24. package/dist/templates/agents.d.ts.map +1 -1
  25. package/dist/templates/agents.js +9 -5
  26. package/dist/templates/agents.js.map +1 -1
  27. package/dist/templates/base.d.ts.map +1 -1
  28. package/dist/templates/base.js +4 -2
  29. package/dist/templates/base.js.map +1 -1
  30. package/dist/templates/db/mysql.js +2 -2
  31. package/dist/templates/db/postgres.js +2 -2
  32. package/dist/templates/db/sqlite.js +2 -2
  33. package/dist/templates/index.d.ts.map +1 -1
  34. package/dist/templates/index.js +1 -0
  35. package/dist/templates/index.js.map +1 -1
  36. package/dist/templates/server.d.ts +1 -0
  37. package/dist/templates/server.d.ts.map +1 -1
  38. package/dist/templates/server.js +90 -20
  39. package/dist/templates/server.js.map +1 -1
  40. package/dist/templates/shell.d.ts.map +1 -1
  41. package/dist/templates/shell.js +60 -29
  42. package/dist/templates/shell.js.map +1 -1
  43. package/dist/templates/todos.js +5 -5
  44. package/package.json +2 -2
  45. package/src/choices.ts +10 -0
  46. package/src/index.ts +11 -0
  47. package/src/inspect.ts +319 -0
  48. package/src/lib.ts +2 -1
  49. package/src/make.ts +449 -53
  50. package/src/mcp.ts +7 -1
  51. package/src/templates/agents.ts +9 -5
  52. package/src/templates/base.ts +4 -2
  53. package/src/templates/db/mysql.ts +2 -2
  54. package/src/templates/db/postgres.ts +2 -2
  55. package/src/templates/db/sqlite.ts +2 -2
  56. package/src/templates/index.ts +1 -0
  57. package/src/templates/server.ts +90 -20
  58. package/src/templates/shell.ts +61 -29
  59. package/src/templates/todos.ts +5 -5
package/src/make.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import type { MakeFeatureAddon } from "./choices.js";
4
+ import type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
5
5
  import {
6
6
  type BeignetConfig,
7
7
  clientIndexPath,
@@ -33,8 +33,11 @@ type MakeResourceOptions = {
33
33
  softDelete?: boolean;
34
34
  };
35
35
 
36
- export type { MakeFeatureAddon } from "./choices.js";
37
- export { makeFeatureAddonChoices } from "./choices.js";
36
+ export type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
37
+ export {
38
+ makeFeatureAddonChoices,
39
+ makeFeatureRecipeChoices,
40
+ } from "./choices.js";
38
41
 
39
42
  type NormalizedMakeFeatureAddon =
40
43
  | "policy"
@@ -51,6 +54,7 @@ type NormalizedMakeFeatureAddon =
51
54
 
52
55
  type MakeFeatureOptions = MakeResourceOptions & {
53
56
  with?: readonly MakeFeatureAddon[];
57
+ recipe?: MakeFeatureRecipe;
54
58
  };
55
59
 
56
60
  type MakeContractOptions = {
@@ -490,17 +494,26 @@ export async function makeResource(
490
494
  export async function makeFeature(
491
495
  options: MakeFeatureOptions,
492
496
  ): Promise<MakeFeatureResult> {
493
- const addons = normalizeMakeFeatureAddons(options.with ?? []);
497
+ const addons = normalizeMakeFeatureAddons([
498
+ ...makeFeatureRecipeAddons(options.recipe),
499
+ ...(options.with ?? []),
500
+ ...(options.events ? (["events"] as const) : []),
501
+ ]);
502
+ const featureOptions = {
503
+ ...options,
504
+ events: addons.includes("event"),
505
+ };
506
+
494
507
  if (addons.length === 0) {
495
- return makeFeatureResource(options);
508
+ return makeFeatureResource(featureOptions);
496
509
  }
497
510
 
498
511
  if (options.dryRun) {
499
- return makeFeatureSlice(options, addons);
512
+ return makeFeatureSlice(featureOptions, addons);
500
513
  }
501
514
 
502
- await makeFeatureSlice({ ...options, dryRun: true }, addons);
503
- return makeFeatureSlice(options, addons);
515
+ await makeFeatureSlice({ ...featureOptions, dryRun: true }, addons);
516
+ return makeFeatureSlice(featureOptions, addons);
504
517
  }
505
518
 
506
519
  export async function makePayments(
@@ -718,6 +731,15 @@ function resourceGenerationOptions(
718
731
  options: MakeResourceOptions,
719
732
  mode: ResourceGenerationMode,
720
733
  ): ResourceGenerationOptions {
734
+ if (mode === "feature") {
735
+ return {
736
+ auth: false,
737
+ tenant: false,
738
+ events: Boolean(options.events),
739
+ softDelete: false,
740
+ };
741
+ }
742
+
721
743
  if (mode !== "resource") {
722
744
  return { auth: false, tenant: false, events: false, softDelete: false };
723
745
  }
@@ -831,6 +853,28 @@ function normalizeMakeFeatureAddons(
831
853
  return featureAddonOrder.filter((addon) => normalized.has(addon));
832
854
  }
833
855
 
856
+ function makeFeatureRecipeAddons(
857
+ recipe: MakeFeatureRecipe | undefined,
858
+ ): MakeFeatureAddon[] {
859
+ if (recipe === "full-slice") {
860
+ return [
861
+ "policy",
862
+ "factories",
863
+ "seeds",
864
+ "tasks",
865
+ "events",
866
+ "listeners",
867
+ "jobs",
868
+ "notifications",
869
+ "schedules",
870
+ "ui",
871
+ "uploads",
872
+ ];
873
+ }
874
+
875
+ return [];
876
+ }
877
+
834
878
  const featureAddonOrder: readonly NormalizedMakeFeatureAddon[] = [
835
879
  "policy",
836
880
  "factory",
@@ -1462,8 +1506,11 @@ export async function makeListener(
1462
1506
  if (!options.skipEventAssert) {
1463
1507
  await assertListenerEventExists(targetDir, names.event, config);
1464
1508
  }
1509
+ await updateListenerProviderWiring(targetDir, names, config, {
1510
+ dryRun: true,
1511
+ });
1465
1512
 
1466
- return makeFeatureArtifact({
1513
+ const result = await makeFeatureArtifact({
1467
1514
  targetDir,
1468
1515
  config,
1469
1516
  force: Boolean(options.force),
@@ -1478,6 +1525,26 @@ export async function makeListener(
1478
1525
  "@beignet/core": beignetDependencyVersion,
1479
1526
  },
1480
1527
  });
1528
+
1529
+ const providerResult = await updateListenerProviderWiring(
1530
+ targetDir,
1531
+ names,
1532
+ config,
1533
+ {
1534
+ dryRun: Boolean(options.dryRun),
1535
+ },
1536
+ );
1537
+ if (providerResult === "created")
1538
+ result.createdFiles.push(providersFilePath(config));
1539
+ if (providerResult === "updated")
1540
+ result.updatedFiles.push(providersFilePath(config));
1541
+ if (providerResult === "skipped")
1542
+ result.skippedFiles.push(providersFilePath(config));
1543
+ if (!result.files.includes(providersFilePath(config))) {
1544
+ result.files.push(providersFilePath(config));
1545
+ }
1546
+
1547
+ return result;
1481
1548
  }
1482
1549
 
1483
1550
  async function assertListenerEventExists(
@@ -1496,6 +1563,98 @@ async function assertListenerEventExists(
1496
1563
  }
1497
1564
  }
1498
1565
 
1566
+ async function updateListenerProviderWiring(
1567
+ targetDir: string,
1568
+ names: ListenerNames,
1569
+ config: ResolvedBeignetConfig,
1570
+ options: { dryRun: boolean },
1571
+ ): Promise<WriteGeneratedFileResult> {
1572
+ const providersFile = providersFilePath(config);
1573
+ const filePath = path.join(targetDir, providersFile);
1574
+ const original = await readOptionalFile(filePath);
1575
+ if (original === undefined) {
1576
+ throw new Error(
1577
+ `Could not find the generated providers file ${providersFile}. Register listeners manually with registerListeners(...), or restore the generated providers file before running make listener.`,
1578
+ );
1579
+ }
1580
+
1581
+ const listenerIndex = featureArtifactIndexFile("listener", names, config);
1582
+ const providerName = `${names.featureSingularCamel}ListenersProvider`;
1583
+ let next = original;
1584
+ next = addNamedImport(next, "registerListeners", "@beignet/core/events");
1585
+ next = addNamedImport(next, "createServiceActor", "@beignet/core/ports");
1586
+ next = addNamedImport(next, "createProvider", "@beignet/core/providers");
1587
+ next = addNamedTypeImport(
1588
+ next,
1589
+ "AppContext",
1590
+ aliasModule(config.paths.appContext),
1591
+ );
1592
+ next = addNamedTypeImport(
1593
+ next,
1594
+ "AppServiceContextInput",
1595
+ aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
1596
+ );
1597
+ next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
1598
+
1599
+ const listenerImport = `import { ${listenerIndex.registryName} } from "${aliasModule(listenerIndex.path)}";`;
1600
+ if (!next.includes(listenerImport)) {
1601
+ next = insertAfterImports(next, listenerImport);
1602
+ }
1603
+
1604
+ if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
1605
+ const providerDefinition = `const ${providerName} = createProvider<
1606
+ \tPick<AppPorts, "eventBus" | "logger">,
1607
+ \tAppContext,
1608
+ \tAppServiceContextInput
1609
+ >()({
1610
+ \tname: "${names.feature.kebab}-listeners",
1611
+ \tsetup({ ports, createServiceContext }) {
1612
+ \t\tconst unregister = registerListeners(ports.eventBus, ${listenerIndex.registryName}, {
1613
+ \t\t\tctx: () =>
1614
+ \t\t\t\tcreateServiceContext({
1615
+ \t\t\t\t\tactor: createServiceActor("beignet-listener"),
1616
+ \t\t\t\t}),
1617
+ \t\t\tonError(error, listener) {
1618
+ \t\t\t\tports.logger.error("Event listener failed", {
1619
+ \t\t\t\t\terror,
1620
+ \t\t\t\t\tlistenerName: listener.name,
1621
+ \t\t\t\t});
1622
+ \t\t\t},
1623
+ \t\t});
1624
+
1625
+ \t\treturn {
1626
+ \t\t\tstop() {
1627
+ \t\t\t\tunregister();
1628
+ \t\t\t},
1629
+ \t\t};
1630
+ \t},
1631
+ });
1632
+ `;
1633
+ const withProvider = next.replace(
1634
+ /\nexport const providers = \[/,
1635
+ `\n${providerDefinition}\nexport const providers = [`,
1636
+ );
1637
+ if (withProvider === next) {
1638
+ throw new Error(
1639
+ `Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`,
1640
+ );
1641
+ }
1642
+ next = withProvider;
1643
+ }
1644
+
1645
+ const appended = appendToNamedArray(next, "providers", providerName);
1646
+ if (appended.kind === "missing") {
1647
+ throw new Error(
1648
+ `Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`,
1649
+ );
1650
+ }
1651
+ if (appended.kind === "updated") next = appended.source;
1652
+
1653
+ if (next === original) return "skipped";
1654
+ if (!options.dryRun) await writeFile(filePath, next);
1655
+ return "updated";
1656
+ }
1657
+
1499
1658
  export async function makeSchedule(
1500
1659
  options: MakeScheduleOptions,
1501
1660
  ): Promise<MakeScheduleResult> {
@@ -1570,6 +1729,12 @@ const cronSecretEnvEntry = `\t\tCRON_SECRET: z.string().min(1).optional(),`;
1570
1729
  // slots in directly below it, matching the documented env shape.
1571
1730
  const cronSecretEnvAnchor = `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`;
1572
1731
 
1732
+ type FileChangeSet = {
1733
+ createdFiles: string[];
1734
+ updatedFiles: string[];
1735
+ skippedFiles: string[];
1736
+ };
1737
+
1573
1738
  type CronSecretWiringPlan = {
1574
1739
  files: Array<GeneratedFile & { result: "created" | "updated" }>;
1575
1740
  skippedFiles: string[];
@@ -1703,23 +1868,58 @@ async function updateOutboxPortWiring(
1703
1868
  targetDir: string,
1704
1869
  config: ResolvedBeignetConfig,
1705
1870
  options: { dryRun: boolean },
1706
- ): Promise<string[]> {
1707
- const updated = new Set<string>();
1871
+ ): Promise<FileChangeSet> {
1872
+ const changes: FileChangeSet = {
1873
+ createdFiles: [],
1874
+ updatedFiles: [],
1875
+ skippedFiles: [],
1876
+ };
1708
1877
 
1709
1878
  if (await updateOutboxPorts(targetDir, config, options)) {
1710
- updated.add(config.paths.ports);
1879
+ changes.updatedFiles.push(config.paths.ports);
1711
1880
  }
1712
1881
  if (await updateOutboxInfrastructurePorts(targetDir, config, options)) {
1713
- updated.add(config.paths.infrastructurePorts);
1882
+ changes.updatedFiles.push(config.paths.infrastructurePorts);
1714
1883
  }
1715
1884
  if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
1716
- updated.add(path.join(infrastructureDir(config), "db/provider.ts"));
1885
+ changes.updatedFiles.push(
1886
+ path.join(infrastructureDir(config), "db/provider.ts"),
1887
+ );
1717
1888
  }
1718
1889
  if (await updateOutboxRepositoriesReturnType(targetDir, config, options)) {
1719
- updated.add(drizzleRepositoriesPath(config));
1890
+ changes.updatedFiles.push(drizzleRepositoriesPath(config));
1720
1891
  }
1721
1892
 
1722
- return [...updated];
1893
+ const schemaResult = await updateOutboxDrizzleSchema(
1894
+ targetDir,
1895
+ config,
1896
+ options,
1897
+ );
1898
+ if (schemaResult.result === "created") {
1899
+ changes.createdFiles.push(schemaResult.path);
1900
+ }
1901
+ if (schemaResult.result === "updated") {
1902
+ changes.updatedFiles.push(schemaResult.path);
1903
+ }
1904
+ if (schemaResult.result === "skipped") {
1905
+ changes.skippedFiles.push(schemaResult.path);
1906
+ }
1907
+ if (schemaResult.canExport) {
1908
+ const schemaIndexUpdated = await updateOutboxDrizzleSchemaIndex(
1909
+ targetDir,
1910
+ config,
1911
+ options,
1912
+ );
1913
+ if (schemaIndexUpdated) {
1914
+ changes.updatedFiles.push(drizzleSchemaIndexPath(config));
1915
+ }
1916
+ }
1917
+
1918
+ return {
1919
+ createdFiles: uniqueStrings(changes.createdFiles),
1920
+ updatedFiles: uniqueStrings(changes.updatedFiles),
1921
+ skippedFiles: uniqueStrings(changes.skippedFiles),
1922
+ };
1723
1923
  }
1724
1924
 
1725
1925
  async function applyOutboxPortWiring(
@@ -1728,11 +1928,19 @@ async function applyOutboxPortWiring(
1728
1928
  config: ResolvedBeignetConfig,
1729
1929
  options: { dryRun: boolean },
1730
1930
  ): Promise<void> {
1731
- const updatedFiles = await updateOutboxPortWiring(targetDir, config, options);
1732
- for (const file of updatedFiles) {
1931
+ const changes = await updateOutboxPortWiring(targetDir, config, options);
1932
+ for (const file of changes.createdFiles) {
1933
+ if (!result.createdFiles.includes(file)) result.createdFiles.push(file);
1934
+ if (!result.files.includes(file)) result.files.push(file);
1935
+ }
1936
+ for (const file of changes.updatedFiles) {
1733
1937
  if (!result.updatedFiles.includes(file)) result.updatedFiles.push(file);
1734
1938
  if (!result.files.includes(file)) result.files.push(file);
1735
1939
  }
1940
+ for (const file of changes.skippedFiles) {
1941
+ if (!result.skippedFiles.includes(file)) result.skippedFiles.push(file);
1942
+ if (!result.files.includes(file)) result.files.push(file);
1943
+ }
1736
1944
  }
1737
1945
 
1738
1946
  async function updateOutboxPorts(
@@ -1862,6 +2070,61 @@ async function updateOutboxRepositoriesReturnType(
1862
2070
  return true;
1863
2071
  }
1864
2072
 
2073
+ async function updateOutboxDrizzleSchema(
2074
+ targetDir: string,
2075
+ config: ResolvedBeignetConfig,
2076
+ options: { dryRun: boolean },
2077
+ ): Promise<{
2078
+ path: string;
2079
+ result?: WriteGeneratedFileResult;
2080
+ canExport: boolean;
2081
+ }> {
2082
+ const indexPath = path.join(targetDir, drizzleSchemaIndexPath(config));
2083
+ const schemaPath = outboxDrizzleSchemaFilePath(config);
2084
+ const destination = path.join(targetDir, schemaPath);
2085
+ if (!(await fileExists(indexPath))) {
2086
+ return { path: schemaPath, canExport: false };
2087
+ }
2088
+
2089
+ const database = await detectResourceDatabase(targetDir, config);
2090
+ const content = outboxDrizzleSchemaFile(drizzleDialects[database]);
2091
+ const existing = await readOptionalFile(destination);
2092
+
2093
+ if (existing === undefined) {
2094
+ if (!options.dryRun) {
2095
+ await mkdir(path.dirname(destination), { recursive: true });
2096
+ await writeFile(destination, content);
2097
+ }
2098
+ return { path: schemaPath, result: "created", canExport: true };
2099
+ }
2100
+
2101
+ if (
2102
+ existing === content ||
2103
+ /\bexport\s+const\s+outboxMessages\b/.test(existing)
2104
+ ) {
2105
+ return { path: schemaPath, result: "skipped", canExport: true };
2106
+ }
2107
+
2108
+ return { path: schemaPath, result: "skipped", canExport: false };
2109
+ }
2110
+
2111
+ async function updateOutboxDrizzleSchemaIndex(
2112
+ targetDir: string,
2113
+ config: ResolvedBeignetConfig,
2114
+ options: { dryRun: boolean },
2115
+ ): Promise<boolean> {
2116
+ const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
2117
+ if (!(await fileExists(filePath))) return false;
2118
+
2119
+ const original = await readFile(filePath, "utf8");
2120
+ const exportLine = 'export { outboxMessages } from "./outbox-messages";';
2121
+ if (original.includes(exportLine)) return false;
2122
+
2123
+ const next = `${original.trimEnd()}\n${exportLine}\n`;
2124
+ if (!options.dryRun) await writeFile(filePath, next);
2125
+ return true;
2126
+ }
2127
+
1865
2128
  function drizzleOutboxPortFactoryName(database: DatabaseName): string {
1866
2129
  if (database === "postgres") return "createDrizzlePostgresOutboxPort";
1867
2130
  if (database === "mysql") return "createDrizzleMysqlOutboxPort";
@@ -2267,11 +2530,12 @@ export async function makeOutbox(
2267
2530
  createdFiles.push(...cronSecretResult.createdFiles);
2268
2531
  updatedFiles.push(...cronSecretResult.updatedFiles);
2269
2532
  skippedFiles.push(...cronSecretResult.skippedFiles);
2270
- updatedFiles.push(
2271
- ...(await updateOutboxPortWiring(targetDir, config, {
2272
- dryRun: Boolean(options.dryRun),
2273
- })),
2274
- );
2533
+ const outboxWiringResult = await updateOutboxPortWiring(targetDir, config, {
2534
+ dryRun: Boolean(options.dryRun),
2535
+ });
2536
+ createdFiles.push(...outboxWiringResult.createdFiles);
2537
+ updatedFiles.push(...outboxWiringResult.updatedFiles);
2538
+ skippedFiles.push(...outboxWiringResult.skippedFiles);
2275
2539
 
2276
2540
  return {
2277
2541
  schemaVersion: 1,
@@ -2286,6 +2550,8 @@ export async function makeOutbox(
2286
2550
  config.paths.infrastructurePorts,
2287
2551
  path.join(infrastructureDir(config), "db/provider.ts"),
2288
2552
  drizzleRepositoriesPath(config),
2553
+ outboxDrizzleSchemaFilePath(config),
2554
+ drizzleSchemaIndexPath(config),
2289
2555
  ],
2290
2556
  createdFiles: uniqueStrings(createdFiles),
2291
2557
  updatedFiles: uniqueStrings(updatedFiles),
@@ -5456,9 +5722,14 @@ function featureUiComponentFiles(
5456
5722
  names: FeatureUiNames,
5457
5723
  config: ResolvedBeignetConfig,
5458
5724
  ): GeneratedFile[] {
5725
+ const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
5459
5726
  const componentPath = featureUiComponentFilePath(names, config);
5460
5727
 
5461
5728
  return [
5729
+ {
5730
+ path: clientQueriesPath,
5731
+ content: featureUiClientQueriesFile(names, config),
5732
+ },
5462
5733
  {
5463
5734
  path: componentPath,
5464
5735
  content: featureUiComponentFile(names, config),
@@ -5721,6 +5992,15 @@ function drizzleResourceSchemaFilePath(
5721
5992
  );
5722
5993
  }
5723
5994
 
5995
+ function outboxDrizzleSchemaFilePath(config: ResolvedBeignetConfig): string {
5996
+ return path.join(
5997
+ infrastructureDir(config),
5998
+ "db",
5999
+ "schema",
6000
+ "outbox-messages.ts",
6001
+ );
6002
+ }
6003
+
5724
6004
  function drizzleResourceRepositoryFilePath(
5725
6005
  names: ResourceNames,
5726
6006
  config: ResolvedBeignetConfig,
@@ -5826,6 +6106,20 @@ function featureUiDir(
5826
6106
  return path.join(resourceFeatureDir(names, config), "components");
5827
6107
  }
5828
6108
 
6109
+ function featureUiClientDir(
6110
+ names: FeatureUiNames,
6111
+ config: ResolvedBeignetConfig,
6112
+ ): string {
6113
+ return path.join(resourceFeatureDir(names, config), "client");
6114
+ }
6115
+
6116
+ function featureUiClientQueriesFilePath(
6117
+ names: FeatureUiNames,
6118
+ config: ResolvedBeignetConfig,
6119
+ ): string {
6120
+ return path.join(featureUiClientDir(names, config), "queries.ts");
6121
+ }
6122
+
5829
6123
  function featureUiComponentFilePath(
5830
6124
  names: FeatureUiNames,
5831
6125
  config: ResolvedBeignetConfig,
@@ -6894,7 +7188,7 @@ function listUseCaseFile(
6894
7188
  `list-${names.pluralKebab}.ts`,
6895
7189
  );
6896
7190
 
6897
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
7191
+ return `import "@beignet/core/server-only";
6898
7192
  import { normalizeCursorPage } from "@beignet/core/pagination";
6899
7193
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
6900
7194
  import {
@@ -6934,7 +7228,7 @@ function createUseCaseFile(
6934
7228
  `create-${names.singularKebab}.ts`,
6935
7229
  );
6936
7230
 
6937
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
7231
+ return `import "@beignet/core/server-only";
6938
7232
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
6939
7233
  import {
6940
7234
  Create${names.singularPascal}InputSchema,
@@ -6966,7 +7260,7 @@ function getUseCaseFile(
6966
7260
  `get-${names.singularKebab}.ts`,
6967
7261
  );
6968
7262
 
6969
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
7263
+ return `import "@beignet/core/server-only";
6970
7264
  import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
6971
7265
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
6972
7266
  import {
@@ -7001,7 +7295,7 @@ function updateUseCaseFile(
7001
7295
  `update-${names.singularKebab}.ts`,
7002
7296
  );
7003
7297
 
7004
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
7298
+ return `import "@beignet/core/server-only";
7005
7299
  import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
7006
7300
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
7007
7301
  ${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
@@ -7050,7 +7344,7 @@ function deleteUseCaseFile(
7050
7344
  `delete-${names.singularKebab}.ts`,
7051
7345
  );
7052
7346
 
7053
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
7347
+ return `import "@beignet/core/server-only";
7054
7348
  import { z } from "zod";
7055
7349
  import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
7056
7350
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
@@ -7160,7 +7454,7 @@ function inMemoryRepositoryFile(
7160
7454
  return true;`
7161
7455
  : `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== filter.tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
7162
7456
 
7163
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
7457
+ return `import "@beignet/core/server-only";
7164
7458
  import { cursorPageResult } from "@beignet/core/pagination";
7165
7459
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
7166
7460
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -7285,6 +7579,78 @@ ${options.softDelete ? `\tdeletedAt: ${dialect.timestampColumn("deleted_at")},\n
7285
7579
  `;
7286
7580
  }
7287
7581
 
7582
+ function outboxDrizzleSchemaFile(dialect: DrizzleDialect): string {
7583
+ if (dialect.subpath === "mysql") {
7584
+ return `import { index, int, longtext, mysqlTable, varchar } from "drizzle-orm/mysql-core";
7585
+
7586
+ export const outboxMessages = mysqlTable(
7587
+ \t"outbox_messages",
7588
+ \t{
7589
+ \t\tid: varchar("id", { length: 255 }).primaryKey(),
7590
+ \t\tkind: varchar("kind", { length: 32 }).notNull(),
7591
+ \t\tname: varchar("name", { length: 255 }).notNull(),
7592
+ \t\tpayloadJson: longtext("payload_json").notNull(),
7593
+ \t\tstatus: varchar("status", { length: 32 }).notNull(),
7594
+ \t\tattempts: int("attempts").notNull().default(0),
7595
+ \t\tmaxAttempts: int("max_attempts").notNull().default(3),
7596
+ \t\tavailableAt: varchar("available_at", { length: 32 }).notNull(),
7597
+ \t\tclaimedAt: varchar("claimed_at", { length: 32 }),
7598
+ \t\tlockedUntil: varchar("locked_until", { length: 32 }),
7599
+ \t\tclaimToken: varchar("claim_token", { length: 64 }),
7600
+ \t\tdeliveredAt: varchar("delivered_at", { length: 32 }),
7601
+ \t\tlastErrorJson: longtext("last_error_json"),
7602
+ \t\tcreatedAt: varchar("created_at", { length: 32 }).notNull(),
7603
+ \t\tupdatedAt: varchar("updated_at", { length: 32 }).notNull(),
7604
+ \t},
7605
+ \t(table) => ({
7606
+ \t\tavailableIdx: index("outbox_messages_available_idx").on(
7607
+ \t\t\ttable.status,
7608
+ \t\t\ttable.availableAt,
7609
+ \t\t),
7610
+ \t\tlockedIdx: index("outbox_messages_locked_idx").on(
7611
+ \t\t\ttable.status,
7612
+ \t\t\ttable.lockedUntil,
7613
+ \t\t),
7614
+ \t}),
7615
+ );
7616
+ `;
7617
+ }
7618
+
7619
+ return `import { index, integer, ${dialect.tableFunction}, text } from "${dialect.columnModule}";
7620
+
7621
+ export const outboxMessages = ${dialect.tableFunction}(
7622
+ \t"outbox_messages",
7623
+ \t{
7624
+ \t\tid: text("id").primaryKey(),
7625
+ \t\tkind: text("kind").notNull(),
7626
+ \t\tname: text("name").notNull(),
7627
+ \t\tpayloadJson: text("payload_json").notNull(),
7628
+ \t\tstatus: text("status").notNull(),
7629
+ \t\tattempts: integer("attempts").notNull().default(0),
7630
+ \t\tmaxAttempts: integer("max_attempts").notNull().default(3),
7631
+ \t\tavailableAt: text("available_at").notNull(),
7632
+ \t\tclaimedAt: text("claimed_at"),
7633
+ \t\tlockedUntil: text("locked_until"),
7634
+ \t\tclaimToken: text("claim_token"),
7635
+ \t\tdeliveredAt: text("delivered_at"),
7636
+ \t\tlastErrorJson: text("last_error_json"),
7637
+ \t\tcreatedAt: text("created_at").notNull(),
7638
+ \t\tupdatedAt: text("updated_at").notNull(),
7639
+ \t},
7640
+ \t(table) => ({
7641
+ \t\tavailableIdx: index("outbox_messages_available_idx").on(
7642
+ \t\t\ttable.status,
7643
+ \t\t\ttable.availableAt,
7644
+ \t\t),
7645
+ \t\tlockedIdx: index("outbox_messages_locked_idx").on(
7646
+ \t\t\ttable.status,
7647
+ \t\t\ttable.lockedUntil,
7648
+ \t\t),
7649
+ \t}),
7650
+ );
7651
+ `;
7652
+ }
7653
+
7288
7654
  function drizzleResourceWhere(
7289
7655
  names: ResourceNames,
7290
7656
  options: ResourceGenerationOptions,
@@ -7467,7 +7833,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
7467
7833
  },
7468
7834
  `;
7469
7835
 
7470
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
7836
+ return `import "@beignet/core/server-only";
7471
7837
  import { cursorPageResult } from "@beignet/core/pagination";
7472
7838
  import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
7473
7839
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
@@ -7702,7 +8068,7 @@ function standaloneUseCaseFile(
7702
8068
  config: ResolvedBeignetConfig,
7703
8069
  filePath: string,
7704
8070
  ): string {
7705
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
8071
+ return `import "@beignet/core/server-only";
7706
8072
  import { z } from "zod";
7707
8073
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
7708
8074
 
@@ -9569,30 +9935,31 @@ function featureUiComponentFile(
9569
9935
  names: FeatureUiNames,
9570
9936
  config: ResolvedBeignetConfig,
9571
9937
  ): string {
9938
+ const componentPath = featureUiComponentFilePath(names, config);
9939
+ const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
9940
+
9572
9941
  return `"use client";
9573
9942
 
9574
9943
  import { contractErrorMessage } from "@beignet/core/client";
9575
9944
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
9576
9945
  import { useState } from "react";
9577
- import { rq } from "${aliasModule(clientIndexPath(config))}";
9578
- import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(
9579
- resourceContractFilePath(names, config),
9580
- )}";
9946
+ import {
9947
+ \tcreate${names.singularPascal}MutationOptions,
9948
+ \tinvalidate${names.pluralPascal},
9949
+ \tlist${names.pluralPascal}QueryOptions,
9950
+ } from "${relativeModule(componentPath, clientQueriesPath)}";
9581
9951
 
9582
9952
  export function ${names.componentExportName}() {
9583
9953
  \tconst [name, setName] = useState("");
9584
9954
  \tconst queryClient = useQueryClient();
9585
- \tconst ${names.pluralCamel}Query = useQuery(
9586
- \t\trq(list${names.pluralPascal}).queryOptions({ query: {} }),
9587
- \t);
9588
- \tconst create${names.singularPascal}Mutation = useMutation(
9589
- \t\trq(create${names.singularPascal}).mutationOptions({
9590
- \t\t\tonSuccess: async () => {
9591
- \t\t\t\tsetName("");
9592
- \t\t\t\tawait rq(list${names.pluralPascal}).invalidate(queryClient);
9593
- \t\t\t},
9594
- \t\t}),
9595
- \t);
9955
+ \tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
9956
+ \tconst create${names.singularPascal}Mutation = useMutation({
9957
+ \t\t...create${names.singularPascal}MutationOptions(),
9958
+ \t\tonSuccess: async () => {
9959
+ \t\t\tsetName("");
9960
+ \t\t\tawait invalidate${names.pluralPascal}(queryClient);
9961
+ \t\t},
9962
+ \t});
9596
9963
 
9597
9964
  \treturn (
9598
9965
  \t\t<section className="${names.pluralKebab}-panel">
@@ -9660,6 +10027,34 @@ export function ${names.componentExportName}() {
9660
10027
  `;
9661
10028
  }
9662
10029
 
10030
+ function featureUiClientQueriesFile(
10031
+ names: FeatureUiNames,
10032
+ config: ResolvedBeignetConfig,
10033
+ ): string {
10034
+ const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
10035
+ const contractsPath = resourceContractFilePath(names, config);
10036
+
10037
+ return `import type { QueryClient } from "@tanstack/react-query";
10038
+ import { rq } from "${aliasModule(clientIndexPath(config))}";
10039
+ import {
10040
+ \tcreate${names.singularPascal},
10041
+ \tlist${names.pluralPascal},
10042
+ } from "${relativeModule(clientQueriesPath, contractsPath)}";
10043
+
10044
+ export function list${names.pluralPascal}QueryOptions() {
10045
+ \treturn rq(list${names.pluralPascal}).queryOptions({ query: {} });
10046
+ }
10047
+
10048
+ export function create${names.singularPascal}MutationOptions() {
10049
+ \treturn rq(create${names.singularPascal}).mutationOptions();
10050
+ }
10051
+
10052
+ export function invalidate${names.pluralPascal}(queryClient: QueryClient) {
10053
+ \treturn rq(list${names.pluralPascal}).invalidate(queryClient);
10054
+ }
10055
+ `;
10056
+ }
10057
+
9663
10058
  function scheduleRouteFile(
9664
10059
  names: ScheduleNames,
9665
10060
  config: ResolvedBeignetConfig,
@@ -9706,7 +10101,7 @@ function routeGroupFile(
9706
10101
  const contractsPath = resourceContractFilePath(names, config);
9707
10102
  const useCasesPath = resourceUseCaseIndexPath(names, config);
9708
10103
 
9709
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
10104
+ return `import "@beignet/core/server-only";
9710
10105
  import { defineRouteGroup } from "@beignet/next";
9711
10106
  import type { AppContext } from "${relativeModule(routeFilePath, config.paths.appContext)}";
9712
10107
  import {
@@ -9743,12 +10138,12 @@ function testFile(
9743
10138
  path.dirname(config.paths.server),
9744
10139
  "context.ts",
9745
10140
  );
9746
- const requestTarget = options.tenant ? "requester" : "app";
10141
+ const requestTarget = "app";
9747
10142
 
9748
10143
  return `import { describe, expect, it } from "bun:test";
9749
10144
  import { createUseCaseTester } from "@beignet/core/application";
9750
10145
  ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` : ""}import { defineRoutes } from "@beignet/web";
9751
- import { createTestApp${options.tenant ? ", createTestRequester" : ""} } from "@beignet/web/testing";
10146
+ import { createTestApp } from "@beignet/web/testing";
9752
10147
  import { createInMemoryDevtools } from "@beignet/devtools";
9753
10148
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
9754
10149
  import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/ports/testing";
@@ -9797,13 +10192,15 @@ ${options.tenant ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",
9797
10192
  overrides: {
9798
10193
  ${names.pluralCamel},
9799
10194
  ${
9800
- options.auth
10195
+ options.auth || options.tenant
9801
10196
  ? `\t\t\t\tauth: {
9802
10197
  getSession: async () => ({
9803
- user: { id: "user_test", name: "Test User" },
10198
+ user: { id: "user_test", name: "Test User"${options.tenant ? `, tenantId: "tenant_test"` : ""} },
10199
+ ${options.tenant ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
9804
10200
  }),
9805
10201
  },
9806
- gate: appPorts.gate,\n`
10202
+ ${options.auth ? `\t\t\t\tgate: appPorts.gate,\n` : ""}
10203
+ `
9807
10204
  : `\t\t\t\tauth: {
9808
10205
  getSession: async () => null,
9809
10206
  },\n`
@@ -9953,7 +10350,6 @@ ${
9953
10350
  routes: defineRoutes<AppContext>([${names.singularCamel}Routes]),
9954
10351
  context: appContext,
9955
10352
  });
9956
- ${options.tenant ? `\t\tconst requester = createTestRequester(app, {\n\t\t\theaders: { "x-tenant-id": "tenant_test" },\n\t\t});\n` : ""}
9957
10353
  const createdViaRoute = await ${requestTarget}.request(create${names.singularPascal}, {
9958
10354
  body: { name: "Second ${names.singularPascal}" },
9959
10355
  });