@beignet/cli 0.0.7 → 0.0.9

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.
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Shared string-edit helpers for registry files.
3
+ *
4
+ * `beignet make` generators and `beignet doctor --fix` both append entries to
5
+ * app-owned registry files such as `server/schedules.ts`, `server/tasks.ts`,
6
+ * and `server/outbox.ts`. Keeping the import insertion, array append, and
7
+ * membership primitives in one module keeps both write paths byte-compatible.
8
+ */
9
+
10
+ /**
11
+ * Source slice for an initializer expression, with absolute offsets.
12
+ */
13
+ export type InitializerInfo = {
14
+ text: string;
15
+ start: number;
16
+ end: number;
17
+ };
18
+
19
+ /**
20
+ * Result of appending an entry to a named registry.
21
+ *
22
+ * `missing` means the registry anchor could not be found or is not an array
23
+ * literal; callers should bail without writing.
24
+ */
25
+ export type AppendResult =
26
+ | { kind: "updated"; source: string }
27
+ | { kind: "unchanged" }
28
+ | { kind: "missing" };
29
+
30
+ /**
31
+ * Find the index of the delimiter closing the one at `openIndex`, skipping
32
+ * string literals.
33
+ */
34
+ export function matchingDelimiterIndex(
35
+ source: string,
36
+ openIndex: number,
37
+ open: string,
38
+ close: string,
39
+ ): number {
40
+ let depth = 0;
41
+ let inString: string | undefined;
42
+ let escaped = false;
43
+
44
+ for (let index = openIndex; index < source.length; index++) {
45
+ const char = source[index];
46
+ if (inString) {
47
+ if (escaped) {
48
+ escaped = false;
49
+ } else if (char === "\\") {
50
+ escaped = true;
51
+ } else if (char === inString) {
52
+ inString = undefined;
53
+ }
54
+ continue;
55
+ }
56
+
57
+ if (char === '"' || char === "'" || char === "`") {
58
+ inString = char;
59
+ continue;
60
+ }
61
+
62
+ if (char === open) {
63
+ depth++;
64
+ continue;
65
+ }
66
+
67
+ if (char === close) {
68
+ depth--;
69
+ if (depth === 0) return index;
70
+ }
71
+ }
72
+
73
+ return -1;
74
+ }
75
+
76
+ /**
77
+ * Insert a line directly after the last top-level import statement.
78
+ */
79
+ export function insertAfterImports(source: string, line: string): string {
80
+ const lines = source.split("\n");
81
+ let lastImportIndex = -1;
82
+
83
+ for (let index = 0; index < lines.length; index++) {
84
+ if (lines[index].startsWith("import ")) {
85
+ lastImportIndex = index;
86
+ }
87
+ }
88
+
89
+ if (lastImportIndex === -1) {
90
+ return `${line}\n${source}`;
91
+ }
92
+
93
+ lines.splice(lastImportIndex + 1, 0, line);
94
+ return lines.join("\n");
95
+ }
96
+
97
+ /**
98
+ * Collect every identifier referenced inside an array (or object) expression.
99
+ */
100
+ export function identifiersFromArrayExpression(
101
+ expression: string,
102
+ ): Set<string> {
103
+ const withoutBrackets = expression.replace(/^\[/, "").replace(/\]$/, "");
104
+
105
+ return new Set(
106
+ Array.from(
107
+ withoutBrackets.matchAll(/\b[A-Za-z_$][\w$]*\b/g),
108
+ ([name]) => name,
109
+ ),
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Append entries to an array literal, preserving single-line and multiline
115
+ * formatting plus trailing commas.
116
+ */
117
+ export function appendToArrayExpression(
118
+ expression: string,
119
+ names: string[],
120
+ ): string {
121
+ const closingBracket = /\]\s*$/.exec(expression);
122
+ if (!closingBracket) return expression;
123
+
124
+ const beforeClosingBracket = expression.slice(0, closingBracket.index);
125
+ const closingBracketText = expression.slice(closingBracket.index);
126
+ const inner = beforeClosingBracket.replace(/^\[/, "");
127
+ if (!inner.trim()) return `[${names.join(", ")}]`;
128
+
129
+ if (!expression.includes("\n")) {
130
+ const separator = /,\s*$/.test(inner) ? " " : ", ";
131
+ return `${beforeClosingBracket}${separator}${names.join(", ")}${closingBracketText}`;
132
+ }
133
+
134
+ const itemIndent =
135
+ inner
136
+ .split("\n")
137
+ .find((line) => line.trim())
138
+ ?.match(/^[\t ]*/)?.[0] ?? "\t";
139
+ const closingIndent = inner.match(/\n([\t ]*)$/)?.[1] ?? "";
140
+ const trimmedBeforeClosingBracket = beforeClosingBracket.replace(/\s*$/, "");
141
+ const appendedNames = names.join(`,\n${itemIndent}`);
142
+
143
+ if (/,\s*$/.test(inner)) {
144
+ return `${trimmedBeforeClosingBracket}\n${itemIndent}${appendedNames},\n${closingIndent}${closingBracketText}`;
145
+ }
146
+
147
+ return `${trimmedBeforeClosingBracket},\n${itemIndent}${appendedNames}${closingBracketText}`;
148
+ }
149
+
150
+ /**
151
+ * Locate the array literal initializing `const <constName> = ...`.
152
+ *
153
+ * Wrapping calls such as `defineTasks([...])` are tolerated because the first
154
+ * `[` after the assignment is used.
155
+ */
156
+ export function arrayInitializerInfo(
157
+ source: string,
158
+ constName: string,
159
+ ): InitializerInfo | undefined {
160
+ const match = new RegExp(`\\bconst\\s+${constName}\\s*=`).exec(source);
161
+ if (!match) return undefined;
162
+
163
+ const openBracket = source.indexOf("[", match.index);
164
+ if (openBracket === -1) return undefined;
165
+
166
+ const closeBracket = matchingDelimiterIndex(source, openBracket, "[", "]");
167
+ if (closeBracket === -1) return undefined;
168
+
169
+ return {
170
+ text: source.slice(openBracket, closeBracket + 1),
171
+ start: openBracket,
172
+ end: closeBracket + 1,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Append `entry` to the array initializing `const <constName> = [...]` and
178
+ * insert `importLine` after the imports when it is not already present.
179
+ *
180
+ * Generalizes the `server/schedules.ts` and `server/tasks.ts` registry update
181
+ * used by `beignet make`. Returns `missing` without touching the source when
182
+ * the array cannot be found, so callers never partially write.
183
+ */
184
+ export function appendToNamedArray(
185
+ source: string,
186
+ constName: string,
187
+ entry: string,
188
+ importLine?: string,
189
+ ): AppendResult {
190
+ const registry = arrayInitializerInfo(source, constName);
191
+ if (!registry) return { kind: "missing" };
192
+
193
+ return appendEntry(source, registry, entry, importLine, (next) =>
194
+ arrayInitializerInfo(next, constName),
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Locate the `events` or `jobs` value inside `defineOutboxRegistry({...})`.
200
+ */
201
+ export function outboxRegistryValueInfo(
202
+ source: string,
203
+ property: "events" | "jobs",
204
+ ): InitializerInfo | undefined {
205
+ const call = /\bdefineOutboxRegistry\s*(?:<[^>]*>\s*)?\(/.exec(source);
206
+ if (!call) return undefined;
207
+
208
+ const openBrace = source.indexOf("{", call.index);
209
+ if (openBrace === -1) return undefined;
210
+
211
+ const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
212
+ if (closeBrace === -1) return undefined;
213
+
214
+ const objectText = source.slice(openBrace, closeBrace + 1);
215
+ const propertyMatch = new RegExp(`\\b${property}\\s*:`).exec(objectText);
216
+ if (!propertyMatch) return undefined;
217
+
218
+ const valueStart = openBrace + propertyMatch.index + propertyMatch[0].length;
219
+ const value = topLevelValueSlice(source, valueStart, closeBrace);
220
+ if (!value) return undefined;
221
+
222
+ return value;
223
+ }
224
+
225
+ /**
226
+ * Append `entry` to the `events` or `jobs` array of
227
+ * `defineOutboxRegistry({...})` and insert `importLine` when needed.
228
+ *
229
+ * Returns `missing` without touching the source when the registry call or
230
+ * property is absent, or when the property value is not an array literal.
231
+ */
232
+ export function appendToOutboxRegistryArray(
233
+ source: string,
234
+ property: "events" | "jobs",
235
+ entry: string,
236
+ importLine?: string,
237
+ ): AppendResult {
238
+ const value = outboxRegistryValueInfo(source, property);
239
+ if (!value) return { kind: "missing" };
240
+
241
+ const entryName = entryIdentifier(entry);
242
+ if (identifiersFromArrayExpression(value.text).has(entryName)) {
243
+ return appendEntry(source, value, entry, importLine, () => undefined, {
244
+ alreadyListed: true,
245
+ });
246
+ }
247
+
248
+ if (!value.text.startsWith("[")) return { kind: "missing" };
249
+
250
+ return appendEntry(source, value, entry, importLine, (next) =>
251
+ outboxRegistryValueInfo(next, property),
252
+ );
253
+ }
254
+
255
+ function entryIdentifier(entry: string): string {
256
+ return entry.replace(/^\.{3}/, "").trim();
257
+ }
258
+
259
+ function appendEntry(
260
+ source: string,
261
+ info: InitializerInfo,
262
+ entry: string,
263
+ importLine: string | undefined,
264
+ relocate: (next: string) => InitializerInfo | undefined,
265
+ options: { alreadyListed?: boolean } = {},
266
+ ): AppendResult {
267
+ const alreadyListed =
268
+ options.alreadyListed ??
269
+ identifiersFromArrayExpression(info.text).has(entryIdentifier(entry));
270
+ let next = source;
271
+
272
+ if (importLine && !next.includes(importLine)) {
273
+ next = insertAfterImports(next, importLine);
274
+ }
275
+
276
+ if (!alreadyListed) {
277
+ const target = next === source ? info : relocate(next);
278
+ if (!target) return { kind: "missing" };
279
+ const nextArray = appendToArrayExpression(target.text, [entry]);
280
+ next = `${next.slice(0, target.start)}${nextArray}${next.slice(target.end)}`;
281
+ }
282
+
283
+ if (next === source) return { kind: "unchanged" };
284
+ return { kind: "updated", source: next };
285
+ }
286
+
287
+ function topLevelValueSlice(
288
+ source: string,
289
+ valueStart: number,
290
+ hardEnd: number,
291
+ ): InitializerInfo | undefined {
292
+ let start = valueStart;
293
+ while (start < hardEnd && /\s/.test(source[start])) start++;
294
+ if (start >= hardEnd) return undefined;
295
+
296
+ let depth = 0;
297
+ let inString: string | undefined;
298
+ let escaped = false;
299
+
300
+ for (let index = start; index <= hardEnd; index++) {
301
+ const char = source[index];
302
+ if (inString) {
303
+ if (escaped) {
304
+ escaped = false;
305
+ } else if (char === "\\") {
306
+ escaped = true;
307
+ } else if (char === inString) {
308
+ inString = undefined;
309
+ }
310
+ continue;
311
+ }
312
+
313
+ if (char === '"' || char === "'" || char === "`") {
314
+ inString = char;
315
+ continue;
316
+ }
317
+
318
+ if (char === "[" || char === "(" || char === "{") {
319
+ depth++;
320
+ continue;
321
+ }
322
+
323
+ if (char === "]" || char === ")" || char === "}") {
324
+ if (depth === 0) {
325
+ return trimmedInfo(source, start, index);
326
+ }
327
+ depth--;
328
+ continue;
329
+ }
330
+
331
+ if (char === "," && depth === 0) {
332
+ return trimmedInfo(source, start, index);
333
+ }
334
+ }
335
+
336
+ return undefined;
337
+ }
338
+
339
+ function trimmedInfo(
340
+ source: string,
341
+ start: number,
342
+ end: number,
343
+ ): InitializerInfo {
344
+ let trimmedEnd = end;
345
+ while (trimmedEnd > start && /\s/.test(source[trimmedEnd - 1])) trimmedEnd--;
346
+
347
+ return {
348
+ text: source.slice(start, trimmedEnd),
349
+ start,
350
+ end: trimmedEnd,
351
+ };
352
+ }
@@ -15,7 +15,7 @@ export function packageJson(ctx: TemplateContext): string {
15
15
  "@beignet/devtools": ctx.beignetVersion,
16
16
  "@beignet/next": ctx.beignetVersion,
17
17
  "@beignet/provider-auth-better-auth": ctx.beignetVersion,
18
- "@beignet/provider-drizzle-turso": ctx.beignetVersion,
18
+ "@beignet/provider-db-drizzle": ctx.beignetVersion,
19
19
  "@beignet/provider-logger-pino": ctx.beignetVersion,
20
20
  "@libsql/client": externalVersions.libsqlClient,
21
21
  "better-auth": externalVersions.betterAuth,
@@ -214,7 +214,7 @@ ${cli} doctor
214
214
  ${shellMap}
215
215
  ## Before deploying
216
216
 
217
- - Create a Turso database or keep \`TURSO_DB_URL=file:local.db\` for local libSQL development.
217
+ - Keep \`SQLITE_DB_URL=file:local.db\` for local libSQL development or point it at a hosted libSQL database such as Turso.
218
218
  - Run \`${cli} db generate\` and \`${cli} db migrate\` after changing the Drizzle schema.
219
219
  - Run \`${cli} db reset\` to rebuild a local SQLite database from the checked-in migrations.
220
220
  - Remove \`DEVTOOLS_ENABLED=true\` in production unless you add authentication and stricter redaction.
@@ -288,8 +288,8 @@ export function envExample(ctx: TemplateContext): string {
288
288
  `LOG_SERVICE=${ctx.name}`,
289
289
  "",
290
290
  "# Use file:local.db for local libSQL or libsql://... for Turso cloud.",
291
- "TURSO_DB_URL=file:local.db",
292
- "# TURSO_DB_AUTH_TOKEN=",
291
+ "SQLITE_DB_URL=file:local.db",
292
+ "# SQLITE_DB_AUTH_TOKEN=",
293
293
  "",
294
294
  "BETTER_AUTH_SECRET=local-dev-better-auth-secret-change-me",
295
295
  "BETTER_AUTH_URL=http://localhost:3000",
@@ -15,8 +15,8 @@ const files = {
15
15
  out: "./drizzle",
16
16
  dialect: "sqlite",
17
17
  dbCredentials: {
18
- url: process.env.TURSO_DB_URL ?? "file:local.db",
19
- authToken: process.env.TURSO_DB_AUTH_TOKEN,
18
+ url: process.env.SQLITE_DB_URL ?? "file:local.db",
19
+ authToken: process.env.SQLITE_DB_AUTH_TOKEN,
20
20
  },
21
21
  };
22
22
  `,
@@ -123,7 +123,7 @@ export const idempotencyRecords = sqliteTable(
123
123
  `,
124
124
  drizzleTodoRepository: `import type { beignetServerOnly } from "@beignet/core/server-only";
125
125
  import { offsetPageResult } from "@beignet/core/pagination";
126
- import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
126
+ import type { DrizzleSqliteDatabase } from "@beignet/provider-db-drizzle/sqlite";
127
127
  import { count, desc, eq } from "drizzle-orm";
128
128
  import type {
129
129
  NewTodo,
@@ -146,7 +146,7 @@ function toTodo(row: TodoRow): Todo {
146
146
  }
147
147
 
148
148
  export function createDrizzleTodoRepository(
149
- db: DrizzleTursoDatabase<typeof schema>,
149
+ db: DrizzleSqliteDatabase<typeof schema>,
150
150
  ): TodoRepository {
151
151
  return {
152
152
  async list(userId, page) {
@@ -208,13 +208,13 @@ export function createDrizzleTodoRepository(
208
208
  };
209
209
  }
210
210
  `,
211
- dbRepositories: `import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
211
+ dbRepositories: `import type { DrizzleSqliteDatabase } from "@beignet/provider-db-drizzle/sqlite";
212
212
  import { createDrizzleTodoRepository } from "@/infra/todos/drizzle-todo-repository";
213
213
  import type { AppTransactionPorts } from "@/ports";
214
214
  import * as schema from "./schema";
215
215
 
216
216
  export function createRepositories(
217
- db: DrizzleTursoDatabase<typeof schema>,
217
+ db: DrizzleSqliteDatabase<typeof schema>,
218
218
  ): Omit<AppTransactionPorts, "idempotency"> {
219
219
  return {
220
220
  todos: createDrizzleTodoRepository(db),
@@ -265,10 +265,10 @@ async function prepare(client: Client): Promise<void> {
265
265
  dbProvider: `import type { beignetServerOnly } from "@beignet/core/server-only";
266
266
  import { createProvider } from "@beignet/core/providers";
267
267
  import {
268
- createDrizzleTursoIdempotencyPort,
269
- createDrizzleTursoUnitOfWork,
268
+ createDrizzleSqliteIdempotencyPort,
269
+ createDrizzleSqliteUnitOfWork,
270
270
  type DbPort,
271
- } from "@beignet/provider-drizzle-turso";
271
+ } from "@beignet/provider-db-drizzle/sqlite";
272
272
  import type { AppPorts } from "@/ports";
273
273
  import { ensureDatabaseReady } from "./database-ready";
274
274
  import { createRepositories } from "./repositories";
@@ -283,21 +283,21 @@ export const starterDatabaseProvider = createProvider<{
283
283
  const dbPort = ports.db;
284
284
  if (!dbPort) {
285
285
  throw new Error(
286
- "starterDatabaseProvider requires a db port. Register createDrizzleTursoProvider({ schema }) before it.",
286
+ "starterDatabaseProvider requires a db port. Register createDrizzleSqliteProvider({ schema }) before it.",
287
287
  );
288
288
  }
289
289
 
290
290
  const repositories = createRepositories(dbPort.db);
291
- const idempotency = createDrizzleTursoIdempotencyPort(dbPort.db);
291
+ const idempotency = createDrizzleSqliteIdempotencyPort(dbPort.db);
292
292
 
293
293
  const providedPorts: Pick<AppPorts, "idempotency" | "todos" | "uow"> = {
294
294
  ...repositories,
295
295
  idempotency,
296
- uow: createDrizzleTursoUnitOfWork({
296
+ uow: createDrizzleSqliteUnitOfWork({
297
297
  db: dbPort.db,
298
298
  createTransactionPorts: (tx) => ({
299
299
  ...createRepositories(tx),
300
- idempotency: createDrizzleTursoIdempotencyPort(tx),
300
+ idempotency: createDrizzleSqliteIdempotencyPort(tx),
301
301
  }),
302
302
  }),
303
303
  };
@@ -317,7 +317,7 @@ import { migrate } from "drizzle-orm/libsql/migrator";
317
317
  import { env } from "@/lib/env";
318
318
 
319
319
  if (
320
- !env.TURSO_DB_URL.startsWith("file:") &&
320
+ !env.SQLITE_DB_URL.startsWith("file:") &&
321
321
  process.env.BEIGNET_ALLOW_DATABASE_RESET !== "true"
322
322
  ) {
323
323
  throw new Error(
@@ -336,8 +336,8 @@ const tables = [
336
336
  ] as const;
337
337
 
338
338
  const client = createClient({
339
- url: env.TURSO_DB_URL,
340
- authToken: env.TURSO_DB_AUTH_TOKEN,
339
+ url: env.SQLITE_DB_URL,
340
+ authToken: env.SQLITE_DB_AUTH_TOKEN,
341
341
  });
342
342
 
343
343
  try {
@@ -5,7 +5,7 @@ export function serverProviders(ctx: TemplateContext): string {
5
5
  'import type { beignetServerOnly } from "@beignet/core/server-only";',
6
6
  'import { createDevtoolsProvider } from "@beignet/devtools";',
7
7
  'import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";',
8
- 'import { createDrizzleTursoProvider } from "@beignet/provider-drizzle-turso";',
8
+ 'import { createDrizzleSqliteProvider } from "@beignet/provider-db-drizzle/sqlite";',
9
9
  hasIntegration(ctx, "inngest")
10
10
  ? 'import { inngestProvider } from "@beignet/provider-inngest";'
11
11
  : undefined,
@@ -26,7 +26,7 @@ export function serverProviders(ctx: TemplateContext): string {
26
26
  "\tcreateDevtoolsProvider(),",
27
27
  "\tcreateAuthBetterAuthProvider<AuthUser, AuthSessionMetadata>(auth),",
28
28
  "\tloggerPinoProvider,",
29
- "\tdrizzleTursoProvider,",
29
+ "\tdrizzleSqliteProvider,",
30
30
  "\tstarterDatabaseProvider,",
31
31
  hasIntegration(ctx, "inngest") ? "\tinngestProvider," : undefined,
32
32
  hasIntegration(ctx, "resend") ? "\tmailResendProvider," : undefined,
@@ -37,7 +37,7 @@ export function serverProviders(ctx: TemplateContext): string {
37
37
 
38
38
  return `${imports.join("\n")}
39
39
 
40
- const drizzleTursoProvider = createDrizzleTursoProvider({ schema });
40
+ const drizzleSqliteProvider = createDrizzleSqliteProvider({ schema });
41
41
 
42
42
  export const providers = [
43
43
  ${entries.join("\n")}
@@ -151,8 +151,8 @@ export const env = createEnv({
151
151
  .default("local-dev-better-auth-secret-change-me"),
152
152
  BETTER_AUTH_URL: z.string().url().default("http://localhost:3000"),
153
153
  BETTER_AUTH_TRUSTED_ORIGINS: z.string().optional(),
154
- TURSO_DB_URL: z.string().default("file:local.db"),
155
- TURSO_DB_AUTH_TOKEN: z.string().optional(),
154
+ SQLITE_DB_URL: z.string().default("file:local.db"),
155
+ SQLITE_DB_AUTH_TOKEN: z.string().optional(),
156
156
  LOG_LEVEL: z
157
157
  .enum(["trace", "debug", "info", "warn", "error", "fatal"])
158
158
  .default("info"),
@@ -344,9 +344,9 @@ export const server = await createNextServer({
344
344
  ports: appPorts,
345
345
  providers,
346
346
  providerConfig: {
347
- "drizzle-turso": {
348
- DB_URL: env.TURSO_DB_URL,
349
- DB_AUTH_TOKEN: env.TURSO_DB_AUTH_TOKEN,
347
+ "drizzle-sqlite": {
348
+ DB_URL: env.SQLITE_DB_URL,
349
+ DB_AUTH_TOKEN: env.SQLITE_DB_AUTH_TOKEN,
350
350
  },
351
351
  },
352
352
  hooks: [createIdempotencyHooks<AppContext>()],
@@ -428,8 +428,8 @@ import * as schema from "@/infra/db/schema";
428
428
  import { env } from "@/lib/env";
429
429
 
430
430
  const client = createClient({
431
- url: env.TURSO_DB_URL,
432
- authToken: env.TURSO_DB_AUTH_TOKEN,
431
+ url: env.SQLITE_DB_URL,
432
+ authToken: env.SQLITE_DB_AUTH_TOKEN,
433
433
  });
434
434
 
435
435
  // Auth routes can be the first thing to touch the database, before any