@beignet/cli 0.0.26 → 0.0.27

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 (75) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +29 -10
  3. package/dist/choices.d.ts +8 -0
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +14 -0
  6. package/dist/choices.js.map +1 -1
  7. package/dist/db.js +2 -2
  8. package/dist/db.js.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +109 -13
  11. package/dist/index.js.map +1 -1
  12. package/dist/inspect.js +51 -3
  13. package/dist/inspect.js.map +1 -1
  14. package/dist/make/inbox.d.ts +63 -0
  15. package/dist/make/inbox.d.ts.map +1 -0
  16. package/dist/make/inbox.js +1747 -0
  17. package/dist/make/inbox.js.map +1 -0
  18. package/dist/make/payments.d.ts +61 -0
  19. package/dist/make/payments.d.ts.map +1 -0
  20. package/dist/make/payments.js +1900 -0
  21. package/dist/make/payments.js.map +1 -0
  22. package/dist/make/shared.d.ts +575 -0
  23. package/dist/make/shared.d.ts.map +1 -0
  24. package/dist/make/shared.js +2166 -0
  25. package/dist/make/shared.js.map +1 -0
  26. package/dist/make/tenancy.d.ts +101 -0
  27. package/dist/make/tenancy.d.ts.map +1 -0
  28. package/dist/make/tenancy.js +4409 -0
  29. package/dist/make/tenancy.js.map +1 -0
  30. package/dist/make.d.ts +8 -44
  31. package/dist/make.d.ts.map +1 -1
  32. package/dist/make.js +71 -3659
  33. package/dist/make.js.map +1 -1
  34. package/dist/mcp.d.ts.map +1 -1
  35. package/dist/mcp.js +44 -3
  36. package/dist/mcp.js.map +1 -1
  37. package/dist/provider-add.d.ts +19 -0
  38. package/dist/provider-add.d.ts.map +1 -0
  39. package/dist/provider-add.js +691 -0
  40. package/dist/provider-add.js.map +1 -0
  41. package/dist/registry-edits.d.ts +9 -0
  42. package/dist/registry-edits.d.ts.map +1 -1
  43. package/dist/registry-edits.js +32 -0
  44. package/dist/registry-edits.js.map +1 -1
  45. package/dist/task.d.ts +2 -0
  46. package/dist/task.d.ts.map +1 -1
  47. package/dist/task.js +2 -0
  48. package/dist/task.js.map +1 -1
  49. package/dist/templates/agents.js +2 -2
  50. package/dist/templates/server.d.ts.map +1 -1
  51. package/dist/templates/server.js +44 -37
  52. package/dist/templates/server.js.map +1 -1
  53. package/dist/templates/shared.d.ts +5 -0
  54. package/dist/templates/shared.d.ts.map +1 -1
  55. package/dist/templates/shared.js +5 -0
  56. package/dist/templates/shared.js.map +1 -1
  57. package/package.json +2 -2
  58. package/skills/app-structure/SKILL.md +1 -1
  59. package/src/choices.ts +28 -0
  60. package/src/db.ts +2 -2
  61. package/src/index.ts +170 -13
  62. package/src/inspect.ts +68 -2
  63. package/src/make/inbox.ts +2015 -0
  64. package/src/make/payments.ts +2182 -0
  65. package/src/make/shared.ts +3550 -0
  66. package/src/make/tenancy.ts +4809 -0
  67. package/src/make.ts +380 -5289
  68. package/src/mcp.ts +65 -2
  69. package/src/provider-add.ts +926 -0
  70. package/src/registry-edits.ts +47 -0
  71. package/src/task.ts +8 -10
  72. package/src/templates/agents.ts +2 -2
  73. package/src/templates/server.ts +44 -37
  74. package/src/templates/shared.ts +5 -0
  75. package/src/test-helpers/generated-app.ts +168 -0
@@ -94,6 +94,53 @@ export function insertAfterImports(source: string, line: string): string {
94
94
  return lines.join("\n");
95
95
  }
96
96
 
