@apso/cli 0.8.6 → 0.10.2

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 (91) hide show
  1. package/README.md +221 -1653
  2. package/dist/commands/config.d.ts +13 -0
  3. package/dist/commands/config.js +114 -0
  4. package/dist/commands/deploy.d.ts +11 -0
  5. package/dist/commands/deploy.js +141 -0
  6. package/dist/commands/dev.d.ts +15 -0
  7. package/dist/commands/dev.js +95 -0
  8. package/dist/commands/generate.d.ts +12 -0
  9. package/dist/commands/generate.js +179 -0
  10. package/dist/commands/init.d.ts +16 -0
  11. package/dist/commands/init.js +313 -0
  12. package/dist/commands/link.d.ts +11 -0
  13. package/dist/commands/link.js +139 -0
  14. package/dist/commands/login.d.ts +22 -0
  15. package/dist/commands/login.js +345 -0
  16. package/dist/commands/logout.d.ts +9 -0
  17. package/dist/commands/logout.js +38 -0
  18. package/dist/commands/logs.d.ts +10 -0
  19. package/dist/commands/logs.js +64 -0
  20. package/dist/commands/mcp/serve.d.ts +11 -0
  21. package/dist/commands/mcp/serve.js +818 -0
  22. package/dist/commands/migrate.d.ts +11 -0
  23. package/dist/commands/migrate.js +137 -0
  24. package/dist/commands/open.d.ts +10 -0
  25. package/dist/commands/open.js +79 -0
  26. package/dist/commands/projects.d.ts +10 -0
  27. package/dist/commands/projects.js +106 -0
  28. package/dist/commands/schema/diff.d.ts +7 -0
  29. package/dist/commands/schema/diff.js +63 -0
  30. package/dist/commands/schema/pull.d.ts +9 -0
  31. package/dist/commands/schema/pull.js +81 -0
  32. package/dist/commands/schema/push.d.ts +9 -0
  33. package/dist/commands/schema/push.js +97 -0
  34. package/dist/commands/schema/validate.d.ts +9 -0
  35. package/dist/commands/schema/validate.js +90 -0
  36. package/dist/commands/server/new.d.ts +2 -19
  37. package/dist/commands/server/new.js +6 -192
  38. package/dist/commands/server/scaffold.d.ts +2 -25
  39. package/dist/commands/server/scaffold.js +6 -312
  40. package/dist/commands/status.d.ts +7 -0
  41. package/dist/commands/status.js +53 -0
  42. package/dist/commands/unlink.d.ts +9 -0
  43. package/dist/commands/unlink.js +47 -0
  44. package/dist/commands/whoami.d.ts +10 -0
  45. package/dist/commands/whoami.js +97 -0
  46. package/dist/lib/api/client.js +1 -1
  47. package/dist/lib/api/services.js +3 -3
  48. package/dist/lib/api/types.d.ts +9 -2
  49. package/dist/lib/apsorc-parser.d.ts +0 -19
  50. package/dist/lib/apsorc-parser.js +2 -73
  51. package/dist/lib/guards.d.ts +3 -20
  52. package/dist/lib/guards.js +1 -113
  53. package/dist/lib/index.d.ts +1 -9
  54. package/dist/lib/index.js +1 -18
  55. package/dist/lib/migrate/entity-generator.d.ts +14 -0
  56. package/dist/lib/migrate/entity-generator.js +7 -0
  57. package/dist/lib/migrate/sandbox.d.ts +42 -0
  58. package/dist/lib/migrate/sandbox.js +337 -0
  59. package/dist/lib/migrate/snapshot.d.ts +80 -0
  60. package/dist/lib/migrate/snapshot.js +100 -0
  61. package/dist/lib/templates/python/models/model-col-datetime.eta +1 -1
  62. package/dist/lib/templates/python/models/model-col-string.eta +1 -1
  63. package/dist/lib/templates/python/models/model-col-text.eta +1 -1
  64. package/dist/lib/templates/python/models/model-col-varchar.eta +1 -1
  65. package/dist/lib/utils/field.d.ts +12 -0
  66. package/dist/lib/utils/field.js +80 -1
  67. package/dist/lib/utils/schema-convert.d.ts +71 -0
  68. package/dist/lib/utils/schema-convert.js +169 -0
  69. package/dist/lib/utils/spinner.d.ts +19 -0
  70. package/dist/lib/utils/spinner.js +87 -0
  71. package/dist/lib/utils/template.d.ts +12 -0
  72. package/dist/lib/utils/template.js +49 -0
  73. package/npm-shrinkwrap.json +18118 -0
  74. package/oclif.manifest.json +547 -41
  75. package/package.json +26 -5
  76. package/dist/lib/controller.d.ts +0 -10
  77. package/dist/lib/controller.js +0 -99
  78. package/dist/lib/dto.d.ts +0 -16
  79. package/dist/lib/dto.js +0 -92
  80. package/dist/lib/entity.d.ts +0 -19
  81. package/dist/lib/entity.js +0 -56
  82. package/dist/lib/enums.d.ts +0 -6
  83. package/dist/lib/enums.js +0 -28
  84. package/dist/lib/gql-dto.d.ts +0 -3
  85. package/dist/lib/gql-dto.js +0 -39
  86. package/dist/lib/index-module.d.ts +0 -13
  87. package/dist/lib/index-module.js +0 -26
  88. package/dist/lib/module.d.ts +0 -13
  89. package/dist/lib/module.js +0 -42
  90. package/dist/lib/service.d.ts +0 -2
  91. package/dist/lib/service.js +0 -61
