@beignet/cli 0.0.42 → 0.0.44

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 +24 -0
  2. package/README.md +21 -5
  3. package/dist/analysis/source-index.d.ts +1 -1
  4. package/dist/analysis/source-index.d.ts.map +1 -1
  5. package/dist/analysis/source-index.js +4 -2
  6. package/dist/analysis/source-index.js.map +1 -1
  7. package/dist/analysis/workspace.d.ts +3 -2
  8. package/dist/analysis/workspace.d.ts.map +1 -1
  9. package/dist/analysis/workspace.js +51 -39
  10. package/dist/analysis/workspace.js.map +1 -1
  11. package/dist/app-map.d.ts.map +1 -1
  12. package/dist/app-map.js +65 -4
  13. package/dist/app-map.js.map +1 -1
  14. package/dist/inspect.d.ts +5 -0
  15. package/dist/inspect.d.ts.map +1 -1
  16. package/dist/inspect.js +32 -135
  17. package/dist/inspect.js.map +1 -1
  18. package/dist/make/inbox.js +4 -4
  19. package/dist/make/payments.js +6 -6
  20. package/dist/make/tenancy.d.ts.map +1 -1
  21. package/dist/make/tenancy.js +12 -14
  22. package/dist/make/tenancy.js.map +1 -1
  23. package/dist/make.d.ts.map +1 -1
  24. package/dist/make.js +33 -23
  25. package/dist/make.js.map +1 -1
  26. package/dist/registry-edits.d.ts +7 -1
  27. package/dist/registry-edits.d.ts.map +1 -1
  28. package/dist/registry-edits.js +61 -43
  29. package/dist/registry-edits.js.map +1 -1
  30. package/dist/templates/agents.d.ts.map +1 -1
  31. package/dist/templates/agents.js +10 -3
  32. package/dist/templates/agents.js.map +1 -1
  33. package/dist/templates/base.d.ts.map +1 -1
  34. package/dist/templates/base.js +4 -2
  35. package/dist/templates/base.js.map +1 -1
  36. package/dist/templates/db/mysql.js +3 -3
  37. package/dist/templates/db/postgres.js +3 -3
  38. package/dist/templates/db/sqlite.js +3 -3
  39. package/dist/templates/server.js +4 -4
  40. package/dist/templates/server.js.map +1 -1
  41. package/dist/templates/todos.js +2 -2
  42. package/package.json +2 -2
  43. package/skills/app-structure/SKILL.md +9 -0
  44. package/src/analysis/source-index.ts +12 -3
  45. package/src/analysis/workspace.ts +94 -52
  46. package/src/app-map.ts +82 -4
  47. package/src/inspect.ts +40 -150
  48. package/src/make/inbox.ts +4 -4
  49. package/src/make/payments.ts +6 -6
  50. package/src/make/tenancy.ts +12 -14
  51. package/src/make.ts +36 -24
  52. package/src/registry-edits.ts +81 -39
  53. package/src/templates/agents.ts +10 -3
  54. package/src/templates/base.ts +4 -2
  55. package/src/templates/db/mysql.ts +3 -3
  56. package/src/templates/db/postgres.ts +3 -3
  57. package/src/templates/db/sqlite.ts +3 -3
  58. package/src/templates/server.ts +4 -4
  59. package/src/templates/todos.ts +2 -2