97
+ /**
98
+ * Add a name to an `import { ... } from "<module>";` value import, creating
99
+ * the import when the module is not imported yet. Names are inserted in
100
+ * sorted position and single-line versus multiline formatting is preserved.
101
+ *
102
+ * Mirrors the `addNamedTypeImport` helper for value imports; type-only
103
+ * imports (`import type { ... }`) from the same module are left untouched.
104
+ */
105
+ export function addNamedValueImport(
106
+ source: string,
107
+ name: string,
108
+ moduleName: string,
109
+ ): string {
110
+ const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
+ const importPattern = new RegExp(
112
+ `import \\{([^}]*)\\} from "${escapedModule}";`,
113
+ );
114
+ const match = importPattern.exec(source);
115
+ if (!match) {
116
+ return insertAfterImports(
117
+ source,
118
+ `import { ${name} } from "${moduleName}";`,
119
+ );
120
+ }
121
+
122
+ const names = match[1]
123
+ .split(",")
124
+ .map((entry) => entry.trim())
125
+ .filter(Boolean);
126
+ if (names.includes(name)) return source;
127
+
128
+ const insertIndex = names.findIndex(
129
+ (existing) => existing.localeCompare(name) > 0,
130
+ );
131
+ if (insertIndex === -1) names.push(name);
132
+ else names.splice(insertIndex, 0, name);
133
+
134
+ const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
135
+ const replacement = match[1].includes("\n")
136
+ ? `import {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
137
+ : `import { ${names.join(", ")} } from "${moduleName}";`;
138
+
139
+ return `${source.slice(0, match.index)}${replacement}${source.slice(
140
+ match.index + match[0].length,
141
+ )}`;
142
+ }
143
+
97
144
  /**
98
145
  * Collect every identifier referenced inside an array (or object) expression.
99
146
  */
package/src/task.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import type { TaskDef } from "@beignet/core/tasks";
3
+ import type { TaskDef, TaskRunContextArgs } from "@beignet/core/tasks";
4
4
  import { createJiti } from "jiti";
5
5
  import { loadBeignetConfig, normalizePath } from "./config.js";
6
6
 
@@ -11,6 +11,7 @@ export type RunAppTaskOptions = {
11
11
  name: string;
12
12
  cwd?: string;
13
13
  input?: string | Record<string, unknown>;
14
+ tenant?: string;
14
15
  modulePath?: string;
15
16
  };
16
17
 
@@ -23,20 +24,15 @@ export type RunAppTaskResult = {
23
24
  cwd: string;
24
25
  modulePath: string;
25
26
  input: unknown;
27
+ tenant?: string;
26
28
  output: unknown;
27
29
  durationMs: number;
28
30
  };
29
31
 
30
- type TaskContextArgs = {
31
- task: TaskDef;
32
- taskName: string;
33
- input: unknown;
34
- };
35
-
36
32
  type TaskModule = {
37
33
  tasks?: readonly unknown[];
38
- createTaskContext?: (args: TaskContextArgs) => unknown;
39
- stopTaskContext?: (ctx: unknown, args: TaskContextArgs) => unknown;
34
+ createTaskContext?: (args: TaskRunContextArgs) => unknown;
35
+ stopTaskContext?: (ctx: unknown, args: TaskRunContextArgs) => unknown;
40
36
  };
41
37
 
42
38
  /**
@@ -56,10 +52,11 @@ export async function runAppTask(
56
52
  // `beignet lint`, work without a built @beignet/core.
57
53
  const { parseTaskInput } = await import("@beignet/core/tasks");
58
54
  const parsedInput = await parseTaskInput(task, rawInput);
59
- const contextArgs = {
55
+ const contextArgs: TaskRunContextArgs = {
60
56
  task,
61
57
  taskName: task.name,
62
58
  input: parsedInput,
59
+ tenant: options.tenant,
63
60
  };
64
61
  const ctx = taskModule.createTaskContext
65
62
  ? await taskModule.createTaskContext(contextArgs)
@@ -78,6 +75,7 @@ export async function runAppTask(
78
75
  cwd,
79
76
  modulePath,
80
77
  input: parsedInput,
78
+ ...(options.tenant !== undefined ? { tenant: options.tenant } : {}),
81
79
  output,
82
80
  durationMs: Math.round(performance.now() - startedAt),
83
81
  };
@@ -118,9 +118,9 @@ skill-loading block.
118
118
  ## MCP server
119
119
 
120
120
  \`.mcp.json\` registers the app-local \`@beignet/cli\` bin at
121
- \`./node_modules/.bin/beignet mcp\`, which exposes routes/doctor/lint/make as
121
+ \`./node_modules/.bin/beignet mcp\`, which exposes routes/doctor/lint/make/provider_add as
122
122
  structured tools named exactly: \`routes\`, \`doctor\`, \`doctor_fix\`,
123
- \`lint\`, \`make\`. Clients that do not read \`.mcp.json\` can use the same
123
+ \`lint\`, \`make\`, \`provider_add\`. Clients that do not read \`.mcp.json\` can use the same
124
124
  command from the app root; use \`${cli} mcp\` only for terminal debugging.
125
125
  `;
126
126
  }