@@ -0,0 +1,818 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const core_1 = require("@oclif/core");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
+ const zod_1 = require("zod");
10
+ const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
11
+ const apsorc_parser_1 = require("../../lib/apsorc-parser");
12
+ const lib_1 = require("../../lib");
13
+ const file_system_1 = require("../../lib/utils/file-system");
14
+ const schema_convert_1 = require("../../lib/utils/schema-convert");
15
+ const perf_hooks_1 = require("perf_hooks");
16
+ class McpServe extends base_command_1.default {
17
+ async run() {
18
+ await this.parse(McpServe);
19
+ const server = new mcp_js_1.McpServer({
20
+ name: "api-tools",
21
+ version: "0.1.0",
22
+ });
23
+ this.registerTools(server);
24
+ this.registerResources(server);
25
+ const transport = new stdio_js_1.StdioServerTransport();
26
+ await server.connect(transport);
27
+ }
28
+ registerTools(server) {
29
+ // ── design_schema ──────────────────────────────────────────────
30
+ server.tool("design_schema", "Design a database schema from application requirements. Takes a description of entities, relationships, and business rules. Returns a validated .apsorc schema definition ready for code generation.", {
31
+ requirements: zod_1.z
32
+ .string()
33
+ .describe("Description of the application's data model: entities, fields, relationships, and business rules"),
34
+ language: zod_1.z
35
+ .enum(["typescript", "python", "go"])
36
+ .optional()
37
+ .describe("Target language for code generation (default: typescript)"),
38
+ multi_tenant: zod_1.z
39
+ .boolean()
40
+ .optional()
41
+ .describe("Whether to add organization-scoped multi-tenancy (default: true)"),
42
+ auth_provider: zod_1.z
43
+ .enum([
44
+ "better-auth",
45
+ "auth0",
46
+ "clerk",
47
+ "cognito",
48
+ "api-key",
49
+ "custom-db-session",
50
+ "none",
51
+ ])
52
+ .optional()
53
+ .describe("Authentication provider (default: none)"),
54
+ }, async ({ requirements, language, multi_tenant, auth_provider }) => {
55
+ const lang = language || "typescript";
56
+ const tenant = multi_tenant !== false;
57
+ const auth = auth_provider && auth_provider !== "none" ? auth_provider : undefined;
58
+ // Load the schema guide for the agent to reference
59
+ const guideLocations = [
60
+ path.join(__dirname, "../../references/apso-schema-guide.md"),
61
+ path.join(process.cwd(), ".claude/skills/schema-architect/references/apso-schema-guide.md"),
62
+ ];
63
+ let guideContent = "";
64
+ for (const loc of guideLocations) {
65
+ if (fs.existsSync(loc)) {
66
+ guideContent = fs.readFileSync(loc, "utf-8");
67
+ break;
68
+ }
69
+ }
70
+ const schemaTemplate = {
71
+ version: 2,
72
+ rootFolder: "src",
73
+ language: lang,
74
+ entities: [],
75
+ relationships: [],
76
+ };
77
+ if (auth) {
78
+ schemaTemplate.auth = { provider: auth };
79
+ }
80
+ const instructions = [
81
+ "# Schema Design Request",
82
+ "",
83
+ "## Requirements",
84
+ requirements,
85
+ "",
86
+ "## Configuration",
87
+ `- Language: ${lang}`,
88
+ `- Multi-tenancy: ${tenant ? "yes (use scopeBy: \"organizationId\" on business entities)" : "no"}`,
89
+ `- Auth provider: ${auth || "none"}`,
90
+ "",
91
+ "## Schema Template",
92
+ "Start from this template and populate entities, fields, and relationships:",
93
+ "```json",
94
+ JSON.stringify(schemaTemplate, null, 2),
95
+ "```",
96
+ "",
97
+ "## Field Type Reference",
98
+ "Valid types: text, integer, float, decimal, numeric, boolean, date, enum, json, json-plain, array",
99
+ "",
100
+ "## Rules",
101
+ '- Entity names in PascalCase (e.g., "Project", "TaskComment")',
102
+ '- Use "text" not "string". Use "date" not "timestamp".',
103
+ "- Fields are required by default. Use `nullable: true` for optional fields.",
104
+ "- Relationships go in the top-level `relationships` array, not inline on entities.",
105
+ '- Relationship types: OneToMany, ManyToOne, ManyToMany, OneToOne.',
106
+ '- Use `to_name` when an entity has multiple relationships to the same target.',
107
+ "- Add composite indexes for common query patterns: `{ fields: [\"orgId\", \"status\"] }`.",
108
+ "",
109
+ ];
110
+ if (guideContent) {
111
+ instructions.push("## Full Schema Reference", guideContent);
112
+ }
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: instructions.join("\n"),
118
+ },
119
+ ],
120
+ };
121
+ });
122
+ // ── validate_schema ────────────────────────────────────────────
123
+ server.tool("validate_schema", "Check a schema definition for errors. Validates field types, relationships, indexes, and constraints. Operates on the .apsorc file in the current project directory.", {
124
+ schema_json: zod_1.z
125
+ .string()
126
+ .optional()
127
+ .describe("Optional: JSON string of a schema to validate. If not provided, reads .apsorc from the current directory."),
128
+ }, async ({ schema_json }) => {
129
+ var _a, _b, _c;
130
+ try {
131
+ if (schema_json) {
132
+ // Validate the provided JSON
133
+ const schema = JSON.parse(schema_json);
134
+ const errors = validateSchemaObject(schema);
135
+ if (errors.length > 0) {
136
+ return {
137
+ content: [
138
+ {
139
+ type: "text",
140
+ text: `Validation failed with ${errors.length} error(s):\n${errors.map((e) => ` - ${e}`).join("\n")}`,
141
+ },
142
+ ],
143
+ isError: true,
144
+ };
145
+ }
146
+ const entityCount = ((_a = schema.entities) === null || _a === void 0 ? void 0 : _a.length) || 0;
147
+ const fieldCount = (schema.entities || []).reduce((sum, e) => { var _a; return sum + (((_a = e.fields) === null || _a === void 0 ? void 0 : _a.length) || 0); }, 0);
148
+ const relCount = ((_b = schema.relationships) === null || _b === void 0 ? void 0 : _b.length) || 0;
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: `Validation passed. ${entityCount} entities, ${fieldCount} fields, ${relCount} relationships.`,
154
+ },
155
+ ],
156
+ };
157
+ }
158
+ // Validate from .apsorc file
159
+ const configPath = (0, apsorc_parser_1.findConfigPath)();
160
+ if (!configPath) {
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: "No .apsorc file found in the current directory or parent directories.",
166
+ },
167
+ ],
168
+ isError: true,
169
+ };
170
+ }
171
+ const parsed = (0, apsorc_parser_1.parseApsorc)();
172
+ const schema = (0, schema_convert_1.apsorcToServiceSchema)(parsed);
173
+ const entityCount = schema.entities.length;
174
+ let fieldCount = 0;
175
+ let relationshipCount = 0;
176
+ const errors = [];
177
+ for (const entity of schema.entities) {
178
+ fieldCount += entity.fields.length;
179
+ relationshipCount += ((_c = entity.relationships) === null || _c === void 0 ? void 0 : _c.length) || 0;
180
+ if (!entity.name || entity.name.trim() === "") {
181
+ errors.push("Entity found with empty name");
182
+ }
183
+ if (entity.relationships) {
184
+ for (const rel of entity.relationships) {
185
+ const targetExists = schema.entities.some((e) => e.name === rel.target);
186
+ if (!targetExists) {
187
+ errors.push(`Entity "${entity.name}": relationship target "${rel.target}" does not reference an existing entity`);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ if (errors.length > 0) {
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: `Schema: ${entityCount} entities, ${fieldCount} fields, ${relationshipCount} relationships\n\nValidation failed with ${errors.length} error(s):\n${errors.map((e) => ` - ${e}`).join("\n")}`,
198
+ },
199
+ ],
200
+ isError: true,
201
+ };
202
+ }
203
+ return {
204
+ content: [
205
+ {
206
+ type: "text",
207
+ text: `Schema: ${entityCount} entities, ${fieldCount} fields, ${relationshipCount} relationships\nValidation passed.`,
208
+ },
209
+ ],
210
+ };
211
+ }
212
+ catch (error) {
213
+ const msg = error instanceof Error ? error.message : String(error);
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: `Validation error: ${msg}`,
219
+ },
220
+ ],
221
+ isError: true,
222
+ };
223
+ }
224
+ });
225
+ // ── scaffold_api ───────────────────────────────────────────────
226
+ server.tool("scaffold_api", "Generate a production-ready REST API from a schema. Creates endpoints, models, validation, DTOs, and OpenAPI docs. Requires an .apsorc file in the current directory.", {
227
+ language: zod_1.z
228
+ .enum(["typescript", "python", "go"])
229
+ .optional()
230
+ .describe("Target language for code generation (default: uses .apsorc config or typescript)"),
231
+ skip_format: zod_1.z
232
+ .boolean()
233
+ .optional()
234
+ .describe("Skip code formatting after generation (default: false)"),
235
+ }, async ({ language, skip_format }) => {
236
+ try {
237
+ const configPath = (0, apsorc_parser_1.findConfigPath)();
238
+ if (!configPath) {
239
+ return {
240
+ content: [
241
+ {
242
+ type: "text",
243
+ text: "No .apsorc file found. Create a schema first using design_schema, or run `apso init` to create a project.",
244
+ },
245
+ ],
246
+ isError: true,
247
+ };
248
+ }
249
+ const { rootFolder, entities, relationshipMap, apiType, auth, language: configLanguage, } = (0, apsorc_parser_1.parseApsorc)();
250
+ // Resolve language
251
+ let lang;
252
+ if (language) {
253
+ lang = language;
254
+ }
255
+ else if (configLanguage && (0, lib_1.isLanguageSupported)(configLanguage)) {
256
+ lang = configLanguage;
257
+ }
258
+ else {
259
+ lang = "typescript";
260
+ }
261
+ const implementedLanguages = (0, lib_1.getImplementedLanguages)();
262
+ if (!implementedLanguages.includes(lang)) {
263
+ return {
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: `Language '${lang}' is not yet implemented. Available: ${implementedLanguages.join(", ")}`,
268
+ },
269
+ ],
270
+ isError: true,
271
+ };
272
+ }
273
+ const rootPath = path.join(process.cwd(), rootFolder);
274
+ const autogenPath = path.join(rootPath, "autogen");
275
+ const lowerCaseApiType = apiType.toLowerCase();
276
+ const generatorConfig = {
277
+ language: lang,
278
+ rootFolder,
279
+ apiType: lowerCaseApiType,
280
+ entities,
281
+ relationshipMap,
282
+ auth,
283
+ };
284
+ const generator = (0, lib_1.createGenerator)(generatorConfig);
285
+ const validationResult = generator.validateConfig(generatorConfig);
286
+ if (!validationResult.valid) {
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: `Configuration validation failed:\n${validationResult.errors.join("\n")}`,
292
+ },
293
+ ],
294
+ isError: true,
295
+ };
296
+ }
297
+ const totalStart = perf_hooks_1.performance.now();
298
+ const filesGenerated = [];
299
+ // Generate enums
300
+ const enumFiles = await generator.generateEnums(entities, lowerCaseApiType);
301
+ for (const file of enumFiles) {
302
+ const fullPath = path.join(autogenPath, file.path);
303
+ await (0, file_system_1.createFile)(fullPath, file.content);
304
+ filesGenerated.push(file.path);
305
+ }
306
+ // Generate per-entity files
307
+ for (const entity of entities) {
308
+ const entityRelationships = relationshipMap[entity.name] || [];
309
+ const allFiles = await Promise.all([
310
+ generator.generateEntity({
311
+ entity,
312
+ relationships: entityRelationships,
313
+ allEntities: entities,
314
+ apiType: lowerCaseApiType,
315
+ }),
316
+ generator.generateDto({
317
+ entity,
318
+ relationships: entityRelationships,
319
+ allEntities: entities,
320
+ apiType: lowerCaseApiType,
321
+ }),
322
+ generator.generateService({
323
+ entity,
324
+ relationships: entityRelationships,
325
+ allEntities: entities,
326
+ apiType: lowerCaseApiType,
327
+ relationshipMap,
328
+ }),
329
+ generator.generateController({
330
+ entity,
331
+ relationships: entityRelationships,
332
+ allEntities: entities,
333
+ apiType: lowerCaseApiType,
334
+ relationshipMap,
335
+ }),
336
+ generator.generateModule({
337
+ entity,
338
+ relationships: entityRelationships,
339
+ allEntities: entities,
340
+ apiType: lowerCaseApiType,
341
+ }),
342
+ ]);
343
+ for (const files of allFiles) {
344
+ for (const file of files) {
345
+ const fullPath = path.join(autogenPath, file.path);
346
+ await (0, file_system_1.createFile)(fullPath, file.content);
347
+ filesGenerated.push(file.path);
348
+ }
349
+ }
350
+ }
351
+ // Generate guards
352
+ const guardFiles = await generator.generateGuards(entities, auth);
353
+ for (const file of guardFiles) {
354
+ const fullPath = path.join(autogenPath, file.path);
355
+ await (0, file_system_1.createFile)(fullPath, file.content);
356
+ filesGenerated.push(file.path);
357
+ }
358
+ // Generate index module
359
+ const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType);
360
+ for (const file of indexFiles) {
361
+ const fullPath = path.join(autogenPath, file.path);
362
+ await (0, file_system_1.createFile)(fullPath, file.content);
363
+ filesGenerated.push(file.path);
364
+ }
365
+ const elapsed = (perf_hooks_1.performance.now() - totalStart).toFixed(0);
366
+ let warnings = "";
367
+ if (validationResult.warnings.length > 0) {
368
+ warnings = `\n\nWarnings:\n${validationResult.warnings.join("\n")}`;
369
+ }
370
+ return {
371
+ content: [
372
+ {
373
+ type: "text",
374
+ text: `Generated ${lang} REST API for ${entities.length} entities (${filesGenerated.length} files) in ${elapsed}ms.\n\nOutput: ${autogenPath}\n\nEntities: ${entities.map((e) => e.name).join(", ")}${warnings}\n\nNext steps:\n1. Run \`npm install\` to install dependencies\n2. Run \`apso dev\` to start the local development server\n3. Open http://localhost:3001/api/docs for OpenAPI documentation`,
375
+ },
376
+ ],
377
+ };
378
+ }
379
+ catch (error) {
380
+ const msg = error instanceof Error ? error.message : String(error);
381
+ return {
382
+ content: [
383
+ {
384
+ type: "text",
385
+ text: `Code generation failed: ${msg}`,
386
+ },
387
+ ],
388
+ isError: true,
389
+ };
390
+ }
391
+ });
392
+ // ── setup_auth ─────────────────────────────────────────────────
393
+ server.tool("setup_auth", "Add authentication and multi-tenancy to an API. Returns the auth entity definitions and configuration to add to your .apsorc schema. After updating the schema, run scaffold_api to regenerate the code.", {
394
+ provider: zod_1.z
395
+ .enum([
396
+ "better-auth",
397
+ "auth0",
398
+ "clerk",
399
+ "cognito",
400
+ "api-key",
401
+ "custom-db-session",
402
+ ])
403
+ .optional()
404
+ .describe("Authentication provider (default: better-auth)"),
405
+ }, async ({ provider }) => {
406
+ const authProvider = provider || "better-auth";
407
+ const authConfigs = {
408
+ "better-auth": {
409
+ auth: { provider: "better-auth" },
410
+ entities: [
411
+ {
412
+ name: "User",
413
+ created_at: true,
414
+ updated_at: true,
415
+ fields: [
416
+ { name: "email", type: "text", length: 255, is_email: true },
417
+ { name: "name", type: "text", length: 100, nullable: true },
418
+ { name: "avatar_url", type: "text", nullable: true },
419
+ { name: "email_verified", type: "boolean", default: "false" },
420
+ ],
421
+ },
422
+ {
423
+ name: "account",
424
+ created_at: true,
425
+ updated_at: true,
426
+ fields: [
427
+ { name: "providerId", type: "text", length: 50 },
428
+ { name: "accountId", type: "text", length: 255 },
429
+ { name: "password", type: "text", nullable: true },
430
+ ],
431
+ },
432
+ {
433
+ name: "session",
434
+ created_at: true,
435
+ updated_at: true,
436
+ fields: [
437
+ { name: "token", type: "text", length: 255, unique: true },
438
+ { name: "expiresAt", type: "date" },
439
+ { name: "ipAddress", type: "text", length: 45, nullable: true },
440
+ { name: "userAgent", type: "text", nullable: true },
441
+ ],
442
+ },
443
+ {
444
+ name: "verification",
445
+ created_at: true,
446
+ updated_at: true,
447
+ fields: [
448
+ { name: "identifier", type: "text", length: 255 },
449
+ { name: "value", type: "text", length: 255 },
450
+ { name: "expiresAt", type: "date" },
451
+ ],
452
+ },
453
+ ],
454
+ relationships: [
455
+ { from: "User", to: "account", type: "OneToMany" },
456
+ { from: "account", to: "User", type: "ManyToOne" },
457
+ { from: "User", to: "session", type: "OneToMany" },
458
+ { from: "session", to: "User", type: "ManyToOne" },
459
+ ],
460
+ },
461
+ "auth0": {
462
+ auth: {
463
+ provider: "auth0",
464
+ jwt: {
465
+ issuer: "https://YOUR_TENANT.auth0.com/",
466
+ audience: "https://api.yourapp.com",
467
+ },
468
+ claims: {
469
+ userId: "sub",
470
+ email: "email",
471
+ roles: "roles",
472
+ organizationId: "org_id",
473
+ },
474
+ },
475
+ entities: [],
476
+ relationships: [],
477
+ },
478
+ clerk: {
479
+ auth: {
480
+ provider: "clerk",
481
+ jwt: {
482
+ issuer: "https://YOUR_CLERK_FRONTEND_API",
483
+ audience: "",
484
+ },
485
+ claims: {
486
+ userId: "sub",
487
+ email: "email",
488
+ roles: "roles",
489
+ organizationId: "org_id",
490
+ },
491
+ },
492
+ entities: [],
493
+ relationships: [],
494
+ },
495
+ cognito: {
496
+ auth: {
497
+ provider: "cognito",
498
+ jwt: {
499
+ issuer: "https://cognito-idp.REGION.amazonaws.com/POOL_ID",
500
+ audience: "YOUR_CLIENT_ID",
501
+ },
502
+ claims: {
503
+ userId: "sub",
504
+ email: "email",
505
+ roles: "cognito:groups",
506
+ organizationId: "custom:org_id",
507
+ },
508
+ },
509
+ entities: [],
510
+ relationships: [],
511
+ },
512
+ "api-key": {
513
+ auth: {
514
+ provider: "api-key",
515
+ apiKeyHeader: "x-api-key",
516
+ apiKeyEntity: "ApiKey",
517
+ },
518
+ entities: [
519
+ {
520
+ name: "ApiKey",
521
+ created_at: true,
522
+ updated_at: true,
523
+ scopeBy: "organizationId",
524
+ fields: [
525
+ { name: "name", type: "text", length: 100 },
526
+ { name: "keyHash", type: "text", length: 255 },
527
+ { name: "prefix", type: "text", length: 10 },
528
+ { name: "scopes", type: "json-plain", nullable: true },
529
+ { name: "expiresAt", type: "date", nullable: true },
530
+ { name: "lastUsedAt", type: "date", nullable: true },
531
+ { name: "status", type: "enum", values: ["active", "revoked"], default: "active" },
532
+ ],
533
+ },
534
+ ],
535
+ relationships: [
536
+ { from: "Organization", to: "ApiKey", type: "OneToMany" },
537
+ { from: "ApiKey", to: "Organization", type: "ManyToOne" },
538
+ ],
539
+ },
540
+ "custom-db-session": {
541
+ auth: { provider: "custom-db-session" },
542
+ entities: [],
543
+ relationships: [],
544
+ },
545
+ };
546
+ const config = authConfigs[authProvider] || authConfigs["better-auth"];
547
+ return {
548
+ content: [
549
+ {
550
+ type: "text",
551
+ text: `# Auth Setup: ${authProvider}\n\nMerge the following into your .apsorc file:\n\n\`\`\`json\n${JSON.stringify(config, null, 2)}\n\`\`\`\n\nAfter updating .apsorc:\n1. Run \`apso generate\` (or use the scaffold_api tool) to regenerate code\n2. Run \`npm install\` if new dependencies are needed\n3. Run \`apso dev\` to start the server and create auth tables\n\n${authProvider === "better-auth" ? "**Important:** BetterAuth stores passwords in the `account` table (not User). The `providerId` field must be set to \"credential\" for email/password login to work. Use `@apso/better-auth-adapter@latest` in your frontend." : "Configure the JWT issuer and audience values for your " + authProvider + " tenant."}`,
552
+ },
553
+ ],
554
+ };
555
+ });
556
+ // ── start_dev_server ───────────────────────────────────────────
557
+ server.tool("start_dev_server", "Start the local development environment. Launches PostgreSQL via Docker Compose and starts the API server with hot reload.", {
558
+ detach: zod_1.z
559
+ .boolean()
560
+ .optional()
561
+ .describe("Run containers in the background (default: false)"),
562
+ build: zod_1.z
563
+ .boolean()
564
+ .optional()
565
+ .describe("Rebuild images before starting (default: false)"),
566
+ }, async ({ detach, build }) => {
567
+ if (!fs.existsSync(path.join(process.cwd(), "docker-compose.yml"))) {
568
+ return {
569
+ content: [
570
+ {
571
+ type: "text",
572
+ text: "No docker-compose.yml found in the current directory. Make sure you are in an Apso project root. Run `apso init` to create a project first.",
573
+ },
574
+ ],
575
+ isError: true,
576
+ };
577
+ }
578
+ const args = ["dev"];
579
+ if (build)
580
+ args.push("--build");
581
+ if (detach)
582
+ args.push("--detach");
583
+ return {
584
+ content: [
585
+ {
586
+ type: "text",
587
+ text: `To start the dev server, run:\n\n\`\`\`bash\napso ${args.join(" ")}\n\`\`\`\n\nThis will:\n1. Start PostgreSQL via Docker Compose\n2. Create database tables from your schema\n3. Start the NestJS server with hot reload\n\nThe API will be available at:\n- API: http://localhost:3001\n- OpenAPI docs: http://localhost:3001/api/docs\n- Health check: http://localhost:3001/health\n\nAlternatively, run these commands individually:\n\`\`\`bash\nnpm run compose # Start PostgreSQL\nnpm run provision # Create tables\nnpm run start:dev # Start API server\n\`\`\``,
588
+ },
589
+ ],
590
+ };
591
+ });
592
+ // ── deploy_api ─────────────────────────────────────────────────
593
+ server.tool("deploy_api", "Deploy the API to production. Handles build, database migration, and infrastructure provisioning on AWS.", {
594
+ skip_migrate: zod_1.z
595
+ .boolean()
596
+ .optional()
597
+ .describe("Skip migration check (default: false)"),
598
+ yes: zod_1.z
599
+ .boolean()
600
+ .optional()
601
+ .describe("Skip confirmation prompt (default: false)"),
602
+ }, async ({ skip_migrate, yes }) => {
603
+ const configPath = (0, apsorc_parser_1.findConfigPath)();
604
+ if (!configPath) {
605
+ return {
606
+ content: [
607
+ {
608
+ type: "text",
609
+ text: "No .apsorc file found. Create and generate a project first.",
610
+ },
611
+ ],
612
+ isError: true,
613
+ };
614
+ }
615
+ const args = ["deploy"];
616
+ if (skip_migrate)
617
+ args.push("--skip-migrate");
618
+ if (yes)
619
+ args.push("--yes");
620
+ return {
621
+ content: [
622
+ {
623
+ type: "text",
624
+ text: `To deploy, run:\n\n\`\`\`bash\napso ${args.join(" ")}\n\`\`\`\n\nPrerequisites:\n1. Authenticate: \`apso login\`\n2. Link project: \`apso link\`\n3. Test migrations locally: \`apso migrate\`\n\nThe deploy command will:\n1. Validate the schema\n2. Run migration sandbox\n3. Show SQL preview for pending migrations\n4. Build and deploy to AWS (Lambda + RDS + API Gateway)\n\nAfter deployment:\n- \`apso status\` — Check deployment status\n- \`apso logs\` — View build logs\n- \`apso open\` — Open service dashboard`,
625
+ },
626
+ ],
627
+ };
628
+ });
629
+ }
630
+ registerResources(server) {
631
+ // ── Schema guide resource ──────────────────────────────────────
632
+ server.resource("schema-reference", "apso://schema-guide", {
633
+ description: "Complete reference for .apsorc schema format: field types, entity definitions, relationship patterns, auth configuration, and working examples.",
634
+ mimeType: "text/markdown",
635
+ }, async () => {
636
+ // Try to find the schema guide
637
+ const locations = [
638
+ path.join(__dirname, "../../references/apso-schema-guide.md"),
639
+ path.join(process.cwd(), ".claude/skills/schema-architect/references/apso-schema-guide.md"),
640
+ ];
641
+ for (const loc of locations) {
642
+ if (fs.existsSync(loc)) {
643
+ const content = fs.readFileSync(loc, "utf-8");
644
+ return {
645
+ contents: [
646
+ {
647
+ uri: "apso://schema-guide",
648
+ mimeType: "text/markdown",
649
+ text: content,
650
+ },
651
+ ],
652
+ };
653
+ }
654
+ }
655
+ // Inline minimal reference
656
+ return {
657
+ contents: [
658
+ {
659
+ uri: "apso://schema-guide",
660
+ mimeType: "text/markdown",
661
+ text: INLINE_SCHEMA_REFERENCE,
662
+ },
663
+ ],
664
+ };
665
+ });
666
+ // ── Current schema resource ────────────────────────────────────
667
+ server.resource("current-schema", "apso://current-schema", {
668
+ description: "The .apsorc schema file from the current project directory.",
669
+ mimeType: "application/json",
670
+ }, async () => {
671
+ const configPath = (0, apsorc_parser_1.findConfigPath)();
672
+ if (!configPath) {
673
+ return {
674
+ contents: [
675
+ {
676
+ uri: "apso://current-schema",
677
+ mimeType: "text/plain",
678
+ text: "No .apsorc file found in the current directory or parent directories.",
679
+ },
680
+ ],
681
+ };
682
+ }
683
+ const content = fs.readFileSync(configPath, "utf-8");
684
+ return {
685
+ contents: [
686
+ {
687
+ uri: "apso://current-schema",
688
+ mimeType: "application/json",
689
+ text: content,
690
+ },
691
+ ],
692
+ };
693
+ });
694
+ }
695
+ }
696
+ McpServe.description = "Start an MCP server exposing Apso tools over stdio. Used by AI coding agents (Claude Code, Cursor, etc.) to design schemas, generate APIs, and deploy backends.";
697
+ McpServe.examples = [
698
+ `$ apso mcp serve`,
699
+ `# In Claude Code settings:`,
700
+ `# { "mcpServers": { "api-tools": { "command": "apso", "args": ["mcp", "serve"] } } }`,
701
+ ];
702
+ McpServe.flags = {
703
+ help: core_1.Flags.help({ char: "h" }),
704
+ };
705
+ exports.default = McpServe;
706
+ // ── Validation helper ──────────────────────────────────────────────
707
+ function validateSchemaObject(schema) {
708
+ const errors = [];
709
+ if (!schema.version) {
710
+ errors.push('Missing "version" field. Use version: 2.');
711
+ }
712
+ else if (schema.version !== 2) {
713
+ errors.push(`Version ${schema.version} is not recommended. Use version: 2.`);
714
+ }
715
+ if (!schema.entities || !Array.isArray(schema.entities)) {
716
+ errors.push('"entities" must be an array.');
717
+ return errors;
718
+ }
719
+ const validFieldTypes = [
720
+ "text", "integer", "float", "decimal", "numeric",
721
+ "boolean", "date", "enum", "json", "json-plain", "array",
722
+ ];
723
+ const validRelTypes = ["OneToMany", "ManyToOne", "ManyToMany", "OneToOne"];
724
+ const entityNames = new Set();
725
+ for (const entity of schema.entities) {
726
+ if (!entity.name || typeof entity.name !== "string") {
727
+ errors.push("Entity found with missing or empty name.");
728
+ continue;
729
+ }
730
+ if (entityNames.has(entity.name)) {
731
+ errors.push(`Duplicate entity name: "${entity.name}".`);
732
+ }
733
+ entityNames.add(entity.name);
734
+ if (entity.fields && Array.isArray(entity.fields)) {
735
+ for (const field of entity.fields) {
736
+ if (!field.name) {
737
+ errors.push(`Entity "${entity.name}": field with missing name.`);
738
+ }
739
+ if (!field.type) {
740
+ errors.push(`Entity "${entity.name}": field "${field.name}" has no type.`);
741
+ }
742
+ else if (!validFieldTypes.includes(field.type)) {
743
+ errors.push(`Entity "${entity.name}": field "${field.name}" has invalid type "${field.type}". Valid: ${validFieldTypes.join(", ")}.`);
744
+ }
745
+ if (field.type === "enum" && (!field.values || !Array.isArray(field.values))) {
746
+ errors.push(`Entity "${entity.name}": enum field "${field.name}" requires a "values" array.`);
747
+ }
748
+ }
749
+ }
750
+ }
751
+ if (schema.relationships && Array.isArray(schema.relationships)) {
752
+ for (const rel of schema.relationships) {
753
+ if (!rel.from || !entityNames.has(rel.from)) {
754
+ errors.push(`Relationship "from" entity "${rel.from}" does not exist.`);
755
+ }
756
+ if (!rel.to || !entityNames.has(rel.to)) {
757
+ errors.push(`Relationship "to" entity "${rel.to}" does not exist.`);
758
+ }
759
+ if (!rel.type || !validRelTypes.includes(rel.type)) {
760
+ errors.push(`Relationship ${rel.from} -> ${rel.to}: invalid type "${rel.type}". Valid: ${validRelTypes.join(", ")}.`);
761
+ }
762
+ }
763
+ }
764
+ return errors;
765
+ }
766
+ // ── Inline schema reference (fallback when guide file not found) ──
767
+ const INLINE_SCHEMA_REFERENCE = `# Schema Quick Reference
768
+
769
+ ## Field Types
770
+ | Type | Description |
771
+ |------|-------------|
772
+ | text | String/varchar (use \`length\` for max) |
773
+ | integer | Integer number |
774
+ | float | Floating point |
775
+ | decimal | Fixed precision (\`precision\`, \`scale\`) |
776
+ | numeric | Alias for decimal |
777
+ | boolean | True/false |
778
+ | date | Date column |
779
+ | enum | Enumerated values (requires \`values\` array) |
780
+ | json | JSON (TypeORM simple-json) |
781
+ | json-plain | JSON (raw jsonb) |
782
+ | array | Array column |
783
+
784
+ ## Entity Structure
785
+ \`\`\`json
786
+ {
787
+ "name": "EntityName",
788
+ "created_at": true,
789
+ "updated_at": true,
790
+ "primaryKeyType": "serial",
791
+ "scopeBy": "organizationId",
792
+ "fields": [
793
+ { "name": "fieldName", "type": "text", "length": 100 }
794
+ ],
795
+ "indexes": [
796
+ { "fields": ["field1", "field2"], "unique": false }
797
+ ]
798
+ }
799
+ \`\`\`
800
+
801
+ ## Relationship Types
802
+ - OneToMany: parent has many children
803
+ - ManyToOne: child belongs to parent
804
+ - ManyToMany: join table between entities
805
+ - OneToOne: one-to-one link
806
+
807
+ ## Relationship Structure
808
+ \`\`\`json
809
+ {
810
+ "from": "EntityA",
811
+ "to": "EntityB",
812
+ "type": "ManyToOne",
813
+ "to_name": "customName",
814
+ "nullable": false,
815
+ "index": true
816
+ }
817
+ \`\`\`
818
+ `;