@archlast/cli 0.0.1

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 (92) hide show
  1. package/README.md +141 -0
  2. package/dist/analyzer.d.ts +96 -0
  3. package/dist/analyzer.d.ts.map +1 -0
  4. package/dist/analyzer.js +404 -0
  5. package/dist/auth.d.ts +14 -0
  6. package/dist/auth.d.ts.map +1 -0
  7. package/dist/auth.js +106 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +322875 -0
  11. package/dist/commands/build.d.ts +6 -0
  12. package/dist/commands/build.d.ts.map +1 -0
  13. package/dist/commands/build.js +36 -0
  14. package/dist/commands/config.d.ts +8 -0
  15. package/dist/commands/config.d.ts.map +1 -0
  16. package/dist/commands/config.js +23 -0
  17. package/dist/commands/data.d.ts +6 -0
  18. package/dist/commands/data.d.ts.map +1 -0
  19. package/dist/commands/data.js +300 -0
  20. package/dist/commands/deploy.d.ts +9 -0
  21. package/dist/commands/deploy.d.ts.map +1 -0
  22. package/dist/commands/deploy.js +59 -0
  23. package/dist/commands/dev.d.ts +10 -0
  24. package/dist/commands/dev.d.ts.map +1 -0
  25. package/dist/commands/dev.js +132 -0
  26. package/dist/commands/generate.d.ts +6 -0
  27. package/dist/commands/generate.d.ts.map +1 -0
  28. package/dist/commands/generate.js +100 -0
  29. package/dist/commands/init.d.ts +7 -0
  30. package/dist/commands/init.d.ts.map +1 -0
  31. package/dist/commands/logs.d.ts +10 -0
  32. package/dist/commands/logs.d.ts.map +1 -0
  33. package/dist/commands/logs.js +38 -0
  34. package/dist/commands/pull.d.ts +16 -0
  35. package/dist/commands/pull.d.ts.map +1 -0
  36. package/dist/commands/pull.js +415 -0
  37. package/dist/commands/restart.d.ts +11 -0
  38. package/dist/commands/restart.d.ts.map +1 -0
  39. package/dist/commands/restart.js +63 -0
  40. package/dist/commands/start.d.ts +11 -0
  41. package/dist/commands/start.d.ts.map +1 -0
  42. package/dist/commands/start.js +74 -0
  43. package/dist/commands/status.d.ts +8 -0
  44. package/dist/commands/status.d.ts.map +1 -0
  45. package/dist/commands/status.js +69 -0
  46. package/dist/commands/stop.d.ts +8 -0
  47. package/dist/commands/stop.d.ts.map +1 -0
  48. package/dist/commands/stop.js +23 -0
  49. package/dist/commands/upgrade.d.ts +12 -0
  50. package/dist/commands/upgrade.d.ts.map +1 -0
  51. package/dist/commands/upgrade.js +77 -0
  52. package/dist/docker/compose.d.ts +3 -0
  53. package/dist/docker/compose.d.ts.map +1 -0
  54. package/dist/docker/compose.js +47 -0
  55. package/dist/docker/config.d.ts +12 -0
  56. package/dist/docker/config.d.ts.map +1 -0
  57. package/dist/docker/config.js +183 -0
  58. package/dist/docker/manager.d.ts +19 -0
  59. package/dist/docker/manager.d.ts.map +1 -0
  60. package/dist/docker/manager.js +239 -0
  61. package/dist/docker/ports.d.ts +6 -0
  62. package/dist/docker/ports.d.ts.map +1 -0
  63. package/dist/docker/restart-on-deploy.d.ts +6 -0
  64. package/dist/docker/restart-on-deploy.d.ts.map +1 -0
  65. package/dist/docker/types.d.ts +36 -0
  66. package/dist/docker/types.d.ts.map +1 -0
  67. package/dist/docker/types.js +1 -0
  68. package/dist/events-listener.d.ts +19 -0
  69. package/dist/events-listener.d.ts.map +1 -0
  70. package/dist/events-listener.js +105 -0
  71. package/dist/generator.d.ts +44 -0
  72. package/dist/generator.d.ts.map +1 -0
  73. package/dist/generator.js +1816 -0
  74. package/dist/generators/di.d.ts +21 -0
  75. package/dist/generators/di.d.ts.map +1 -0
  76. package/dist/generators/di.js +100 -0
  77. package/dist/index.d.ts +7 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +4 -0
  80. package/dist/project.d.ts +18 -0
  81. package/dist/project.d.ts.map +1 -0
  82. package/dist/protocol.d.ts +58 -0
  83. package/dist/protocol.d.ts.map +1 -0
  84. package/dist/protocol.js +5 -0
  85. package/dist/uploader.d.ts +63 -0
  86. package/dist/uploader.d.ts.map +1 -0
  87. package/dist/uploader.js +255 -0
  88. package/dist/watcher.d.ts +13 -0
  89. package/dist/watcher.d.ts.map +1 -0
  90. package/dist/watcher.js +38 -0
  91. package/package.json +58 -0
  92. package/scripts/postinstall.cjs +65 -0