@@ -952,7 +952,7 @@ export const workspaceMemberWithUserSchema = workspaceMemberSchema.extend({
952
952
  export const workspaceInviteSchema = z.object({
953
953
  \tid: z.string().min(1),
954
954
  \tworkspaceId: z.string().min(1),
955
- \temail: z.string().email(),
955
+ \temail: z.email(),
956
956
  \trole: workspaceRoleSchema,
957
957
  \ttoken: z.string().min(1),
958
958
  \tinvitedByUserId: z.string().min(1),
@@ -1019,7 +1019,7 @@ export const listInvitesOutputSchema = z.object({
1019
1019
  });
1020
1020
 
1021
1021
  export const createInviteInputSchema = z.object({
1022
- \temail: z.string().email(),
1022
+ \temail: z.email(),
1023
1023
  \trole: workspaceRoleSchema.default("member"),
1024
1024
  });
1025
1025
 
@@ -2005,7 +2005,7 @@ const invitePayloadSchema = z.object({
2005
2005
  \tinviteId: z.string().min(1),
2006
2006
  \tworkspaceId: z.string().min(1),
2007
2007
  \tworkspaceName: z.string().min(1),
2008
- \temail: z.string().email(),
2008
+ \temail: z.email(),
2009
2009
  \trole: z.enum(["admin", "member"]),
2010
2010
  \ttoken: z.string().min(1),
2011
2011
  \tinvitedByName: z.string().optional(),
@@ -3242,9 +3242,7 @@ export const workspaces = ${dialect.tableFunction}(
3242
3242
  \t\tcreatedAt: ${column("created_at")}.notNull(),
3243
3243
  \t\tupdatedAt: ${column("updated_at")}.notNull(),
3244
3244
  \t},
3245
- \t(table) => ({
3246
- \t\tslugUnique: uniqueIndex("workspaces_slug_unique").on(table.slug),
3247
- \t}),
3245
+ \t(table) => [uniqueIndex("workspaces_slug_unique").on(table.slug)],
3248
3246
  );
3249
3247
 
3250
3248
  export const workspaceMembers = ${dialect.tableFunction}(
@@ -3256,12 +3254,12 @@ export const workspaceMembers = ${dialect.tableFunction}(
3256
3254
  \t\trole: ${column("role")}.$type<WorkspaceRole>().notNull(),
3257
3255
  \t\tjoinedAt: ${column("joined_at")}.notNull(),
3258
3256
  \t},
3259
- \t(table) => ({
3260
- \t\tworkspaceUserUnique: uniqueIndex(
3257
+ \t(table) => [
3258
+ \t\tuniqueIndex(
3261
3259
  \t\t\t"workspace_members_workspace_user_unique",
3262
3260
  \t\t).on(table.workspaceId, table.userId),
3263
- \t\tuserIdx: index("workspace_members_user_idx").on(table.userId),
3264
- \t}),
3261
+ \t\tindex("workspace_members_user_idx").on(table.userId),
3262
+ \t],
3265
3263
  );
3266
3264
 
3267
3265
  export const workspaceInvites = ${dialect.tableFunction}(
@@ -3278,15 +3276,15 @@ export const workspaceInvites = ${dialect.tableFunction}(
3278
3276
  \t\tacceptedAt: ${column("accepted_at")},
3279
3277
  \t\tcreatedAt: ${column("created_at")}.notNull(),
3280
3278
  \t},
3281
- \t(table) => ({
3282
- \t\ttokenUnique: uniqueIndex("workspace_invites_token_unique").on(
3279
+ \t(table) => [
3280
+ \t\tuniqueIndex("workspace_invites_token_unique").on(
3283
3281
  \t\t\ttable.token,
3284
3282
  \t\t),
3285
- \t\tworkspaceStatusIdx: index("workspace_invites_workspace_status_idx").on(
3283
+ \t\tindex("workspace_invites_workspace_status_idx").on(
3286
3284
  \t\t\ttable.workspaceId,
3287
3285
  \t\t\ttable.status,
3288
3286
  \t\t),
3289
- \t}),
3287
+ \t],
3290
3288
  );
3291
3289
  `;
3292
3290
  }
package/src/make.ts CHANGED
@@ -1575,7 +1575,8 @@ const cronSecretEnvEntry = `\t\tCRON_SECRET: z.string().min(1).optional(),`;
1575
1575
 
1576
1576
  // The literal APP_URL line every generated lib/env.ts ships with. CRON_SECRET
1577
1577
  // slots in directly below it, matching the documented env shape.
1578
- const cronSecretEnvAnchor = `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`;
1578
+ const cronSecretEnvAnchor = `\t\tAPP_URL: z.url().default("http://localhost:3000"),`;
1579
+ const legacyCronSecretEnvAnchor = `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`;
1579
1580
 
1580
1581
  type CronSecretWiringPlan = {
1581
1582
  files: Array<GeneratedFile & { result: "created" | "updated" }>;
@@ -1596,10 +1597,15 @@ async function planCronSecretWiring(
1596
1597
  `Could not find the generated env file ${cronSecretEnvFile}. ${manualInstruction}`,
1597
1598
  );
1598
1599
  }
1600
+ const envAnchor = envSource.includes(cronSecretEnvAnchor)
1601
+ ? cronSecretEnvAnchor
1602
+ : envSource.includes(legacyCronSecretEnvAnchor)
1603
+ ? legacyCronSecretEnvAnchor
1604
+ : undefined;
1599
1605
 
1600
1606
  if (/\bCRON_SECRET\b/.test(envSource)) {
1601
1607
  plan.skippedFiles.push(cronSecretEnvFile);
1602
- } else if (!envSource.includes(cronSecretEnvAnchor)) {
1608
+ } else if (envAnchor === undefined) {
1603
1609
  throw new Error(
1604
1610
  `Could not find the generated APP_URL server entry in ${cronSecretEnvFile}. ${manualInstruction}`,
1605
1611
  );
@@ -1607,8 +1613,8 @@ async function planCronSecretWiring(
1607
1613
  plan.files.push({
1608
1614
  path: cronSecretEnvFile,
1609
1615
  content: envSource.replace(
1610
- cronSecretEnvAnchor,
1611
- `${cronSecretEnvAnchor}\n${cronSecretEnvEntry}`,
1616
+ envAnchor,
1617
+ `${envAnchor}\n${cronSecretEnvEntry}`,
1612
1618
  ),
1613
1619
  result: "updated",
1614
1620
  });
@@ -2218,6 +2224,10 @@ export async function makeUpload(
2218
2224
  dryRun: true,
2219
2225
  },
2220
2226
  );
2227
+ // Validate central registration before generating feature files or applying
2228
+ // provider wiring. Customized routes require an explicit manual edit rather
2229
+ // than a successful-looking partial generation.
2230
+ await updateUploadRoute(targetDir, names, config, { dryRun: true });
2221
2231
 
2222
2232
  const result = await makeFeatureArtifact({
2223
2233
  targetDir,
@@ -3716,7 +3726,9 @@ async function updateUploadRoute(
3716
3726
 
3717
3727
  const registry = defineUploadsInitializerInfo(next, "uploadRegistry");
3718
3728
  if (!registry) {
3719
- return "skipped";
3729
+ throw new Error(
3730
+ `Cannot register ${registryName} in ${file}: expected an uploadRegistry initialized with defineUploads({ ... }). Restore that registry shape or add ${registryKey}: ${registryName} manually before rerunning beignet make upload.`,
3731
+ );
3720
3732
  }
3721
3733
  if (!new RegExp(`\\b${registryKey}\\s*:`).test(registry.text)) {
3722
3734
  next = `${next.slice(0, registry.end)}\t${registryKey}: ${registryName},\n${next.slice(
@@ -3912,7 +3924,7 @@ function schemasFile(
3912
3924
  return `import { z } from "zod";
3913
3925
 
3914
3926
  export const ${names.singularPascal}Schema = z.object({
3915
- id: z.string().uuid(),
3927
+ id: z.uuid(),
3916
3928
  ${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
3917
3929
  version: z.number().int().min(1),
3918
3930
  createdAt: z.string().datetime(),
@@ -3926,7 +3938,7 @@ const ${names.singularPascal}CursorPayloadSchema = z.object({
3926
3938
  sortBy: ${names.singularPascal}SortBySchema,
3927
3939
  sortDirection: ${names.singularPascal}SortDirectionSchema,
3928
3940
  sortValue: z.string(),
3929
- id: z.string().uuid(),
3941
+ id: z.uuid(),
3930
3942
  });
3931
3943
 
3932
3944
  export type ${names.singularPascal}SortBy = z.infer<
@@ -4019,7 +4031,7 @@ ${
4019
4031
  mode === "resource"
4020
4032
  ? `
4021
4033
  export const ${names.singularPascal}IdInputSchema = z.object({
4022
- id: z.string().uuid(),
4034
+ id: z.uuid(),
4023
4035
  });
4024
4036
 
4025
4037
  export const Update${names.singularPascal}BodySchema = z.object({
@@ -4479,16 +4491,16 @@ export const outboxMessages = mysqlTable(
4479
4491
  \t\tcreatedAt: varchar("created_at", { length: 32 }).notNull(),
4480
4492
  \t\tupdatedAt: varchar("updated_at", { length: 32 }).notNull(),
4481
4493
  \t},
4482
- \t(table) => ({
4483
- \t\tavailableIdx: index("outbox_messages_available_idx").on(
4494
+ \t(table) => [
4495
+ \t\tindex("outbox_messages_available_idx").on(
4484
4496
  \t\t\ttable.status,
4485
4497
  \t\t\ttable.availableAt,
4486
4498
  \t\t),
4487
- \t\tlockedIdx: index("outbox_messages_locked_idx").on(
4499
+ \t\tindex("outbox_messages_locked_idx").on(
4488
4500
  \t\t\ttable.status,
4489
4501
  \t\t\ttable.lockedUntil,
4490
4502
  \t\t),
4491
- \t}),
4503
+ \t],
4492
4504
  );
4493
4505
  `;
4494
4506
  }
@@ -4515,16 +4527,16 @@ export const outboxMessages = ${dialect.tableFunction}(
4515
4527
  \t\tcreatedAt: text("created_at").notNull(),
4516
4528
  \t\tupdatedAt: text("updated_at").notNull(),
4517
4529
  \t},
4518
- \t(table) => ({
4519
- \t\tavailableIdx: index("outbox_messages_available_idx").on(
4530
+ \t(table) => [
4531
+ \t\tindex("outbox_messages_available_idx").on(
4520
4532
  \t\t\ttable.status,
4521
4533
  \t\t\ttable.availableAt,
4522
4534
  \t\t),
4523
- \t\tlockedIdx: index("outbox_messages_locked_idx").on(
4535
+ \t\tindex("outbox_messages_locked_idx").on(
4524
4536
  \t\t\ttable.status,
4525
4537
  \t\t\ttable.lockedUntil,
4526
4538
  \t\t),
4527
- \t}),
4539
+ \t],
4528
4540
  );
4529
4541
  `;
4530
4542
  }
@@ -4912,7 +4924,7 @@ const ${names.pluralCamel} = defineContractGroup()
4912
4924
  });
4913
4925
 
4914
4926
  export const ${names.singularCamel}Schema = z.object({
4915
- id: z.string().uuid(),
4927
+ id: z.uuid(),
4916
4928
  name: z.string().min(1),
4917
4929
  });
4918
4930
 
@@ -5121,19 +5133,19 @@ import { z } from "zod";
5121
5133
 
5122
5134
  export const ${names.singularPascal}Created = defineEvent("${names.pluralCamel}.created", {
5123
5135
  payload: z.object({
5124
- id: z.string().uuid(),
5136
+ id: z.uuid(),
5125
5137
  }),
5126
5138
  });
5127
5139
 
5128
5140
  export const ${names.singularPascal}Updated = defineEvent("${names.pluralCamel}.updated", {
5129
5141
  payload: z.object({
5130
- id: z.string().uuid(),
5142
+ id: z.uuid(),
5131
5143
  }),
5132
5144
  });
5133
5145
 
5134
5146
  export const ${names.singularPascal}Deleted = defineEvent("${names.pluralCamel}.deleted", {
5135
5147
  payload: z.object({
5136
- id: z.string().uuid(),
5148
+ id: z.uuid(),
5137
5149
  }),
5138
5150
  });
5139
5151
 
@@ -5209,7 +5221,7 @@ function eventFile(names: EventNames): string {
5209
5221
  import { z } from "zod";
5210
5222
 
5211
5223
  export const ${names.payloadSchemaName} = z.object({
5212
- \tid: z.string().uuid(),
5224
+ \tid: z.uuid(),
5213
5225
  });
5214
5226
 
5215
5227
  export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
@@ -5226,7 +5238,7 @@ import { z } from "zod";
5226
5238
  import { defineJob } from "${aliasModule(config.paths.jobsBuilder)}";
5227
5239
 
5228
5240
  export const ${names.payloadSchemaName} = z.object({
5229
- \tid: z.string().uuid(),
5241
+ \tid: z.uuid(),
5230
5242
  });
5231
5243
 
5232
5244
  export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
@@ -5317,9 +5329,9 @@ import { z } from "zod";
5317
5329
  import { defineNotification } from "${aliasModule(config.paths.notificationsBuilder)}";
5318
5330
 
5319
5331
  export const ${names.payloadSchemaName} = z.object({
5320
- \tid: z.string().uuid(),
5332
+ \tid: z.uuid(),
5321
5333
  \tsubject: z.string(),
5322
- \trecipientEmail: z.string().email(),
5334
+ \trecipientEmail: z.email(),
5323
5335
  });
5324
5336
 
5325
5337
  export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
@@ -1,3 +1,5 @@
1
+ import ts from "typescript";
2
+
1
3
  /**
2
4
  * Shared string-edit helpers for registry files.
3
5
  *
@@ -28,21 +30,35 @@ export type AppendResult =
28
30
  | { kind: "missing" };
29
31
 
30
32
  /**
31
- * Find the index of the delimiter closing the one at `openIndex`, skipping
32
- * string literals.
33
+ * Yield indexes that belong to executable source, excluding string literals
34
+ * and line/block comments. Registry scanners share this lexer so delimiters
35
+ * in user comments cannot corrupt their depth tracking.
33
36
  */
34
- export function matchingDelimiterIndex(
37
+ export function* codeCharacterIndexes(
35
38
  source: string,
36
- openIndex: number,
37
- open: string,
38
- close: string,
39
- ): number {
40
- let depth = 0;
39
+ start = 0,
40
+ end = source.length,
41
+ ): Generator<number> {
41
42
  let inString: string | undefined;
42
43
  let escaped = false;
44
+ let inLineComment = false;
45
+ let inBlockComment = false;
43
46
 
44
- for (let index = openIndex; index < source.length; index++) {
47
+ for (let index = start; index < end; index++) {
45
48
  const char = source[index];
49
+ const next = source[index + 1];
50
+
51
+ if (inLineComment) {
52
+ if (char === "\n" || char === "\r") inLineComment = false;
53
+ continue;
54
+ }
55
+ if (inBlockComment) {
56
+ if (char === "*" && next === "/") {
57
+ inBlockComment = false;
58
+ index++;
59
+ }
60
+ continue;
61
+ }
46
62
  if (inString) {
47
63
  if (escaped) {
48
64
  escaped = false;
@@ -54,11 +70,40 @@ export function matchingDelimiterIndex(
54
70
  continue;
55
71
  }
56
72
 
73
+ if (char === "/" && next === "/") {
74
+ inLineComment = true;
75
+ index++;
76
+ continue;
77
+ }
78
+ if (char === "/" && next === "*") {
79
+ inBlockComment = true;
80
+ index++;
81
+ continue;
82
+ }
57
83
  if (char === '"' || char === "'" || char === "`") {
58
84
  inString = char;
59
85
  continue;
60
86
  }
61
87
 
88
+ yield index;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Find the index of the delimiter closing the one at `openIndex`, skipping
94
+ * string literals and comments.
95
+ */
96
+ export function matchingDelimiterIndex(
97
+ source: string,
98
+ openIndex: number,
99
+ open: string,
100
+ close: string,
101
+ ): number {
102
+ let depth = 0;
103
+
104
+ for (const index of codeCharacterIndexes(source, openIndex)) {
105
+ const char = source[index];
106
+
62
107
  if (char === open) {
63
108
  depth++;
64
109
  continue;
@@ -77,21 +122,32 @@ export function matchingDelimiterIndex(
77
122
  * Insert a line directly after the last top-level import statement.
78
123
  */
79
124
  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
- }
125
+ const sourceFile = ts.createSourceFile(
126
+ "registry.ts",
127
+ source,
128
+ ts.ScriptTarget.Latest,
129
+ true,
130
+ ts.ScriptKind.TS,
131
+ );
132
+ let lastImport: ts.ImportDeclaration | undefined;
133
+ for (const statement of sourceFile.statements) {
134
+ if (ts.isImportDeclaration(statement)) lastImport = statement;
87
135
  }
88
136
 
89
- if (lastImportIndex === -1) {
137
+ if (!lastImport) {
90
138
  return `${line}\n${source}`;
91
139
  }
92
140
 
93
- lines.splice(lastImportIndex + 1, 0, line);
94
- return lines.join("\n");
141
+ const lineBreak = source.includes("\r\n") ? "\r\n" : "\n";
142
+ const lineEnd = source.indexOf(lineBreak, lastImport.end);
143
+ if (lineEnd === -1) {
144
+ return `${source}${lineBreak}${line}${lineBreak}`;
145
+ }
146
+
147
+ const insertAt = lineEnd + lineBreak.length;
148
+ return `${source.slice(0, insertAt)}${line}${lineBreak}${source.slice(
149
+ insertAt,
150
+ )}`;
95
151
  }
96
152
 
97
153
  /**
@@ -147,11 +203,14 @@ export function addNamedValueImport(
147
203
  export function identifiersFromArrayExpression(
148
204
  expression: string,
149
205
  ): Set<string> {
150
- const withoutBrackets = expression.replace(/^\[/, "").replace(/\]$/, "");
206
+ const code = Array.from({ length: expression.length }, () => " ");
207
+ for (const index of codeCharacterIndexes(expression)) {
208
+ code[index] = expression[index];
209
+ }
151
210
 
152
211
  return new Set(
153
212
  Array.from(
154
- withoutBrackets.matchAll(/\b[A-Za-z_$][\w$]*\b/g),
213
+ code.join("").matchAll(/\b[A-Za-z_$][\w$]*\b/g),
155
214
  ([name]) => name,
156
215
  ),
157
216
  );
@@ -342,26 +401,9 @@ function topLevelValueSlice(
342
401
  if (start >= hardEnd) return undefined;
343
402
 
344
403
  let depth = 0;
345
- let inString: string | undefined;
346
- let escaped = false;
347
404
 
348
- for (let index = start; index <= hardEnd; index++) {
405
+ for (const index of codeCharacterIndexes(source, start, hardEnd + 1)) {
349
406
  const char = source[index];
350
- if (inString) {
351
- if (escaped) {
352
- escaped = false;
353
- } else if (char === "\\") {
354
- escaped = true;
355
- } else if (char === inString) {
356
- inString = undefined;
357
- }
358
- continue;
359
- }
360
-
361
- if (char === '"' || char === "'" || char === "`") {
362
- inString = char;
363
- continue;
364
- }
365
407
 
366
408
  if (char === "[" || char === "(" || char === "{") {
367
409
  depth++;
@@ -31,8 +31,8 @@ export function agentsMd(ctx: TemplateContext): string {
31
31
  "\n| Server Component request context | Use an app-owned server-only helper such as `lib/server-context.ts` that wraps `server.createContextFromNext()` in React `cache(...)`. Layouts may read `ctx.auth`, `ctx.tenant`, and request metadata for redirects and shell state; feature data still belongs behind use cases. |" +
32
32
  "\n| Server Component React Query prefetch | Use an app-owned helper such as `lib/server-react-query.ts` on top of the request context: keep the `rq(contract).queryOptions(...)` key, replace only the server `queryFn` with the direct use case call, and do not create a parallel `rqServer` abstraction. |";
33
33
  const specificSkills = ctx.api
34
- ? "`@beignet/next#routes-server`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`"
35
- : "`@beignet/next#routes-server`, `@beignet/react-query#client`, `@beignet/react-hook-form#forms`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`";
34
+ ? "`@beignet/next#routes-server`, `@beignet/web#fetch-server`, `@beignet/devtools#runtime-safety`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`"
35
+ : "`@beignet/next#routes-server`, `@beignet/web#fetch-server`, `@beignet/devtools#runtime-safety`, `@beignet/react-query#client`, `@beignet/react-hook-form#forms`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`";
36
36
 
37
37
  return `# ${ctx.name} — agent guide
38
38
 
@@ -62,6 +62,9 @@ use, so their absence is fine. \`${cli} make event\`, \`${cli} make job\`,
62
62
  \`${cli} make listener\`, \`${cli} make schedule\`, \`${cli} make task\`,
63
63
  \`${cli} make seed\`, and \`${cli} make upload\` create or update their
64
64
  required app entrypoints.
65
+ Upload generation validates the central \`uploadRegistry = defineUploads({ ... })\`
66
+ shape before writing; when a custom route cannot be edited safely, follow the
67
+ manual registry instruction in the error.
65
68
  \`${cli} doctor\` detects registration drift. Preview exact repair patches
66
69
  with \`${cli} doctor --fix --dry-run\`, then apply the returned plan with
67
70
  \`${cli} doctor --fix --plan <plan-id>\` and optional
@@ -115,7 +118,11 @@ need one check alone.
115
118
 
116
119
  Before exploring unfamiliar code, run \`${cli} map\` for the human inventory
117
120
  or \`${cli} map --json --feature <name>\` for an evidence-backed graph scoped
118
- to one feature.
121
+ to one feature. The graph honors the app's complete TypeScript \`baseUrl\` and
122
+ \`paths\` configuration. Fix \`local_import_unresolved\` and
123
+ \`contract_declaration_unresolved\` entries rather than treating their missing
124
+ edges as proof that no relationship exists. Registration diagnostics expose
125
+ the declaration as \`subject.file\` and \`subject.exportName\`.
119
126
 
120
127
  ## Package skills
121
128
 
@@ -122,7 +122,9 @@ export function packageJson(ctx: TemplateContext): string {
122
122
  function intentSkillPackages(ctx: TemplateContext): string[] {
123
123
  const packages = [
124
124
  "@beignet/core",
125
+ "@beignet/devtools",
125
126
  "@beignet/next",
127
+ "@beignet/web",
126
128
  "@beignet/cli",
127
129
  "@beignet/provider-auth-better-auth",
128
130
  "@beignet/provider-db-drizzle",
@@ -362,7 +364,7 @@ ${optionalAuthRedirects}
362
364
  ? ""
363
365
  : "; it refuses non-local hosts unless `BEIGNET_ALLOW_DATABASE_RESET=true`"
364
366
  }.
365
- - Remove \`DEVTOOLS_ENABLED=true\` in production unless you add authentication and stricter redaction.
367
+ - Remove \`DEVTOOLS_ENABLED=true\` outside development unless you add authentication and stricter redaction.
366
368
  - Set \`APP_URL\`, \`BETTER_AUTH_SECRET\`, \`LOG_LEVEL\`, and provider-specific variables in your hosting environment.
367
369
  - Set \`BETTER_AUTH_TRUSTED_ORIGINS\` before serving auth across multiple origins.
368
370
  - Review the starter authorization policy before exposing user-owned data.
@@ -463,7 +465,7 @@ export function envExample(ctx: TemplateContext): string {
463
465
  "# Copy to .env.local and fill in values for your app.",
464
466
  "APP_URL=http://localhost:3000",
465
467
  "",
466
- "# Dev-only default; remove or protect in production.",
468
+ "# Local-development default; remove or protect outside development.",
467
469
  "DEVTOOLS_ENABLED=true",
468
470
  "",
469
471
  "LOG_LEVEL=info",
@@ -126,9 +126,9 @@ export const todos = mysqlTable(
126
126
  // unmapped, and list ordering compares the strings lexicographically.
127
127
  createdAt: varchar("created_at", { length: 32 }).notNull(),
128
128
  },
129
- (table) => ({
130
- userIdx: index("todos_user_idx").on(table.userId, table.createdAt),
131
- }),
129
+ (table) => [
130
+ index("todos_user_idx").on(table.userId, table.createdAt),
131
+ ],
132
132
  );
133
133
  `,
134
134
  drizzleTodoRepository: `import "@beignet/core/server-only";
@@ -116,9 +116,9 @@ export const todos = pgTable(
116
116
  // unmapped, and list ordering compares the strings lexicographically.
117
117
  createdAt: text("created_at").notNull(),
118
118
  },
119
- (table) => ({
120
- userIdx: index("todos_user_idx").on(table.userId, table.createdAt),
121
- }),
119
+ (table) => [
120
+ index("todos_user_idx").on(table.userId, table.createdAt),
121
+ ],
122
122
  );
123
123
  `,
124
124
  drizzleTodoRepository: `import "@beignet/core/server-only";
@@ -112,9 +112,9 @@ export const todos = sqliteTable(
112
112
  .default(false),
113
113
  createdAt: text("created_at").notNull(),
114
114
  },
115
- (table) => ({
116
- userIdx: index("todos_user_idx").on(table.userId, table.createdAt),
117
- }),
115
+ (table) => [
116
+ index("todos_user_idx").on(table.userId, table.createdAt),
117
+ ],
118
118
  );
119
119
  `,
120
120
  drizzleTodoRepository: `import "@beignet/core/server-only";
@@ -34,7 +34,7 @@ export function serverProviders(ctx: TemplateContext): string {
34
34
 
35
35
  const entries = [
36
36
  "\tcreateDevtoolsProvider(),",
37
- "\tcreateBetterAuthProvider<AuthUser, AuthSessionMetadata>(auth),",
37
+ "\tcreateBetterAuthProvider<AuthUser, AuthSessionMetadata>({ auth }),",
38
38
  "\tcreatePinoLoggerProvider(),",
39
39
  `\t${db.providerInstance},`,
40
40
  "\tstarterDatabaseProvider,",
@@ -196,12 +196,12 @@ export const env = createEnv({
196
196
  NODE_ENV: z
197
197
  .enum(["development", "test", "production"])
198
198
  .default("development"),
199
- APP_URL: z.string().url().default("http://localhost:3000"),
199
+ APP_URL: z.url().default("http://localhost:3000"),
200
200
  DEVTOOLS_ENABLED: BooleanEnv.optional(),
201
201
  BETTER_AUTH_SECRET: isProductionRuntime
202
202
  ? BetterAuthSecret
203
203
  : BetterAuthSecret.default(LOCAL_BETTER_AUTH_SECRET),
204
- BETTER_AUTH_URL: z.string().url().default("http://localhost:3000"),
204
+ BETTER_AUTH_URL: z.url().default("http://localhost:3000"),
205
205
  BETTER_AUTH_TRUSTED_ORIGINS: z.string().optional(),
206
206
  ${databaseEnvLines}
207
207
  LOG_LEVEL: z
@@ -217,7 +217,7 @@ ${inngestEnvLines}
217
217
  export const isProduction = env.NODE_ENV === "production";
218
218
 
219
219
  export const isDevtoolsEnabled =
220
- env.DEVTOOLS_ENABLED ?? process.env.NODE_ENV !== "production";
220
+ env.DEVTOOLS_ENABLED ?? env.NODE_ENV === "development";
221
221
  `;
222
222
  }
223
223
 
@@ -2,7 +2,7 @@ const files = {
2
2
  schemas: `import { z } from "zod";
3
3
 
4
4
  export const TodoSchema = z.object({
5
- id: z.string().uuid(),
5
+ id: z.uuid(),
6
6
  userId: z.string(),
7
7
  title: z.string().min(1),
8
8
  completed: z.boolean(),
@@ -26,7 +26,7 @@ export const ListTodosOutputSchema = z.object({
26
26
  });
27
27
 
28
28
  export const TodoIdInputSchema = z.object({
29
- id: z.string().uuid(),
29
+ id: z.uuid(),
30
30
  });
31
31
 
32
32
  export const CreateTodoInputSchema = z.object({