@@ -296,16 +296,18 @@ export function server(ctx: TemplateContext): string {
296
296
  \t\t},`;
297
297
 
298
298
  return `import "@beignet/core/server-only";
299
- import { createNextServer } from "@beignet/next";
299
+ import { createNextServer, createNextServerLoader } from "@beignet/next";
300
300
  import { createErrorReportingHooks, createIdempotencyHooks } from "@beignet/core/server";
301
301
  import type { AppContext } from "@/app-context";
302
302
  import { appPorts } from "@/infra/app-ports";
303
303
  import { env } from "@/lib/env";
304
304
  import { appContext } from "./context";
305
- import { providers } from "./providers";
306
305
  import { routes } from "./routes";
307
306
 
308
- export const server = await createNextServer({
307
+ export const getServer = createNextServerLoader(async () => {
308
+ const { providers } = await import("./providers");
309
+
310
+ return createNextServer({
309
311
  ports: appPorts,
310
312
  providers,
311
313
  providerConfig: {
@@ -333,6 +335,7 @@ ${providerConfigEntries}
333
335
  },
334
336
  };
335
337
  },
338
+ });
336
339
  });
337
340
  `;
338
341
  }
@@ -557,15 +560,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
557
560
  return typeof value === "object" && value !== null;
558
561
  }
559
562
  `,
560
- apiCatchAllRoute: `import { server } from "@/server";
561
-
562
- export const GET = server.api;
563
- export const HEAD = server.api;
564
- export const OPTIONS = server.api;
565
- export const PATCH = server.api;
566
- export const POST = server.api;
567
- export const PUT = server.api;
568
- export const DELETE = server.api;
563
+ apiCatchAllRoute: `import { createApiRoute } from "@beignet/next";
564
+ import { getServer } from "@/server";
565
+
566
+ export const { DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT } =
567
+ createApiRoute(getServer);
569
568
  `,
570
569
  healthRoute: `import { env } from "@/lib/env";
571
570
 
@@ -584,12 +583,12 @@ export function GET() {
584
583
  );
585
584
  }
586
585
  `,
587
- readyRoute: `import { createHealthHandler } from "@beignet/core/server";
586
+ readyRoute: `import { createHealthRoute } from "@beignet/next";
588
587
  import { env } from "@/lib/env";
589
- import { server } from "@/server";
588
+ import { getServer } from "@/server";
590
589
 
591
- const readiness = createHealthHandler(
592
- server.ports,
590
+ export const { GET } = createHealthRoute(
591
+ getServer,
593
592
  {
594
593
  checks: {
595
594
  database: (ports) => ports.db.checkHealth(),
@@ -598,42 +597,50 @@ const readiness = createHealthHandler(
598
597
  },
599
598
  env.NODE_ENV,
600
599
  );
601
-
602
- export async function GET(request: Request) {
603
- const response = await readiness(request);
604
-
605
- return Response.json(response.body, {
606
- status: response.status,
607
- headers: {
608
- ...response.headers,
609
- "cache-control": "no-store",
610
- },
611
- });
612
- }
613
600
  `,
614
601
  devtoolsRoute: `import { createDevtoolsRoute } from "@beignet/devtools";
615
602
  import { env } from "@/lib/env";
616
- import { server } from "@/server";
603
+ import { getServer } from "@/server";
617
604
 
618
- export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
619
- basePath: "/api/devtools",
620
- enabled: env.DEVTOOLS_ENABLED,
621
- });
605
+ export const { GET, POST } = createDevtoolsRoute(
606
+ async () => (await getServer()).ports.devtools,
607
+ {
608
+ basePath: "/api/devtools",
609
+ enabled: env.DEVTOOLS_ENABLED,
610
+ },
611
+ );
622
612
  `,