@@ -0,0 +1,1816 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { join } from "path";
4
+ import { Project, SyntaxKind } from "ts-morph";
5
+ import { generateDI } from "./generators/di";
6
+ // Helper to resolve imports
7
+ function resolveImport(typeText, sourceFile) {
8
+ return typeText.replace(/import\("([^"]+)"\)\./g, (match, path) => {
9
+ if (path.includes("packages/server/src/db/sqlite")) {
10
+ return "Document";
11
+ }
12
+ return "";
13
+ });
14
+ }
15
+ export class TypeGenerator {
16
+ archlastPath;
17
+ constructor(archlastPath) {
18
+ this.archlastPath = path.resolve(archlastPath);
19
+ }
20
+ async generate(analysis) {
21
+ const srcPath = path.join(this.archlastPath, "src");
22
+ const generatedDir = path.join(this.archlastPath, "_generated");
23
+ // Ensure directory exists
24
+ if (!fs.existsSync(generatedDir)) {
25
+ fs.mkdirSync(generatedDir, { recursive: true });
26
+ }
27
+ // Generate types using ts-morph for sophisticated type inference
28
+ const typeInfos = await this.analyzeTypesWithTsMorph(srcPath);
29
+ const rpcTypeInfos = await this.analyzeRpcTypesWithTsMorph(srcPath);
30
+ const apiTypes = await this.generateApiTypes(typeInfos);
31
+ const serverTypes = await this.generateServerTypes();
32
+ const indexExport = this.generateIndexExport();
33
+ const rpcTypes = await this.generateRpcTypes(rpcTypeInfos);
34
+ const trpcRouter = await this.generateTrpcRouter(rpcTypeInfos);
35
+ const apiPath = path.join(generatedDir, "api.ts");
36
+ const serverPath = path.join(generatedDir, "server.ts");
37
+ const indexPath = path.join(generatedDir, "index.ts");
38
+ const rpcPath = path.join(generatedDir, "rpc.ts");
39
+ const trpcRouterPath = path.join(generatedDir, "trpc-router.ts");
40
+ fs.writeFileSync(apiPath, apiTypes, "utf-8");
41
+ fs.writeFileSync(serverPath, serverTypes, "utf-8");
42
+ fs.writeFileSync(indexPath, indexExport, "utf-8");
43
+ fs.writeFileSync(rpcPath, rpcTypes, "utf-8");
44
+ fs.writeFileSync(trpcRouterPath, trpcRouter, "utf-8");
45
+ // Generate CRUD handlers
46
+ await this.generateCrudHandlers(generatedDir);
47
+ // Generate DI registration code if there are injectables
48
+ if (analysis.injectables && analysis.injectables.length > 0) {
49
+ await generateDI(generatedDir, analysis.injectables);
50
+ console.log(` Generated DI registration with ${analysis.injectables.length} providers`);
51
+ }
52
+ // Log RPC generation
53
+ if (rpcTypeInfos.length > 0) {
54
+ console.log(` Generated ${rpcTypeInfos.length} RPC procedure types`);
55
+ console.log(` Generated tRPC router with AppRouter type`);
56
+ }
57
+ }
58
+ async analyzeTypesWithTsMorph(srcPath) {
59
+ const project = new Project({
60
+ skipAddingFilesFromTsConfig: true,
61
+ });
62
+ project.addSourceFilesAtPaths(path.join(srcPath, "**/*.ts"));
63
+ const typeInfos = [];
64
+ const sourceFiles = project.getSourceFiles();
65
+ for (const sourceFile of sourceFiles) {
66
+ const fileName = sourceFile.getBaseName();
67
+ const moduleName = fileName.replace(/\.ts$/, "");
68
+ const exportedVariables = sourceFile
69
+ .getVariableDeclarations()
70
+ .filter((v) => v.isExported());
71
+ for (const variable of exportedVariables) {
72
+ const name = variable.getName();
73
+ const initializer = variable.getInitializer();
74
+ if (initializer && initializer.getKind() === SyntaxKind.CallExpression) {
75
+ const callExpr = initializer.asKindOrThrow(SyntaxKind.CallExpression);
76
+ const functionName = callExpr.getExpression().getText();
77
+ if (functionName === "mutation" ||
78
+ functionName === "query" ||
79
+ functionName === "action") {
80
+ const typeInfo = this.extractTypeInfo(callExpr, moduleName, name, functionName, sourceFile);
81
+ if (typeInfo) {
82
+ typeInfos.push(typeInfo);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ return typeInfos;
89
+ }
90
+ async analyzeRpcTypesWithTsMorph(srcPath) {
91
+ const project = new Project({
92
+ skipAddingFilesFromTsConfig: true,
93
+ });
94
+ project.addSourceFilesAtPaths(path.join(srcPath, "**/*.ts"));
95
+ const typeInfos = [];
96
+ const sourceFiles = project.getSourceFiles();
97
+ for (const sourceFile of sourceFiles) {
98
+ const fileName = sourceFile.getBaseName();
99
+ const moduleName = fileName.replace(/\.ts$/, "");
100
+ const exportedVariables = sourceFile
101
+ .getVariableDeclarations()
102
+ .filter((v) => v.isExported());
103
+ for (const variable of exportedVariables) {
104
+ const name = variable.getName();
105
+ const initializer = variable.getInitializer();
106
+ if (initializer && initializer.getKind() === SyntaxKind.CallExpression) {
107
+ const callExpr = initializer.asKindOrThrow(SyntaxKind.CallExpression);
108
+ const callText = callExpr.getExpression().getText();
109
+ // Check for rpc.query or rpc.mutation
110
+ if (callText === "rpc.query" || callText === "rpc.mutation") {
111
+ const typeInfo = this.extractRpcTypeInfo(callExpr, moduleName, name, callText === "rpc.query" ? "query" : "mutation", sourceFile);
112
+ if (typeInfo) {
113
+ typeInfos.push(typeInfo);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ return typeInfos;
120
+ }
121
+ extractRpcTypeInfo(callExpr, moduleName, name, procedureType, sourceFile) {
122
+ const firstArg = callExpr.getArguments()[0];
123
+ let handlerFunc = null;
124
+ let authMode = undefined;
125
+ if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
126
+ const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression);
127
+ const handlerProp = obj?.getProperty("handler");
128
+ if (handlerProp && handlerProp.getKind() === SyntaxKind.PropertyAssignment) {
129
+ handlerFunc = handlerProp
130
+ .asKind(SyntaxKind.PropertyAssignment)
131
+ ?.getInitializer()
132
+ ?.asKind(SyntaxKind.ArrowFunction);
133
+ }
134
+ // Extract auth mode
135
+ const authProp = obj?.getProperty("auth");
136
+ if (authProp && authProp.getKind() === SyntaxKind.PropertyAssignment) {
137
+ const authInit = authProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
138
+ if (authInit && authInit.getKind() === SyntaxKind.StringLiteral) {
139
+ const authValue = authInit.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
140
+ if (authValue === "required" ||
141
+ authValue === "optional" ||
142
+ authValue === "public") {
143
+ authMode = authValue;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ if (!handlerFunc)
149
+ return null;
150
+ let argsType = "Record<string, never>";
151
+ let hasArgs = false;
152
+ // Extract args from Zod schema
153
+ if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
154
+ const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression);
155
+ const argsProp = obj?.getProperty("args");
156
+ if (argsProp && argsProp.getKind() === SyntaxKind.PropertyAssignment) {
157
+ const argsInit = argsProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
158
+ if (argsInit) {
159
+ // Handle both direct object literal { id: z.string() } and z.object({ id: z.string() })
160
+ if (argsInit.getKind() === SyntaxKind.ObjectLiteralExpression) {
161
+ argsType = this.parseZodSchema(argsInit);
162
+ hasArgs = true;
163
+ }
164
+ else if (argsInit.getKind() === SyntaxKind.CallExpression) {
165
+ // Check if it's z.object({ ... })
166
+ const callText = argsInit.getText();
167
+ if (callText.startsWith("z.object(") || callText.startsWith("v.object(")) {
168
+ // Get the first argument of z.object() call
169
+ const callExprArgs = argsInit.asKind(SyntaxKind.CallExpression)?.getArguments();
170
+ if (callExprArgs && callExprArgs.length > 0) {
171
+ const objectArg = callExprArgs[0];
172
+ if (objectArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
173
+ argsType = this.parseZodSchema(objectArg);
174
+ hasArgs = true;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ // Fallback to inferred args type
183
+ if (!hasArgs) {
184
+ const params = handlerFunc.getParameters();
185
+ if (params.length >= 2) {
186
+ const argsParam = params[1];
187
+ const typeObj = argsParam.getType();
188
+ if (typeObj.isObject() &&
189
+ !typeObj.isArray() &&
190
+ typeObj.getProperties().length > 0) {
191
+ const props = typeObj.getProperties().map((p) => {
192
+ const decl = p.getValueDeclaration() ?? p.getDeclarations()[0];
193
+ const propType = decl
194
+ ? decl
195
+ .getType()
196
+ .getText(decl)
197
+ .replace(/import\("[^"]+"\)\./g, "")
198
+ : "any";
199
+ return `${p.getName()}: ${propType}`;
200
+ });
201
+ argsType = `{ ${props.join("; ")} }`;
202
+ hasArgs = true;
203
+ }
204
+ }
205
+ }
206
+ // Extract return type
207
+ let returnType = this.inferReturnType(handlerFunc, sourceFile);
208
+ // Calculate import path relative to _generated directory
209
+ // sourceFile.getFilePath() gives absolute path, we need relative path like ../src/rpc-procedures
210
+ const filePath = sourceFile.getFilePath();
211
+ const srcDir = path.join(this.archlastPath, "src");
212
+ let relativePath = path.relative(srcDir, filePath);
213
+ // Remove .ts extension
214
+ relativePath = relativePath.replace(/\.ts$/, "");
215
+ // Create import path from _generated: ../src/filename
216
+ const importPath = `../src/${relativePath}`;
217
+ return {
218
+ name,
219
+ moduleName,
220
+ procedureType,
221
+ argsType,
222
+ returnType,
223
+ auth: authMode,
224
+ importPath,
225
+ hasArgs,
226
+ };
227
+ }
228
+ extractTypeInfo(callExpr, moduleName, name, type, sourceFile) {
229
+ const firstArg = callExpr.getArguments()[0];
230
+ let handlerFunc = null;
231
+ let authMode = undefined;
232
+ if (firstArg.getKind() === SyntaxKind.ArrowFunction) {
233
+ handlerFunc = firstArg.asKind(SyntaxKind.ArrowFunction);
234
+ }
235
+ else if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
236
+ const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression);
237
+ const handlerProp = obj?.getProperty("handler");
238
+ if (handlerProp && handlerProp.getKind() === SyntaxKind.PropertyAssignment) {
239
+ handlerFunc = handlerProp
240
+ .asKind(SyntaxKind.PropertyAssignment)
241
+ ?.getInitializer()
242
+ ?.asKind(SyntaxKind.ArrowFunction);
243
+ }
244
+ // Extract auth mode
245
+ const authProp = obj?.getProperty("auth");
246
+ if (authProp && authProp.getKind() === SyntaxKind.PropertyAssignment) {
247
+ const authInit = authProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
248
+ if (authInit && authInit.getKind() === SyntaxKind.StringLiteral) {
249
+ const authValue = authInit.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
250
+ if (authValue === "required" ||
251
+ authValue === "optional" ||
252
+ authValue === "public") {
253
+ authMode = authValue;
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (!handlerFunc)
259
+ return null;
260
+ let argsType = "Record<string, never>";
261
+ // Extract args from Zod schema
262
+ if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
263
+ const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression);
264
+ const argsProp = obj?.getProperty("args");
265
+ if (argsProp && argsProp.getKind() === SyntaxKind.PropertyAssignment) {
266
+ const argsInit = argsProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
267
+ if (argsInit) {
268
+ // Handle both direct object literal { id: z.string() } and z.object({ id: z.string() })
269
+ if (argsInit.getKind() === SyntaxKind.ObjectLiteralExpression) {
270
+ argsType = this.parseZodSchema(argsInit);
271
+ }
272
+ else if (argsInit.getKind() === SyntaxKind.CallExpression) {
273
+ // Check if it's z.object({ ... })
274
+ const callText = argsInit.getText();
275
+ if (callText.startsWith("z.object(") || callText.startsWith("v.object(")) {
276
+ // Get the first argument of z.object() call
277
+ const callExprArgs = argsInit.asKind(SyntaxKind.CallExpression)?.getArguments();
278
+ if (callExprArgs && callExprArgs.length > 0) {
279
+ const objectArg = callExprArgs[0];
280
+ if (objectArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
281
+ argsType = this.parseZodSchema(objectArg);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ // Fallback to inferred args type
290
+ if (argsType === "Record<string, never>") {
291
+ const params = handlerFunc.getParameters();
292
+ if (params.length >= 2) {
293
+ const argsParam = params[1];
294
+ const typeObj = argsParam.getType();
295
+ if (typeObj.isObject() &&
296
+ !typeObj.isArray() &&
297
+ typeObj.getProperties().length > 0) {
298
+ const props = typeObj.getProperties().map((p) => {
299
+ const decl = p.getValueDeclaration() ?? p.getDeclarations()[0];
300
+ const propType = decl
301
+ ? decl
302
+ .getType()
303
+ .getText(decl)
304
+ .replace(/import\("[^"]+"\)\./g, "")
305
+ : "any";
306
+ return `${p.getName()}: ${propType}`;
307
+ });
308
+ argsType = `{ ${props.join("; ")} }`;
309
+ }
310
+ }
311
+ }
312
+ // Extract return type
313
+ let returnType = this.inferReturnType(handlerFunc, sourceFile);
314
+ return {
315
+ name,
316
+ moduleName,
317
+ type,
318
+ argsType,
319
+ returnType,
320
+ auth: authMode,
321
+ };
322
+ }
323
+ parseZodSchema(argsInit) {
324
+ const props = argsInit.asKind(SyntaxKind.ObjectLiteralExpression)?.getProperties();
325
+ const typeProps = [];
326
+ props?.forEach((p) => {
327
+ const prop = p.asKind(SyntaxKind.PropertyAssignment);
328
+ if (prop) {
329
+ const propName = prop.getName();
330
+ const init = prop.getInitializer();
331
+ let propType = "any";
332
+ let optional = false;
333
+ const text = init?.getText() || "";
334
+ if (text.includes("v.optional(") || text.includes("z.optional(")) {
335
+ optional = true;
336
+ }
337
+ if (text.includes("v.string()") || text.includes("z.string()")) {
338
+ propType = "string";
339
+ }
340
+ else if (text.includes("v.number()") || text.includes("z.number()")) {
341
+ propType = "number";
342
+ }
343
+ else if (text.includes("v.boolean()") || text.includes("z.boolean()")) {
344
+ propType = "boolean";
345
+ }
346
+ else if (text.includes("v.array(") || text.includes("z.array(")) {
347
+ if (text.includes("v.object(") || text.includes("z.object(")) {
348
+ propType = "any[]";
349
+ }
350
+ else if (text.includes("v.string()") || text.includes("z.string()")) {
351
+ propType = "string[]";
352
+ }
353
+ else if (text.includes("v.number()") || text.includes("z.number()")) {
354
+ propType = "number[]";
355
+ }
356
+ else {
357
+ propType = "any[]";
358
+ }
359
+ }
360
+ else if (text.includes("v.object(") || text.includes("z.object(")) {
361
+ propType = "Record<string, any>";
362
+ }
363
+ if (optional) {
364
+ propType = `${propType} | undefined`;
365
+ }
366
+ typeProps.push(`${propName}: ${propType};`);
367
+ }
368
+ });
369
+ return typeProps.length > 0 ? `{ ${typeProps.join(" ")} }` : "Record<string, never>";
370
+ }
371
+ inferReturnType(handlerFunc, sourceFile) {
372
+ const returnTypeNode = handlerFunc.getReturnType();
373
+ let returnTypeText = returnTypeNode.getText();
374
+ // Unwrap Promise<T> -> T
375
+ if (returnTypeText.startsWith("Promise<") && returnTypeText.endsWith(">")) {
376
+ returnTypeText = returnTypeText.slice(8, -1);
377
+ }
378
+ // Clean up return type if it contains import paths
379
+ returnTypeText = resolveImport(returnTypeText, sourceFile);
380
+ // Replace any with DataModel types even if TypeScript inferred it
381
+ if (returnTypeText.includes("any")) {
382
+ const body = handlerFunc.getBody();
383
+ if (body) {
384
+ const bodyText = body.getText();
385
+ // More flexible regex to handle multiline method chaining - match either direct call or chained call
386
+ let tableMatch = bodyText.match(/\.(query|list|get|insert|update|delete)\s*\(\s*["'](\w+)["']/);
387
+ const tableName = tableMatch ? tableMatch[2] : null;
388
+ if (tableName) {
389
+ const tableType = `DataModel["${tableName}"]`;
390
+ // Check what operation is being performed
391
+ const isFindMany = bodyText.includes(".findMany()") || bodyText.includes(".list(");
392
+ const isNullable = bodyText.includes(".findFirst()") ||
393
+ bodyText.includes(".findUnique()") ||
394
+ bodyText.includes(".get(");
395
+ // Replace based on operation or existing type
396
+ if (returnTypeText === "any[]" || (returnTypeText === "any" && isFindMany)) {
397
+ returnTypeText = `${tableType}[]`;
398
+ }
399
+ else if (returnTypeText === "any | null" ||
400
+ returnTypeText === "any|null" ||
401
+ (returnTypeText === "any" && isNullable)) {
402
+ returnTypeText = `${tableType} | null`;
403
+ }
404
+ }
405
+ }
406
+ }
407
+ // If we got an object literal with 'any' fields, try to refine the types from the body
408
+ if (returnTypeText.includes(": any")) {
409
+ const body = handlerFunc.getBody();
410
+ if (body) {
411
+ const bodyText = body.getText();
412
+ // Extract field names from the return type
413
+ const fieldMatches = returnTypeText.matchAll(/(\w+):\s*any/g);
414
+ const fields = Array.from(fieldMatches, (m) => m[1]);
415
+ if (fields.length > 0) {
416
+ // Extract table name for type inference (handle multiline)
417
+ const tableMatch = bodyText.match(/ctx\.db\.(query|list|get|insert|update|delete)\s*\(\s*["'](\w+)["']/s);
418
+ const tableName = tableMatch ? tableMatch[2] : null;
419
+ const tableType = tableName ? `DataModel["${tableName}"]` : "any";
420
+ const refinedFields = fields.map((field) => {
421
+ // Check Promise.all destructuring first
422
+ const destructureMatch = bodyText.match(/const\s*\[([^\]]+)\]\s*=\s*await\s+Promise\.all\s*\(\s*\[([\s\S]+?)\]\s*\)/);
423
+ if (destructureMatch) {
424
+ const vars = destructureMatch[1]
425
+ .split(",")
426
+ .map((v) => v.trim());
427
+ const fieldIndex = vars.indexOf(field);
428
+ if (fieldIndex !== -1) {
429
+ const arrayContent = destructureMatch[2];
430
+ const expressions = [];
431
+ let currentExpr = "";
432
+ let depth = 0;
433
+ for (let i = 0; i < arrayContent.length; i++) {
434
+ const char = arrayContent[i];
435
+ if (char === "(" || char === "[" || char === "{")
436
+ depth++;
437
+ else if (char === ")" || char === "]" || char === "}")
438
+ depth--;
439
+ if (char === "," && depth === 0) {
440
+ expressions.push(currentExpr.trim());
441
+ currentExpr = "";
442
+ }
443
+ else {
444
+ currentExpr += char;
445
+ }
446
+ }
447
+ if (currentExpr.trim())
448
+ expressions.push(currentExpr.trim());
449
+ if (expressions[fieldIndex]) {
450
+ const expr = expressions[fieldIndex].trim();
451
+ if (expr.includes(".count()"))
452
+ return `${field}: number`;
453
+ if (expr.includes(".findMany()") || expr.includes(".list("))
454
+ return `${field}: ${tableType}[]`;
455
+ if (expr.includes(".findFirst()") ||
456
+ expr.includes(".findUnique()") ||
457
+ expr.includes(".get("))
458
+ return `${field}: ${tableType} | null`;
459
+ }
460
+ }
461
+ }
462
+ // Look for variable declarations
463
+ const declPattern = new RegExp(`(const|let|var)\\s+${field}\\s*=\\s*(?:await\\s+)?(.+?)(?:;|\\n)`, "s");
464
+ const match = declPattern.exec(bodyText);
465
+ if (match) {
466
+ const rhs = match[2].trim();
467
+ if (rhs.includes(".count()"))
468
+ return `${field}: number`;
469
+ if (rhs.includes(".findMany()") || rhs.includes(".list("))
470
+ return `${field}: ${tableType}[]`;
471
+ if (rhs.includes(".findFirst()") ||
472
+ rhs.includes(".findUnique()") ||
473
+ rhs.includes(".get("))
474
+ return `${field}: ${tableType} | null`;
475
+ if (rhs.includes(".insert("))
476
+ return `${field}: string`;
477
+ if (rhs.includes(".insertMany("))
478
+ return `${field}: string[]`;
479
+ if (rhs.includes(".updateMany()") || rhs.includes(".deleteMany()"))
480
+ return `${field}: number`;
481
+ }
482
+ return `${field}: any`;
483
+ });
484
+ // Reconstruct the type
485
+ returnTypeText = `{ ${refinedFields.join("; ")} }`;
486
+ }
487
+ }
488
+ }
489
+ // Better handling of common return types
490
+ if (returnTypeText === "any" || returnTypeText === "void" || returnTypeText === "never") {
491
+ // Try to infer from the body
492
+ const body = handlerFunc.getBody();
493
+ if (body) {
494
+ const bodyText = body.getText();
495
+ // Extract table name from db operations (handle multiline)
496
+ const tableMatch = bodyText.match(/ctx\.db\.(query|list|get|insert|update|delete)\s*\(\s*["'](\w+)["']/s);
497
+ const tableName = tableMatch ? tableMatch[2] : null;
498
+ const tableType = tableName ? `DataModel["${tableName}"]` : "any";
499
+ // Check for common db patterns (check more specific ones first)
500
+ if (bodyText.includes(".insertMany(")) {
501
+ returnTypeText = "string[]";
502
+ }
503
+ else if (bodyText.includes(".updateMany(") || bodyText.includes(".deleteMany(")) {
504
+ returnTypeText = "number";
505
+ }
506
+ else if (bodyText.includes(".findMany()") || bodyText.includes(".list(")) {
507
+ returnTypeText = `${tableType}[]`;
508
+ }
509
+ else if (bodyText.includes(".findFirst()") ||
510
+ bodyText.includes(".findUnique()")) {
511
+ returnTypeText = `${tableType} | null`;
512
+ }
513
+ else if (bodyText.includes(".get(")) {
514
+ returnTypeText = `${tableType} | null`;
515
+ }
516
+ else if (bodyText.includes(".count()")) {
517
+ // Check if it's a final return or intermediate
518
+ if (bodyText.match(/return\s+.*\.count\(\)/)) {
519
+ returnTypeText = "number";
520
+ }
521
+ }
522
+ else if (bodyText.includes(".insert(")) {
523
+ returnTypeText = "string";
524
+ }
525
+ else if (bodyText.includes(".update(") || bodyText.includes(".delete(")) {
526
+ // Check if it's the last operation
527
+ if (bodyText.match(/return\s+.*\.(?:update|delete)\(/)) {
528
+ returnTypeText = "void";
529
+ }
530
+ }
531
+ // Object literal return - try to extract shape
532
+ const objMatch = bodyText.match(/return\s*{\s*([^}]+)}\s*;?/);
533
+ if (objMatch && returnTypeText === "any") {
534
+ const fields = objMatch[1]
535
+ .split(",")
536
+ .map((f) => {
537
+ const parts = f.trim().split(":");
538
+ if (parts.length === 1) {
539
+ // Shorthand property
540
+ return parts[0].trim();
541
+ }
542
+ return parts[0].trim();
543
+ })
544
+ .filter(Boolean);
545
+ if (fields.length > 0) {
546
+ // Try to infer field types from variable declarations
547
+ const fieldTypes = fields.map((field) => {
548
+ // Check Promise.all destructuring first
549
+ const destructureMatch = bodyText.match(/const\s*\[([^\]]+)\]\s*=\s*await\s+Promise\.all\s*\(\s*\[([\s\S]+?)\]\s*\)/);
550
+ if (destructureMatch) {
551
+ const vars = destructureMatch[1]
552
+ .split(",")
553
+ .map((v) => v.trim());
554
+ const fieldIndex = vars.indexOf(field);
555
+ if (fieldIndex !== -1) {
556
+ const arrayContent = destructureMatch[2];
557
+ const expressions = [];
558
+ let currentExpr = "";
559
+ let depth = 0;
560
+ for (let i = 0; i < arrayContent.length; i++) {
561
+ const char = arrayContent[i];
562
+ if (char === "(" || char === "[" || char === "{")
563
+ depth++;
564
+ else if (char === ")" || char === "]" || char === "}")
565
+ depth--;
566
+ if (char === "," && depth === 0) {
567
+ expressions.push(currentExpr.trim());
568
+ currentExpr = "";
569
+ }
570
+ else {
571
+ currentExpr += char;
572
+ }
573
+ }
574
+ if (currentExpr.trim())
575
+ expressions.push(currentExpr.trim());
576
+ if (expressions[fieldIndex]) {
577
+ const expr = expressions[fieldIndex].trim();
578
+ if (expr.includes(".count()"))
579
+ return `${field}: number`;
580
+ if (expr.includes(".findMany()") || expr.includes(".list("))
581
+ return `${field}: ${tableType}[]`;
582
+ if (expr.includes(".findFirst()") ||
583
+ expr.includes(".findUnique()") ||
584
+ expr.includes(".get("))
585
+ return `${field}: ${tableType} | null`;
586
+ }
587
+ }
588
+ }
589
+ // More comprehensive field type detection - handle await and multiline
590
+ const fieldDeclPattern = new RegExp(`(const|let|var)\\s+${field}\\s*=\\s*(?:await\\s+)?(.+?)(?:;|\\n)`, "s");
591
+ const fieldDecl = fieldDeclPattern.exec(bodyText);
592
+ if (fieldDecl) {
593
+ const rhs = fieldDecl[2].trim();
594
+ if (rhs.includes(".count()"))
595
+ return `${field}: number`;
596
+ if (rhs.includes(".findMany()") || rhs.includes(".list("))
597
+ return `${field}: ${tableType}[]`;
598
+ if (rhs.includes(".findFirst()") ||
599
+ rhs.includes(".findUnique()") ||
600
+ rhs.includes(".get("))
601
+ return `${field}: ${tableType} | null`;
602
+ if (rhs.includes(".insert("))
603
+ return `${field}: string`;
604
+ if (rhs.includes(".insertMany("))
605
+ return `${field}: string[]`;
606
+ if (rhs.includes(".updateMany()") || rhs.includes(".deleteMany()"))
607
+ return `${field}: number`;
608
+ if (rhs.includes(".update(") || rhs.includes(".delete("))
609
+ return `${field}: void`;
610
+ }
611
+ return `${field}: any`;
612
+ });
613
+ returnTypeText = `{ ${fieldTypes.join("; ")} }`;
614
+ }
615
+ }
616
+ }
617
+ }
618
+ // Clean up complex types
619
+ returnTypeText = returnTypeText.replace(/import\("[^"]+"\)\./g, "");
620
+ return returnTypeText;
621
+ }
622
+ async generateDataModelFromSchema() {
623
+ // Check for both single file and folder-based schemas
624
+ const schemaPath = join(this.archlastPath, "src", "schema.ts");
625
+ const schemaFolderPath = join(this.archlastPath, "src", "schema");
626
+ const schemaIndexPath = join(schemaFolderPath, "index.ts");
627
+ let schemaContent = "";
628
+ let schemaSource = "";
629
+ if (await Bun.file(schemaPath).exists()) {
630
+ schemaContent = await Bun.file(schemaPath).text();
631
+ schemaSource = "schema.ts";
632
+ console.log(`Reading schema from ${schemaPath}`);
633
+ }
634
+ else if (await Bun.file(schemaIndexPath).exists()) {
635
+ schemaContent = await Bun.file(schemaIndexPath).text();
636
+ schemaSource = "schema/index.ts";
637
+ console.log(`Reading schema from ${schemaIndexPath}`);
638
+ }
639
+ else {
640
+ // Scan folder for schema files
641
+ const folderFiles = await this.scanSchemaFolder(schemaFolderPath);
642
+ schemaContent = folderFiles.map(f => f.content).join("\n");
643
+ schemaSource = "schema/*.ts";
644
+ console.log(`Reading schema from folder: ${schemaFolderPath}`);
645
+ }
646
+ // Extract table definitions from schema
647
+ // Supports both:
648
+ // 1. Standalone: export const tasks = defineTable({ ...
649
+ // 2. Inside defineSchema: tasks: defineTable({ ...
650
+ // Regex matches both patterns
651
+ const tableMatches = schemaContent.matchAll(/(?:export\s+(?:const|let)\s+)?(\w+)\s*(?::\s*\w+)?\s*[:=]\s*defineTable\s*\(\s*\{([\s\S]+?)\n\s*}(?:,\s*({[\s\S]+?\n\s*}))?\s*\)/g);
652
+ const tables = {};
653
+ for (const match of tableMatches) {
654
+ const tableName = match[1];
655
+ const fields = match[2];
656
+ const options = match[3];
657
+ console.log(`Processing table: ${tableName}`);
658
+ tables[tableName] = {
659
+ fields: [],
660
+ createFields: [],
661
+ updateFields: [],
662
+ fieldMetadata: {}
663
+ };
664
+ // Match field definitions line by line to avoid greedy regex issues
665
+ const lines = fields.split("\n");
666
+ for (const line of lines) {
667
+ const fieldMatch = line.match(/^\s*(\w+):\s*v\.([a-zA-Z]+)\(([^)]*)\)(.*)/);
668
+ if (!fieldMatch)
669
+ continue;
670
+ const fieldName = fieldMatch[1];
671
+ console.log(` Found field: ${fieldName}`);
672
+ if (fieldName === "_id")
673
+ continue;
674
+ const baseType = fieldMatch[2];
675
+ const modifiers = fieldMatch[4] || "";
676
+ let tsType = "any";
677
+ switch (baseType) {
678
+ case "string":
679
+ tsType = "string";
680
+ break;
681
+ case "number":
682
+ tsType = "number";
683
+ break;
684
+ case "boolean":
685
+ tsType = "boolean";
686
+ break;
687
+ case "id":
688
+ tsType = "string";
689
+ break;
690
+ case "date":
691
+ tsType = "Date";
692
+ break;
693
+ case "datetime":
694
+ tsType = "Date";
695
+ break;
696
+ case "array": {
697
+ const inner = (fieldMatch[3] || "").trim();
698
+ if (inner.includes("v.string") || inner.includes("z.string")) {
699
+ tsType = "string[]";
700
+ }
701
+ else if (inner.includes("v.number") || inner.includes("z.number")) {
702
+ tsType = "number[]";
703
+ }
704
+ else if (inner.includes("v.boolean") || inner.includes("z.boolean")) {
705
+ tsType = "boolean[]";
706
+ }
707
+ else {
708
+ tsType = "any[]";
709
+ }
710
+ break;
711
+ }
712
+ case "object":
713
+ tsType = "Record<string, any>";
714
+ break;
715
+ }
716
+ const isOptional = modifiers.includes(".optional()") || modifiers.includes(".nullable()");
717
+ const isAutoGenerated = modifiers.includes(".autoGenerated()");
718
+ const isCreatedNow = modifiers.includes(".createdNow()");
719
+ const isUpdateNow = modifiers.includes(".updateNow()");
720
+ const isDefault = modifiers.includes(".default(");
721
+ // Calculate modifier flags
722
+ let modFlags = 0;
723
+ if (isCreatedNow)
724
+ modFlags |= (1 << 0); // CreatedNow
725
+ if (isUpdateNow)
726
+ modFlags |= (1 << 1); // UpdateNow
727
+ if (isAutoGenerated)
728
+ modFlags |= (1 << 2); // AutoGenerated
729
+ if (modifiers.includes(".searchable()"))
730
+ modFlags |= (1 << 3); // Searchable
731
+ if (isDefault)
732
+ modFlags |= (1 << 4); // HasDefault
733
+ if (modifiers.includes(".unique()") || modifiers.includes(".applyUnique()"))
734
+ modFlags |= (1 << 5); // Unique
735
+ if (modifiers.includes(".index()") || modifiers.includes(".applyIndex()"))
736
+ modFlags |= (1 << 6); // Indexed
737
+ if (isOptional)
738
+ modFlags |= (1 << 7); // Nullable
739
+ tables[tableName].fieldMetadata[fieldName] = {
740
+ modifiers: modFlags,
741
+ type: tsType,
742
+ optional: isOptional
743
+ };
744
+ // Add to base DataModel type
745
+ tables[tableName].fields.push(` ${fieldName}${isOptional ? "?" : ""}: ${tsType};`);
746
+ // Add to CreateInput if not auto-generated and not a system field
747
+ if (!isAutoGenerated &&
748
+ fieldName !== "createdAt" &&
749
+ fieldName !== "updatedAt" &&
750
+ fieldName !== "_id" &&
751
+ fieldName !== "_collection") {
752
+ tables[tableName].createFields.push(` ${fieldName}?: ${tsType};`);
753
+ }
754
+ // Add to UpdateInput if not auto-generated and not read-only
755
+ if (!isAutoGenerated &&
756
+ fieldName !== "createdAt" &&
757
+ fieldName !== "_id" &&
758
+ fieldName !== "_collection") {
759
+ tables[tableName].updateFields.push(` ${fieldName}?: ${tsType};`);
760
+ }
761
+ }
762
+ // Parse relationships from options
763
+ if (options) {
764
+ const relBlockMatch = options.match(/relationships:\s*{([\s\S]+?)}/);
765
+ if (relBlockMatch) {
766
+ const relContent = relBlockMatch[1];
767
+ const relMatches = relContent.matchAll(/(\w+):\s*(hasMany|belongsTo|hasOne)\(["'](\w+)["']/g);
768
+ for (const relMatch of relMatches) {
769
+ const relName = relMatch[1];
770
+ const relType = relMatch[2];
771
+ const targetTable = relMatch[3];
772
+ let tsType = "any";
773
+ if (relType === "hasMany") {
774
+ tsType = `DataModel["${targetTable}"][]`;
775
+ }
776
+ else {
777
+ tsType = `DataModel["${targetTable}"] | null`;
778
+ }
779
+ // Relationships are always optional in the DataModel as they need to be loaded
780
+ tables[tableName].fields.push(` ${relName}?: ${tsType};`);
781
+ console.log(` Found relationship: ${relName} -> ${targetTable} (${relType})`);
782
+ }
783
+ }
784
+ }
785
+ if (tables[tableName].fields.length === 0) {
786
+ console.log(` No fields found for table ${tableName}. Raw fields content:\n${fields}`);
787
+ }
788
+ }
789
+ // Generate DataModel type
790
+ const tableTypes = Object.entries(tables)
791
+ .map(([name, data]) => {
792
+ return ` ${name}: {\n${data.fields.join("\n")}\n _id: string;\n _collection: "${name}";\n };`;
793
+ })
794
+ .join("\n");
795
+ const dataModel = `export type DataModel = {\n${tableTypes}\n};`;
796
+ // Generate Input Types
797
+ const inputTypesList = [];
798
+ for (const [name, data] of Object.entries(tables)) {
799
+ const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
800
+ inputTypesList.push(`export type ${pascalName}CreateInput = {\n${data.createFields.length > 0 ? data.createFields.join("\n") : " // All fields are auto-generated"}\n};`);
801
+ inputTypesList.push(`export type ${pascalName}UpdateInput = {\n${data.updateFields.length > 0 ? data.updateFields.join("\n") : " // All fields are read-only"}\n};`);
802
+ }
803
+ const inputTypes = `// ============================================================================
804
+ // PRISMA-STYLE INPUT TYPES
805
+ // ============================================================================
806
+
807
+ ${inputTypesList.join("\n\n")}
808
+
809
+ export type GeneratedInputModels = {
810
+ ${Object.keys(tables).map(name => {
811
+ const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
812
+ return ` ${name}: {
813
+ create: ${pascalName}CreateInput;
814
+ update: ${pascalName}UpdateInput;
815
+ };`;
816
+ }).join("\n")}
817
+ };
818
+ `;
819
+ // Generate Relationship Types
820
+ const relationshipTypesList = [];
821
+ for (const [name, data] of Object.entries(tables)) {
822
+ const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
823
+ // Extract relationship fields from the base type
824
+ const relationshipFields = data.fields.filter(f => f.includes("DataModel["));
825
+ if (relationshipFields.length > 0) {
826
+ relationshipTypesList.push(`export type ${pascalName}WithRelations = DataModel["${name}"] & {\n${relationshipFields.join("\n")}\n};`);
827
+ }
828
+ else {
829
+ relationshipTypesList.push(`export type ${pascalName}WithRelations = DataModel["${name}"];`);
830
+ }
831
+ }
832
+ const relationshipTypes = `// ============================================================================
833
+ // RELATIONSHIP TYPES
834
+ // ============================================================================
835
+
836
+ ${relationshipTypesList.join("\n\n")}
837
+ `;
838
+ return { dataModel, inputTypes, relationshipTypes };
839
+ }
840
+ /**
841
+ * Scan schema folder for all .ts files
842
+ */
843
+ async scanSchemaFolder(folderPath) {
844
+ const files = [];
845
+ // Check if folder exists
846
+ try {
847
+ const entryNames = fs.readdirSync(folderPath);
848
+ for (const name of entryNames) {
849
+ if (!name.endsWith(".ts"))
850
+ continue;
851
+ if (name === "index.ts")
852
+ continue;
853
+ const filePath = join(folderPath, name);
854
+ // Skip directories
855
+ const stat = fs.statSync(filePath);
856
+ if (stat.isDirectory())
857
+ continue;
858
+ const content = fs.readFileSync(filePath, "utf-8");
859
+ files.push({ filePath, content });
860
+ }
861
+ }
862
+ catch {
863
+ // Folder doesn't exist
864
+ }
865
+ return files;
866
+ }
867
+ async generateApiTypes(typeInfos) {
868
+ let output = `// This file is auto-generated by Archlast CLI\n`;
869
+ output += `// Do not edit manually\n\n`;
870
+ output += `import type { FunctionReference } from '@archlast/client';\n\n`;
871
+ // Generate DataModel type from schema
872
+ const { dataModel: dataModelType, inputTypes, relationshipTypes } = await this.generateDataModelFromSchema();
873
+ output += `// Data model types\n${dataModelType}\n\n`;
874
+ // Generate ApiDefinition
875
+ output += `export type ApiDefinition = {\n`;
876
+ for (const info of typeInfos) {
877
+ const fullName = `${info.moduleName}.${info.name}`;
878
+ output += ` "${fullName}": {\n`;
879
+ output += ` type: "${info.type}";\n`;
880
+ output += ` args: ${info.argsType};\n`;
881
+ output += ` return: ${info.returnType};\n`;
882
+ if (info.auth) {
883
+ output += ` auth: "${info.auth}";\n`;
884
+ }
885
+ output += ` };\n`;
886
+ }
887
+ output += `};\n\n`;
888
+ // Generate api object
889
+ output += `export const api = {\n`;
890
+ const groupedByModule = typeInfos.reduce((acc, info) => {
891
+ if (!acc[info.moduleName])
892
+ acc[info.moduleName] = [];
893
+ acc[info.moduleName].push(info);
894
+ return acc;
895
+ }, {});
896
+ for (const [moduleName, funcs] of Object.entries(groupedByModule)) {
897
+ // Quote module name if it contains special characters
898
+ const quotedModuleName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(moduleName)
899
+ ? moduleName
900
+ : `"${moduleName}"`;
901
+ output += ` ${quotedModuleName}: {\n`;
902
+ for (const func of funcs) {
903
+ const fullName = `${moduleName}.${func.name}`;
904
+ const authStr = func.auth ? `, _auth: '${func.auth}'` : "";
905
+ output += ` ${func.name}: { _name: '${fullName}', _type: '${func.type}', _args: undefined as any, _return: undefined as any${authStr} } as FunctionReference<'${func.type}', '${fullName}', ${func.argsType}, ${func.returnType}>,\n`;
906
+ }
907
+ output += ` },\n`;
908
+ }
909
+ output += `} as const;\n\n`;
910
+ output += `export type Api = typeof api;\n`;
911
+ return output;
912
+ }
913
+ async generateRpcTypes(rpcTypeInfos) {
914
+ let output = `// This file is auto-generated by Archlast CLI\n`;
915
+ output += `// Do not edit manually\n\n`;
916
+ output += `// tRPC Procedure Type Definitions\n`;
917
+ output += `// This file defines the input/output types for all RPC procedures\n`;
918
+ output += `// Use with @trpc/client for fully typed API calls\n\n`;
919
+ // Import only DataModel - no tRPC dependencies
920
+ output += `import type { DataModel } from './server';\n\n`;
921
+ // Group procedures by module name
922
+ const groupedByModule = rpcTypeInfos.reduce((acc, info) => {
923
+ if (!acc[info.moduleName])
924
+ acc[info.moduleName] = [];
925
+ acc[info.moduleName].push(info);
926
+ return acc;
927
+ }, {});
928
+ // Generate typescript types for each procedure
929
+ output += `// ============================================\n`;
930
+ output += `// Procedure Input/Output Types\n`;
931
+ output += `// ============================================\n\n`;
932
+ for (const [moduleName, procs] of Object.entries(groupedByModule)) {
933
+ // Convert hyphenated names to camelCase
934
+ const safeModuleName = moduleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
935
+ const pascalModuleName = safeModuleName.charAt(0).toUpperCase() + safeModuleName.slice(1);
936
+ output += `// ${pascalModuleName} Module\n`;
937
+ for (const proc of procs) {
938
+ output += `export type ${proc.name}Input = ${proc.argsType};\n`;
939
+ output += `export type ${proc.name}Output = ${proc.returnType};\n`;
940
+ }
941
+ output += `\n`;
942
+ }
943
+ // Generate a combined namespace for all procedures
944
+ output += `// ============================================\n`;
945
+ output += `// All Procedures Types\n`;
946
+ output += `// ============================================\n\n`;
947
+ output += `export interface RpcProcedures {\n`;
948
+ for (const [moduleName, procs] of Object.entries(groupedByModule)) {
949
+ const safeModuleName = moduleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
950
+ output += ` ${safeModuleName}: {\n`;
951
+ for (const proc of procs) {
952
+ output += ` ${proc.name}: {\n`;
953
+ output += ` input: ${proc.argsType};\n`;
954
+ output += ` output: ${proc.returnType};\n`;
955
+ output += ` };\n`;
956
+ }
957
+ output += ` };\n`;
958
+ }
959
+ output += `}\n\n`;
960
+ // Export list of all procedure names
961
+ output += `// Procedure names for reference\n`;
962
+ output += `export const PROCEDURES = {\n`;
963
+ for (const [moduleName, procs] of Object.entries(groupedByModule)) {
964
+ const safeModuleName = moduleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
965
+ output += ` ${safeModuleName}: [\n`;
966
+ for (const proc of procs) {
967
+ output += ` "${proc.name}",\n`;
968
+ }
969
+ output += ` ],\n`;
970
+ }
971
+ output += `} as const;\n`;
972
+ return output;
973
+ }
974
+ /**
975
+ * Generate runtime tRPC router definition file
976
+ * This creates a real t.router() that imports and wraps the actual RPC procedures
977
+ */
978
+ async generateTrpcRouter(rpcTypeInfos) {
979
+ let output = `// This file is auto-generated by Archlast CLI\n`;
980
+ output += `// Do not edit manually\n\n`;
981
+ output += `// tRPC Router Runtime Definition\n`;
982
+ output += `// This file creates a real tRPC router that wraps the RPC procedures\n`;
983
+ output += `// Client: Import type { AppRouter } for full type safety\n\n`;
984
+ // Import tRPC and actual RpcCtx type from @archlast/server
985
+ output += `import { initTRPC } from "@trpc/server";\n`;
986
+ output += `import type { RpcCtx } from "@archlast/server/functions/types";\n\n`;
987
+ // Group procedures by import path (to avoid duplicate imports)
988
+ const procsByImportPath = rpcTypeInfos.reduce((acc, info) => {
989
+ if (!acc[info.importPath])
990
+ acc[info.importPath] = [];
991
+ acc[info.importPath].push(info);
992
+ return acc;
993
+ }, {});
994
+ // Generate imports for each source file
995
+ output += `// ============================================\n`;
996
+ output += `// Import RPC Procedures\n`;
997
+ output += `// ============================================\n\n`;
998
+ for (const [importPath, procs] of Object.entries(procsByImportPath)) {
999
+ const exportNames = procs.map((p) => p.name).join(", ");
1000
+ output += `import { ${exportNames} } from "${importPath}";\n`;
1001
+ }
1002
+ output += `\n`;
1003
+ // Initialize tRPC with proper context type
1004
+ output += `// ============================================\n`;
1005
+ output += `// Initialize tRPC\n`;
1006
+ output += `// ============================================\n\n`;
1007
+ output += `const t = initTRPC.context<RpcCtx>().create();\n\n`;
1008
+ // Build the router
1009
+ output += `// ============================================\n`;
1010
+ output += `// Build Runtime Router\n`;
1011
+ output += `// ============================================\n\n`;
1012
+ output += `/**\n`;
1013
+ output += ` * tRPC Router\n`;
1014
+ output += ` * Wraps the RPC procedures in tRPC's t.procedure API\n`;
1015
+ output += ` * Client: Use createTRPCClient<typeof appRouter> for type-safe calls\n`;
1016
+ output += ` */\n`;
1017
+ output += `export const appRouter = t.router({\n`;
1018
+ // Group procedures by module name for nested router structure
1019
+ const groupedByModule = rpcTypeInfos.reduce((acc, info) => {
1020
+ if (!acc[info.moduleName])
1021
+ acc[info.moduleName] = [];
1022
+ acc[info.moduleName].push(info);
1023
+ return acc;
1024
+ }, {});
1025
+ for (const [moduleName, procs] of Object.entries(groupedByModule)) {
1026
+ // Convert hyphenated names to camelCase
1027
+ const safeModuleName = moduleName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1028
+ output += ` ${safeModuleName}: t.router({\n`;
1029
+ for (const proc of procs) {
1030
+ // Generate the procedure wrapper
1031
+ // We access the procedure's args and handler from the imported object
1032
+ output += ` ${proc.name}: t.procedure`;
1033
+ // Add input validation if args exist (use non-null assertion since args is optional in RpcDefinition)
1034
+ if (proc.hasArgs) {
1035
+ output += `.input(${proc.name}.args!)`;
1036
+ }
1037
+ // Add query or mutation
1038
+ if (proc.procedureType === "query") {
1039
+ output += `.query(async ({ ctx, input }) => {\n`;
1040
+ }
1041
+ else {
1042
+ output += `.mutation(async ({ ctx, input }) => {\n`;
1043
+ }
1044
+ // Call the original handler
1045
+ output += ` return ${proc.name}.handler(ctx, input);\n`;
1046
+ output += ` }),\n`;
1047
+ }
1048
+ output += ` }),\n`;
1049
+ }
1050
+ output += `});\n\n`;
1051
+ // Export the AppRouter type
1052
+ output += `// ============================================\n`;
1053
+ output += `// Export Router Type\n`;
1054
+ output += `// ============================================\n\n`;
1055
+ output += `export type AppRouter = typeof appRouter;\n\n`;
1056
+ // Export tRPC instance for server use
1057
+ output += `export { t };\n`;
1058
+ return output;
1059
+ }
1060
+ async generateServerTypes() {
1061
+ const { dataModel, inputTypes, relationshipTypes } = await this.generateDataModelFromSchema();
1062
+ return `// This file is auto-generated by Archlast CLI
1063
+ // Do not edit manually
1064
+
1065
+ import "reflect-metadata";
1066
+ import { z } from "zod";
1067
+ import type { FunctionReference } from "@archlast/client";
1068
+ import type { GenericDatabaseReader, GenericDatabaseWriter, RelationsMap, Document } from "@archlast/server/db/interfaces";
1069
+ import type { AuthContext } from "@archlast/server/auth/interfaces";
1070
+ import type { IQueryable, IMutableQueryable, IDbSet, IChangeTracker } from "@archlast/server/repository/interfaces";
1071
+ import type { DbContext, EntityEntry, EntityState, IRepository } from "@archlast/server/repository/factory";
1072
+ import type { SchedulerService } from "@archlast/server/jobs/scheduler";
1073
+ import type { JobQueue } from "@archlast/server/jobs/queue";
1074
+ import type { Storage } from "@archlast/server/storage/types";
1075
+ import type { LogService } from "@archlast/server/logging/logger";
1076
+ import { WebhookGuard } from "@archlast/server/webhook/definition";
1077
+
1078
+
1079
+ // ============================================================================
1080
+ // DATA MODEL
1081
+ // ============================================================================
1082
+
1083
+ ${dataModel}
1084
+
1085
+ // Re-export validator
1086
+ export { v } from "@archlast/server/schema/validators";
1087
+
1088
+ ${inputTypes}
1089
+
1090
+ ${relationshipTypes}
1091
+
1092
+ // ============================================================================
1093
+ // DI DECORATORS (inline implementations)
1094
+ // ============================================================================
1095
+
1096
+ export interface InjectableOptions {
1097
+ provide: string;
1098
+ useFactory?: string;
1099
+ scope?: "singleton" | "transient";
1100
+ }
1101
+
1102
+ export function Injectable(options?: InjectableOptions): ClassDecorator {
1103
+ return function <TFunction extends Function>(target: TFunction): TFunction {
1104
+ // Store metadata on the constructor itself
1105
+ (target as any).__injectable__ = options || { provide: target.name };
1106
+ return target;
1107
+ };
1108
+ }
1109
+
1110
+ export function Factory(token: string): ClassDecorator {
1111
+ return function <TFunction extends Function>(target: TFunction): TFunction {
1112
+ // Store metadata on the constructor itself
1113
+ (target as any).__factory__ = token;
1114
+ return target;
1115
+ };
1116
+ }
1117
+
1118
+ // ============================================================================
1119
+ // RE-EXPORTS (webhook guards from package)
1120
+ // ============================================================================
1121
+
1122
+ // Import and re-export webhook guards from the server package
1123
+ // Note: When deployed, these will be available from @archlast/server/webhook/guard
1124
+ export { createWebhookGuard, createHmacGuard } from "@archlast/server/webhook/guard";
1125
+
1126
+ // ============================================================================
1127
+ // CONTEXT TYPES
1128
+ // ============================================================================
1129
+
1130
+ /**
1131
+ * DI methods available in function contexts
1132
+ */
1133
+ export interface DIContext {
1134
+ /**
1135
+ * Resolve a singleton service by token
1136
+ * @example ctx.service<IUserService>("UserService")
1137
+ */
1138
+ service<T>(token: string): T;
1139
+
1140
+ /**
1141
+ * Resolve a factory implementation by token and implementation key
1142
+ * @example ctx.factory<IEmailProvider>("EmailProvider", "Resend")
1143
+ */
1144
+ factory<T>(token: string, implementation?: string): T;
1145
+ }
1146
+
1147
+ /**
1148
+ * Server context contains all server-wide dependencies
1149
+ */
1150
+ export interface ServerContext extends DIContext {
1151
+ auth: AuthContext;
1152
+ db: GenericDatabaseWriter<DataModel>;
1153
+ /** Uncached database client for system functions */
1154
+ rawDb: GenericDatabaseWriter<DataModel>;
1155
+ /** Repository with EF Core DbContext pattern - supports change tracking and SaveChangesAsync() */
1156
+ repository: IRepository<DataModel, GeneratedInputModels>;
1157
+ storage: Storage;
1158
+ logger: LogService;
1159
+ scheduler: SchedulerService;
1160
+ jobQueue: JobQueue;
1161
+ }
1162
+
1163
+ /**
1164
+ * Query context - read-only database operations
1165
+ */
1166
+ export interface QueryCtx extends DIContext {
1167
+ auth: AuthContext;
1168
+ db: GenericDatabaseReader<DataModel>;
1169
+ /** Uncached database client for system functions */
1170
+ rawDb: GenericDatabaseReader<DataModel>;
1171
+ /** Repository with EF Core DbContext pattern - query-only access but with FindAsync, Find, Entry methods */
1172
+ repository: IRepository<DataModel, GeneratedInputModels>;
1173
+ storage: Storage;
1174
+ logger: LogService;
1175
+ scheduler: SchedulerService;
1176
+ jobQueue: JobQueue;
1177
+ }
1178
+
1179
+ /**
1180
+ * Mutation context - read/write database operations
1181
+ */
1182
+ export interface MutationCtx extends DIContext {
1183
+ auth: AuthContext;
1184
+ db: GenericDatabaseWriter<DataModel>;
1185
+ /** Uncached database client for system functions that need fresh data */
1186
+ rawDb: GenericDatabaseWriter<DataModel>;
1187
+ /** Repository with EF Core DbContext pattern - supports change tracking and SaveChangesAsync() with input types */
1188
+ repository: IRepository<DataModel, GeneratedInputModels>;
1189
+ storage: Storage;
1190
+ logger: LogService;
1191
+ scheduler: SchedulerService;
1192
+ jobQueue: JobQueue;
1193
+ }
1194
+
1195
+ /**
1196
+ * Action context - full read/write database access for side effects and external APIs
1197
+ */
1198
+ export interface ActionCtx extends DIContext {
1199
+ auth: AuthContext;
1200
+ db: GenericDatabaseWriter<DataModel>;
1201
+ /** Uncached database client for system functions that need fresh data */
1202
+ rawDb: GenericDatabaseWriter<DataModel>;
1203
+ /** Repository with EF Core DbContext pattern - supports change tracking and SaveChangesAsync() with input types */
1204
+ repository: IRepository<DataModel, GeneratedInputModels>;
1205
+ storage: Storage;
1206
+ logger: LogService;
1207
+ scheduler: SchedulerService;
1208
+ jobQueue: JobQueue;
1209
+ }
1210
+
1211
+ /**
1212
+ * Service context - request-scoped context for services
1213
+ */
1214
+ export interface ServiceContext extends DIContext {
1215
+ auth: AuthContext;
1216
+ db: GenericDatabaseWriter<DataModel>;
1217
+ /** Uncached database client for system functions that need fresh data */
1218
+ rawDb: GenericDatabaseWriter<DataModel>;
1219
+ /** Repository with EF Core DbContext pattern - supports change tracking and SaveChangesAsync() with input types */
1220
+ repository: IRepository<DataModel, GeneratedInputModels>;
1221
+ storage: Storage;
1222
+ logger: LogService;
1223
+ scheduler: SchedulerService;
1224
+ jobQueue: JobQueue;
1225
+ }
1226
+
1227
+ /**
1228
+ * HTTP request object with path parameters and query params
1229
+ */
1230
+ export interface HttpRequest {
1231
+ raw: Request;
1232
+ method: string;
1233
+ path: string;
1234
+ headers: Headers;
1235
+ params: Record<string, string>;
1236
+ query: Record<string, string>;
1237
+ json<T = unknown>(): Promise<T>;
1238
+ text(): Promise<string>;
1239
+ formData(): Promise<FormData>;
1240
+ blob(): Promise<Blob>;
1241
+ arrayBuffer(): Promise<ArrayBuffer>;
1242
+ }
1243
+
1244
+ /**
1245
+ * HTTP context - includes server context and request
1246
+ */
1247
+ export interface HttpCtx extends DIContext {
1248
+ auth: AuthContext;
1249
+ db: GenericDatabaseWriter<DataModel>;
1250
+ repository: { [K in keyof DataModel]: IMutableQueryable<DataModel[K]> };
1251
+ storage: Storage;
1252
+ logger: LogService;
1253
+ scheduler: SchedulerService;
1254
+ jobQueue: JobQueue;
1255
+ server: ServerContext;
1256
+ req: HttpRequest;
1257
+ }
1258
+
1259
+ /**
1260
+ * Webhook context - similar to MutationCtx
1261
+ */
1262
+ export interface WebhookCtx extends DIContext {
1263
+ auth: AuthContext;
1264
+ db: GenericDatabaseWriter<DataModel>;
1265
+ repository: { [K in keyof DataModel]: IMutableQueryable<DataModel[K]> };
1266
+ storage: Storage;
1267
+ logger: LogService;
1268
+ scheduler: SchedulerService;
1269
+ jobQueue: JobQueue;
1270
+ server: ServerContext;
1271
+ req: Request;
1272
+ }
1273
+
1274
+ /**
1275
+ * RPC (tRPC-style) context - For type-safe public API procedures
1276
+ * Similar to QueryCtx/MutationCtx but designed for external RPC calls
1277
+ */
1278
+ export interface RpcCtx<
1279
+ DataModel extends Record<string, Document> = any,
1280
+ Relations extends RelationsMap<DataModel> = RelationsMap<DataModel>,
1281
+ > extends DIContext {
1282
+ auth: AuthContext;
1283
+ db: GenericDatabaseWriter<DataModel, Relations>;
1284
+ repository: { [K in keyof DataModel]: IMutableQueryable<DataModel[K]> };
1285
+ storage: Storage;
1286
+ logger: LogService;
1287
+ scheduler: SchedulerService;
1288
+ jobQueue: JobQueue;
1289
+ server: ServerContext;
1290
+ }
1291
+
1292
+ export type QueryHandler<Args, Return> = (ctx: QueryCtx, args: Args) => Promise<Return>;
1293
+ export type MutationHandler<Args, Return> = (ctx: MutationCtx, args: Args) => Promise<Return>;
1294
+ export type ActionHandler<Args, Return> = (ctx: ActionCtx, args: Args) => Promise<Return>;
1295
+ export type HttpHandler<Args, Return> = (ctx: HttpCtx, args: Args) => Promise<Return> | Return;
1296
+ export type WebhookHandler<Args, Return> = (ctx: WebhookCtx, args: Args) => Promise<Return> | Return;
1297
+
1298
+ export type FunctionType = "query" | "mutation" | "action" | "http" | "webhook";
1299
+ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
1300
+
1301
+ /**
1302
+ * Auth mode determines authentication requirements for a function
1303
+ * - "required" (default): Authentication is required (session or API key)
1304
+ * - "optional": Authentication is optional (ctx.auth may be unauthenticated)
1305
+ * - "public": No authentication required (function is publicly accessible)
1306
+ */
1307
+ export type AuthMode = "required" | "optional" | "public";
1308
+
1309
+ export interface FunctionDefinition {
1310
+ type: FunctionType;
1311
+ handler: Function;
1312
+ args?: z.ZodType<any>;
1313
+ name?: string; // Assigned at registration
1314
+
1315
+ /**
1316
+ * Auth mode - defaults to "required"
1317
+ */
1318
+ auth?: AuthMode;
1319
+
1320
+ /**
1321
+ * Required permissions - checked before handler execution
1322
+ * Example: ["tasks.read", "tasks.write"]
1323
+ */
1324
+ permissions?: string[];
1325
+
1326
+ /**
1327
+ * Allowed scopes for API key auth
1328
+ * If not specified, function is accessible to all auth types
1329
+ */
1330
+ scopes?: ("query" | "mutation" | "action")[];
1331
+ }
1332
+
1333
+ /**
1334
+ * HTTP-specific function definition
1335
+ */
1336
+ export interface HttpDefinition<Args = unknown, Return = unknown> {
1337
+ type: "http";
1338
+ method: HttpMethod;
1339
+ path: string;
1340
+ handler: HttpHandler<Args, Return>;
1341
+ auth?: AuthMode;
1342
+ permissions?: string[];
1343
+ middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
1344
+ }
1345
+
1346
+ /**
1347
+ * Webhook-specific function definition
1348
+ */
1349
+ export interface WebhookDefinition<Event = unknown, Return = unknown> {
1350
+ type: "webhook";
1351
+ method: HttpMethod;
1352
+ path: string;
1353
+ handler: WebhookHandler<Event, Return>;
1354
+ eventSchema?: z.ZodType<Event>;
1355
+ guards?: WebhookGuard[];
1356
+ auth?: AuthMode;
1357
+ }
1358
+
1359
+ // Overload for config object with args (explicit z.ZodType)
1360
+ export function query<Args extends z.ZodType<any, any, any>, Return>(config: {
1361
+ args: Args;
1362
+ handler: QueryHandler<z.infer<Args>, Return>;
1363
+ auth?: AuthMode;
1364
+ permissions?: string[];
1365
+ scopes?: ("query" | "mutation" | "action")[];
1366
+ }): FunctionDefinition;
1367
+ // Overload for config object with args (z.ZodRawShape)
1368
+ export function query<Args extends z.ZodRawShape, Return>(config: {
1369
+ args: Args;
1370
+ handler: QueryHandler<z.infer<z.ZodObject<Args>>, Return>;
1371
+ auth?: AuthMode;
1372
+ permissions?: string[];
1373
+ scopes?: ("query" | "mutation" | "action")[];
1374
+ }): FunctionDefinition;
1375
+ // Overload for config object without args
1376
+ export function query<Return>(config: {
1377
+ handler: QueryHandler<Record<string, never>, Return>;
1378
+ auth?: AuthMode;
1379
+ permissions?: string[];
1380
+ scopes?: ("query" | "mutation" | "action")[];
1381
+ }): FunctionDefinition;
1382
+ // Overload for simple handler (no args)
1383
+ export function query<Return>(
1384
+ handler: QueryHandler<Record<string, never>, Return>
1385
+ ): FunctionDefinition;
1386
+ export function query(configOrHandler: any): FunctionDefinition {
1387
+ if (typeof configOrHandler === "function") {
1388
+ return {
1389
+ type: "query",
1390
+ handler: configOrHandler,
1391
+ auth: "required", // default
1392
+ };
1393
+ }
1394
+ // If args is already a ZodType (z.object, etc.), use it directly; otherwise wrap with z.object()
1395
+ const args = configOrHandler.args && typeof configOrHandler.args === "object" && "parse" in configOrHandler.args
1396
+ ? configOrHandler.args
1397
+ : configOrHandler.args
1398
+ ? z.object(configOrHandler.args)
1399
+ : undefined;
1400
+
1401
+ return {
1402
+ type: "query",
1403
+ handler: configOrHandler.handler,
1404
+ args,
1405
+ auth: configOrHandler.auth || "required",
1406
+ permissions: configOrHandler.permissions,
1407
+ scopes: configOrHandler.scopes,
1408
+ };
1409
+ }
1410
+
1411
+ // Overload for config object with args (explicit z.ZodType)
1412
+ export function mutation<Args extends z.ZodType<any, any, any>, Return>(config: {
1413
+ args: Args;
1414
+ handler: MutationHandler<z.infer<Args>, Return>;
1415
+ auth?: AuthMode;
1416
+ permissions?: string[];
1417
+ scopes?: ("query" | "mutation" | "action")[];
1418
+ }): FunctionDefinition;
1419
+ // Overload for config object with args (z.ZodRawShape)
1420
+ export function mutation<Args extends z.ZodRawShape, Return>(config: {
1421
+ args: Args;
1422
+ handler: MutationHandler<z.infer<z.ZodObject<Args>>, Return>;
1423
+ auth?: AuthMode;
1424
+ permissions?: string[];
1425
+ scopes?: ("query" | "mutation" | "action")[];
1426
+ }): FunctionDefinition;
1427
+ // Overload for config object without args
1428
+ export function mutation<Return>(config: {
1429
+ handler: MutationHandler<Record<string, never>, Return>;
1430
+ auth?: AuthMode;
1431
+ permissions?: string[];
1432
+ scopes?: ("query" | "mutation" | "action")[];
1433
+ }): FunctionDefinition;
1434
+ // Overload for simple handler (no args)
1435
+ export function mutation<Return>(
1436
+ handler: MutationHandler<Record<string, never>, Return>
1437
+ ): FunctionDefinition;
1438
+ export function mutation(configOrHandler: any): FunctionDefinition {
1439
+ if (typeof configOrHandler === "function") {
1440
+ return {
1441
+ type: "mutation",
1442
+ handler: configOrHandler,
1443
+ auth: "required", // default
1444
+ };
1445
+ }
1446
+ // If args is already a ZodType (z.object, etc.), use it directly; otherwise wrap with z.object()
1447
+ const args = configOrHandler.args && typeof configOrHandler.args === "object" && "parse" in configOrHandler.args
1448
+ ? configOrHandler.args
1449
+ : configOrHandler.args
1450
+ ? z.object(configOrHandler.args)
1451
+ : undefined;
1452
+
1453
+ return {
1454
+ type: "mutation",
1455
+ handler: configOrHandler.handler,
1456
+ args,
1457
+ auth: configOrHandler.auth || "required",
1458
+ permissions: configOrHandler.permissions,
1459
+ scopes: configOrHandler.scopes,
1460
+ };
1461
+ }
1462
+
1463
+ export function action<Args = unknown, Return = unknown>(
1464
+ handlerOrConfig:
1465
+ | ActionHandler<Args, Return>
1466
+ | {
1467
+ handler: ActionHandler<Args, Return>;
1468
+ auth?: AuthMode;
1469
+ permissions?: string[];
1470
+ scopes?: ("query" | "mutation" | "action")[];
1471
+ }
1472
+ ): FunctionDefinition {
1473
+ if (typeof handlerOrConfig === "function") {
1474
+ return {
1475
+ type: "action",
1476
+ handler: handlerOrConfig,
1477
+ auth: "required", // default
1478
+ };
1479
+ }
1480
+ return {
1481
+ type: "action",
1482
+ handler: handlerOrConfig.handler,
1483
+ auth: handlerOrConfig.auth || "required",
1484
+ permissions: handlerOrConfig.permissions,
1485
+ scopes: handlerOrConfig.scopes,
1486
+ };
1487
+ }
1488
+
1489
+ /**
1490
+ * HTTP endpoint builders
1491
+ */
1492
+ export const http = {
1493
+ get: <Args = unknown, Return = Response>(config: {
1494
+ path: string;
1495
+ handler: HttpHandler<Args, Return>;
1496
+ auth?: AuthMode;
1497
+ permissions?: string[];
1498
+ middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
1499
+ }): HttpDefinition<Args, Return> => ({
1500
+ type: "http",
1501
+ method: "GET",
1502
+ ...config,
1503
+ }),
1504
+
1505
+ post: <Args = unknown, Return = Response>(config: {
1506
+ path: string;
1507
+ handler: HttpHandler<Args, Return>;
1508
+ auth?: AuthMode;
1509
+ permissions?: string[];
1510
+ middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
1511
+ }): HttpDefinition<Args, Return> => ({
1512
+ type: "http",
1513
+ method: "POST",
1514
+ ...config,
1515
+ }),
1516
+
1517
+ put: <Args = unknown, Return = Response>(config: {
1518
+ path: string;
1519
+ handler: HttpHandler<Args, Return>;
1520
+ auth?: AuthMode;
1521
+ permissions?: string[];
1522
+ middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
1523
+ }): HttpDefinition<Args, Return> => ({
1524
+ type: "http",
1525
+ method: "PUT",
1526
+ ...config,
1527
+ }),
1528
+
1529
+ delete: <Args = unknown, Return = Response>(config: {
1530
+ path: string;
1531
+ handler: HttpHandler<Args, Return>;
1532
+ auth?: AuthMode;
1533
+ permissions?: string[];
1534
+ middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
1535
+ }): HttpDefinition<Args, Return> => ({
1536
+ type: "http",
1537
+ method: "DELETE",
1538
+ ...config,
1539
+ }),
1540
+
1541
+ patch: <Args = unknown, Return = Response>(config: {
1542
+ path: string;
1543
+ handler: HttpHandler<Args, Return>;
1544
+ auth?: AuthMode;
1545
+ permissions?: string[];
1546
+ middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
1547
+ }): HttpDefinition<Args, Return> => ({
1548
+ type: "http",
1549
+ method: "PATCH",
1550
+ ...config,
1551
+ }),
1552
+
1553
+ head: <Args = unknown, Return = Response>(config: {
1554
+ path: string;
1555
+ handler: HttpHandler<Args, Return>;
1556
+ auth?: AuthMode;
1557
+ permissions?: string[];
1558
+ middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
1559
+ }): HttpDefinition<Args, Return> => ({
1560
+ type: "http",
1561
+ method: "HEAD",
1562
+ ...config,
1563
+ }),
1564
+
1565
+ options: <Args = unknown, Return = Response>(config: {
1566
+ path: string;
1567
+ handler: HttpHandler<Args, Return>;
1568
+ auth?: AuthMode;
1569
+ permissions?: string[];
1570
+ middleware?: Array<(ctx: HttpCtx, next: () => Promise<Response>) => Promise<Response>>;
1571
+ }): HttpDefinition<Args, Return> => ({
1572
+ type: "http",
1573
+ method: "OPTIONS",
1574
+ ...config,
1575
+ }),
1576
+ };
1577
+
1578
+ /**
1579
+ * Webhook builder
1580
+ */
1581
+ export function webhook<Event = unknown, Return = unknown>(config: {
1582
+ path: string;
1583
+ handler: WebhookHandler<Event, Return>;
1584
+ eventSchema?: z.ZodType<Event>;
1585
+ guards?: WebhookGuard[];
1586
+ auth?: AuthMode;
1587
+ method?: HttpMethod;
1588
+ }): WebhookDefinition<Event, Return> {
1589
+ return {
1590
+ type: "webhook",
1591
+ method: config.method ?? "POST",
1592
+ path: config.path,
1593
+ handler: config.handler,
1594
+ eventSchema: config.eventSchema,
1595
+ guards: config.guards ?? [],
1596
+ auth: config.auth ?? "public", // Webhooks are typically public with guard verification
1597
+ };
1598
+ }
1599
+
1600
+ /**
1601
+ * Type guards
1602
+ */
1603
+ export function isHttpDefinition(value: any): value is HttpDefinition {
1604
+ return value && typeof value === "object" && value.type === "http";
1605
+ }
1606
+
1607
+ export function isWebhookDefinition(value: any): value is WebhookDefinition {
1608
+ return value && typeof value === "object" && value.type === "webhook";
1609
+ }
1610
+ `;
1611
+ }
1612
+ generateIndexExport() {
1613
+ return `// This file is auto-generated by Archlast CLI
1614
+ // Do not edit manually
1615
+
1616
+ export { api } from './api';
1617
+ export type { Api } from './api';
1618
+ `;
1619
+ }
1620
+ /**
1621
+ * Generate CRUD handlers for all collections in the schema
1622
+ * Creates _generated/crud/ directory with individual collection files
1623
+ */
1624
+ async generateCrudHandlers(generatedDir) {
1625
+ const crudDir = path.join(generatedDir, "crud");
1626
+ // Ensure crud directory exists
1627
+ if (!fs.existsSync(crudDir)) {
1628
+ fs.mkdirSync(crudDir, { recursive: true });
1629
+ }
1630
+ // Get schema tables
1631
+ const { dataModel, inputTypes } = await this.generateDataModelFromSchema();
1632
+ // Extract table names from the DataModel type
1633
+ const tables = this.extractTableNames(dataModel);
1634
+ if (tables.length === 0) {
1635
+ console.log(" No tables found for CRUD generation");
1636
+ return;
1637
+ }
1638
+ console.log(` Generating CRUD handlers for ${tables.length} collections`);
1639
+ // Generate individual collection files
1640
+ for (const tableName of tables) {
1641
+ const handlerCode = this.generateCrudHandlerForTable(tableName);
1642
+ const filePath = path.join(crudDir, `${tableName}.ts`);
1643
+ fs.writeFileSync(filePath, handlerCode, "utf-8");
1644
+ }
1645
+ // Generate index.ts that re-exports all handlers
1646
+ const indexCode = this.generateCrudIndex(tables);
1647
+ const indexPath = path.join(crudDir, "index.ts");
1648
+ fs.writeFileSync(indexPath, indexCode, "utf-8");
1649
+ console.log(` CRUD handlers generated in _generated/crud/`);
1650
+ }
1651
+ /**
1652
+ * Extract table names from the DataModel type string
1653
+ */
1654
+ extractTableNames(dataModel) {
1655
+ // Match table names from DataModel type definition
1656
+ // Pattern: ` tableName: {` or ` "tableName": {`
1657
+ const matches = dataModel.matchAll(/(?:\s*)(?:["']?)(\w+)(?:["']?)\s*:\s*{/g);
1658
+ const tables = [];
1659
+ for (const match of matches) {
1660
+ const tableName = match[1];
1661
+ // Skip system fields and type definitions
1662
+ if (tableName !== "_id" &&
1663
+ tableName !== "_collection" &&
1664
+ tableName !== "DataModel" &&
1665
+ tableName !== "export" &&
1666
+ tableName !== "type") {
1667
+ tables.push(tableName);
1668
+ }
1669
+ }
1670
+ return tables;
1671
+ }
1672
+ /**
1673
+ * Generate CRUD handler code for a single table (public for CLI command)
1674
+ */
1675
+ generateCrudHandlerForTable(tableName) {
1676
+ const pascalName = tableName.charAt(0).toUpperCase() + tableName.slice(1);
1677
+ const camelName = tableName;
1678
+ return `// Auto-generated CRUD handlers for "${tableName}"
1679
+ // This file is auto-generated by Archlast CLI - do not edit manually
1680
+
1681
+ import { http } from "../server";
1682
+ import type { DataModel } from "../server";
1683
+
1684
+ const collection = "${tableName}" as const;
1685
+
1686
+ // Type for the collection document
1687
+ type ${pascalName}Doc = DataModel["${tableName}"];
1688
+ // Type for creating a document (excludes auto-generated fields)
1689
+ type ${pascalName}Create = Omit<${pascalName}Doc, "_id" | "_collection">;
1690
+ // Type for updating a document (all fields optional)
1691
+ type ${pascalName}Update = Partial<${pascalName}Create>;
1692
+
1693
+ /**
1694
+ * GET /${tableName}
1695
+ * List all ${tableName} records with optional pagination
1696
+ */
1697
+ export const list${pascalName} = http.get({
1698
+ path: "/${tableName}",
1699
+ auth: "public",
1700
+ handler: async (ctx) => {
1701
+ // Query params are already parsed into ctx.req.query
1702
+ const limitStr = ctx.req.query.limit;
1703
+ const offsetStr = ctx.req.query.offset;
1704
+ const limit = limitStr ? Number(limitStr) : 50;
1705
+ const offset = offsetStr ? Number(offsetStr) : 0;
1706
+
1707
+ // Use .take() and .skip() for pagination
1708
+ const result = await ctx.db.query(collection)
1709
+ .take(limit)
1710
+ .skip(offset)
1711
+ .findMany();
1712
+
1713
+ return result;
1714
+ },
1715
+ });
1716
+
1717
+ /**
1718
+ * GET /${tableName}/:id
1719
+ * Get a single ${tableName} record by ID
1720
+ */
1721
+ export const get${pascalName} = http.get({
1722
+ path: "/${tableName}/:id",
1723
+ auth: "public",
1724
+ handler: async (ctx) => {
1725
+ // Path params are available in ctx.req.params
1726
+ const id = ctx.req.params.id;
1727
+ const result = await ctx.db.get(collection, id);
1728
+
1729
+ if (!result) {
1730
+ return { error: "${tableName} not found" };
1731
+ }
1732
+
1733
+ return result;
1734
+ },
1735
+ });
1736
+
1737
+ /**
1738
+ * POST /${tableName}
1739
+ * Create a new ${tableName} record
1740
+ */
1741
+ export const create${pascalName} = http.post({
1742
+ path: "/${tableName}",
1743
+ auth: "public",
1744
+ handler: async (ctx) => {
1745
+ // Parse JSON body from the request with proper type
1746
+ const data = await ctx.req.json<${pascalName}Create>();
1747
+
1748
+ const result = await ctx.db.insert(collection, data);
1749
+
1750
+ return result;
1751
+ },
1752
+ });
1753
+
1754
+ /**
1755
+ * PATCH /${tableName}/:id
1756
+ * Update a ${tableName} record
1757
+ */
1758
+ export const update${pascalName} = http.patch({
1759
+ path: "/${tableName}/:id",
1760
+ auth: "public",
1761
+ handler: async (ctx) => {
1762
+ const id = ctx.req.params.id;
1763
+ const data = await ctx.req.json<${pascalName}Update>();
1764
+
1765
+ const existing = await ctx.db.get(collection, id);
1766
+ if (!existing) {
1767
+ return { error: "${tableName} not found" };
1768
+ }
1769
+
1770
+ const result = await ctx.db.update(collection, id, data);
1771
+ return result;
1772
+ },
1773
+ });
1774
+
1775
+ /**
1776
+ * DELETE /${tableName}/:id
1777
+ * Delete a ${tableName} record
1778
+ */
1779
+ export const delete${pascalName} = http.delete({
1780
+ path: "/${tableName}/:id",
1781
+ auth: "public",
1782
+ handler: async (ctx) => {
1783
+ const id = ctx.req.params.id;
1784
+
1785
+ const existing = await ctx.db.get(collection, id);
1786
+ if (!existing) {
1787
+ return { error: "${tableName} not found" };
1788
+ }
1789
+
1790
+ await ctx.db.delete(collection, id);
1791
+ return { success: true };
1792
+ },
1793
+ });
1794
+ `;
1795
+ }
1796
+ /**
1797
+ * Generate the index.ts file for CRUD handlers
1798
+ */
1799
+ generateCrudIndex(tables) {
1800
+ const exports = tables.map(tableName => {
1801
+ const pascalName = tableName.charAt(0).toUpperCase() + tableName.slice(1);
1802
+ return `export * from "./${tableName}";`;
1803
+ }).join("\n");
1804
+ return `// Auto-generated CRUD exports
1805
+ // This file is auto-generated by Archlast CLI - do not edit manually
1806
+
1807
+ ${exports}
1808
+
1809
+ // Re-export all handlers as a grouped object
1810
+ import * as tasksHandlers from "./tasks";
1811
+ import * as usersHandlers from "./users";
1812
+
1813
+ // Add more collections as needed...
1814
+ `;
1815
+ }
1816
+ }