623
613
  openApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
624
614
  import { env } from "@/lib/env";
625
- import { server } from "@/server";
615
+ import { contracts } from "@/server/routes";
626
616
 
627
- export const GET = createOpenAPIHandler(server.contracts, {
617
+ export const GET = createOpenAPIHandler(contracts, {
628
618
  title: "Beignet starter API",
629
619
  version: "0.1.0",
630
620
  servers: [{ url: env.APP_URL }],
631
621
  });
632
622
  `,
633
623
  authRoute: `import { toNextJsHandler } from "better-auth/next-js";
634
- import { auth } from "@/lib/better-auth";
635
624
 
636
- export const { GET, POST } = toNextJsHandler(auth);
625
+ type AuthRouteMethod = "GET" | "POST";
626
+
627
+ async function handleAuthRoute(
628
+ method: AuthRouteMethod,
629
+ request: Request,
630
+ ): Promise<Response> {
631
+ const { auth } = await import("@/lib/better-auth");
632
+ const handlers = toNextJsHandler(auth);
633
+
634
+ return handlers[method](request);
635
+ }
636
+
637
+ export function GET(request: Request): Promise<Response> {
638
+ return handleAuthRoute("GET", request);
639
+ }
640
+
641
+ export function POST(request: Request): Promise<Response> {
642
+ return handleAuthRoute("POST", request);
643
+ }
637
644
  `,
638
645
  // API-only scaffold frontend: a minimal layout and landing page that point
639
646
  // at the contract-backed API surface, plus a typed client without React
@@ -54,7 +54,12 @@ export const externalVersions = {
54
54
  pglite: "^0.5.2",
55
55
  pino: "^9.7.0",
56
56
  resend: "^6.14.0",
57
+ sentryNode: "^10.58.0",
58
+ nodemailer: "^8.0.5",
59
+ openFeatureServerSdk: "^1.22.0",
57
60
  typesPg: "^8.11.0",
61
+ ioredis: "^5.0.0",
62
+ awsSdkS3: "^3.955.0",
58
63
  upstashRateLimit: "^2.0.0",
59
64
  upstashRedis: "^1.0.0",
60
65
  };
@@ -0,0 +1,168 @@
1
+ import { expect } from "bun:test";
2
+ import { access, mkdir, symlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * Test helpers for typechecking and running generated apps inside the CLI
7
+ * test suite. Shared by make.test.ts, make-tenancy.test.ts, and
8
+ * make-inbox.test.ts; excluded from the published build.
9
+ */
10
+
11
+ const repoRoot = path.resolve(import.meta.dir, "../../../..");
12
+
13
+ /**
14
+ * Symlink the workspace and vendored dependencies a generated app needs for a
15
+ * real `tsc --noEmit` and `bun test` run, without a package-manager install.
16
+ * Pass `shell: true` for full-stack apps so the shadcn shell dependencies
17
+ * (icons, radix, theming) resolve too.
18
+ */
19
+ export async function linkSmokeDependencies(
20
+ targetDir: string,
21
+ options: { shell?: boolean } = {},
22
+ ): Promise<void> {
23
+ const nodeModules = path.join(targetDir, "node_modules");
24
+ const scopeDir = path.join(nodeModules, "@beignet");
25
+ await mkdir(scopeDir, { recursive: true });
26
+
27
+ const beignetPackages = [
28
+ "core",
29
+ "devtools",
30
+ "next",
31
+ "provider-auth-better-auth",
32
+ "provider-db-drizzle",
33
+ "provider-event-bus-memory",
34
+ "provider-logger-pino",
35
+ "provider-mail-resend",
36
+ "provider-storage-local",
37
+ "react-hook-form",
38
+ "react-query",
39
+ "web",
40
+ ];
41
+
42
+ for (const packageName of beignetPackages) {
43
+ await linkDependency(
44
+ path.join(repoRoot, "packages", packageName),
45
+ path.join(scopeDir, packageName),
46
+ );
47
+ }
48
+
49
+ const externalSource = path.join(
50
+ repoRoot,
51
+ "apps/example-linear-clone/node_modules",
52
+ );
53
+ const externalPackages = [
54
+ "@electric-sql/pglite",
55
+ "@hookform/resolvers",
56
+ "@libsql/client",
57
+ "@tanstack/react-query",
58
+ "better-auth",
59
+ "@types/bun",
60
+ "@types/node",
61
+ "@types/pg",
62
+ "@types/react",
63
+ "@types/react-dom",
64
+ "drizzle-orm",
65
+ "mysql2",
66
+ "next",
67
+ "pg",
68
+ "pino",
69
+ "react",
70
+ "react-dom",
71
+ "react-hook-form",
72
+ "resend",
73
+ "typescript",
74
+ "zod",
75
+ ];
76
+ if (options.shell) {
77
+ // Full-stack apps also import the shadcn shell dependencies.
78
+ externalPackages.push(
79
+ "class-variance-authority",
80
+ "clsx",
81
+ "lucide-react",
82
+ "next-themes",
83
+ "radix-ui",
84
+ "tailwind-merge",
85
+ );
86
+ }
87
+ const externalPackageSources: Record<string, string> = {
88
+ "@electric-sql/pglite": path.join(
89
+ repoRoot,
90
+ "packages/provider-db-drizzle/node_modules/@electric-sql/pglite",
91
+ ),
92
+ "@libsql/client": path.join(
93
+ repoRoot,
94
+ "packages/provider-db-drizzle/node_modules/@libsql/client",
95
+ ),
96
+ "@types/pg": path.join(
97
+ repoRoot,
98
+ "packages/provider-db-drizzle/node_modules/@types/pg",
99
+ ),
100
+ "drizzle-orm": path.join(
101
+ repoRoot,
102
+ "packages/provider-db-drizzle/node_modules/drizzle-orm",
103
+ ),
104
+ mysql2: path.join(
105
+ repoRoot,
106
+ "packages/provider-db-drizzle/node_modules/mysql2",
107
+ ),
108
+ pg: path.join(repoRoot, "packages/provider-db-drizzle/node_modules/pg"),
109
+ "better-auth": path.join(
110
+ repoRoot,
111
+ "packages/provider-auth-better-auth/node_modules/better-auth",
112
+ ),
113
+ resend: path.join(
114
+ repoRoot,
115
+ "packages/provider-mail-resend/node_modules/resend",
116
+ ),
117
+ "next-themes": path.join(
118
+ repoRoot,
119
+ "apps/example-ehr/node_modules/next-themes",
120
+ ),
121
+ };
122
+
123
+ for (const packageName of externalPackages) {
124
+ await linkDependency(
125
+ externalPackageSources[packageName] ??
126
+ path.join(externalSource, packageName),
127
+ path.join(nodeModules, packageName),
128
+ );
129
+ }
130
+
131
+ await linkDependency(
132
+ path.join(
133
+ repoRoot,
134
+ "node_modules/.bun/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec",
135
+ ),
136
+ path.join(nodeModules, "@standard-schema/spec"),
137
+ );
138
+ }
139
+
140
+ async function linkDependency(
141
+ source: string,
142
+ destination: string,
143
+ ): Promise<void> {
144
+ await access(source);
145
+ await mkdir(path.dirname(destination), { recursive: true });
146
+ await symlink(source, destination);
147
+ }
148
+
149
+ /**
150
+ * Run a command inside a generated app and fail the test with the combined
151
+ * output when it exits non-zero.
152
+ */
153
+ export async function expectAppCommand(
154
+ cwd: string,
155
+ command: readonly string[],
156
+ ): Promise<void> {
157
+ const proc = Bun.spawnSync(command, {
158
+ cwd,
159
+ env: {
160
+ ...process.env,
161
+ NEXT_TELEMETRY_DISABLED: "1",
162
+ },
163
+ stderr: "pipe",
164
+ stdout: "pipe",
165
+ });
166
+ const output = `${proc.stdout.toString()}${proc.stderr.toString()}`;
167
+ expect(proc.exitCode, output).toBe(0);
168
+ }