@objectifthunes/create-sandstone 0.1.0 → 1.0.0

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/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +31 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/add-adapter.d.ts +2 -0
  6. package/dist/commands/add-adapter.d.ts.map +1 -0
  7. package/dist/commands/add-adapter.js +683 -0
  8. package/dist/commands/add-adapter.js.map +1 -0
  9. package/dist/commands/generate-adapter.d.ts +2 -0
  10. package/dist/commands/generate-adapter.d.ts.map +1 -0
  11. package/dist/commands/generate-adapter.js +467 -0
  12. package/dist/commands/generate-adapter.js.map +1 -0
  13. package/dist/commands/generate-entity.d.ts +2 -0
  14. package/dist/commands/generate-entity.d.ts.map +1 -0
  15. package/dist/commands/generate-entity.js +210 -0
  16. package/dist/commands/generate-entity.js.map +1 -0
  17. package/dist/generator.d.ts +3 -0
  18. package/dist/generator.d.ts.map +1 -0
  19. package/dist/generator.js +85 -0
  20. package/dist/generator.js.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +107 -61
  24. package/dist/index.js.map +1 -0
  25. package/dist/presets.d.ts +45 -0
  26. package/dist/presets.d.ts.map +1 -0
  27. package/dist/presets.js +105 -0
  28. package/dist/presets.js.map +1 -0
  29. package/dist/prompts.d.ts +3 -13
  30. package/dist/prompts.d.ts.map +1 -0
  31. package/dist/prompts.js +222 -49
  32. package/dist/prompts.js.map +1 -0
  33. package/dist/templates/claude-md.d.ts +3 -0
  34. package/dist/templates/claude-md.d.ts.map +1 -0
  35. package/dist/templates/claude-md.js +853 -0
  36. package/dist/templates/claude-md.js.map +1 -0
  37. package/dist/templates/docker-compose.d.ts +3 -0
  38. package/dist/templates/docker-compose.d.ts.map +1 -0
  39. package/dist/templates/docker-compose.js +75 -0
  40. package/dist/templates/docker-compose.js.map +1 -0
  41. package/dist/templates/env-example.d.ts +3 -0
  42. package/dist/templates/env-example.d.ts.map +1 -0
  43. package/dist/templates/env-example.js +137 -0
  44. package/dist/templates/env-example.js.map +1 -0
  45. package/dist/templates/gitignore.d.ts +2 -0
  46. package/dist/templates/gitignore.d.ts.map +1 -0
  47. package/dist/templates/gitignore.js +11 -0
  48. package/dist/templates/gitignore.js.map +1 -0
  49. package/dist/templates/infrastructure.d.ts +3 -0
  50. package/dist/templates/infrastructure.d.ts.map +1 -0
  51. package/dist/templates/infrastructure.js +647 -0
  52. package/dist/templates/infrastructure.js.map +1 -0
  53. package/dist/templates/main-express.d.ts +3 -0
  54. package/dist/templates/main-express.d.ts.map +1 -0
  55. package/dist/templates/main-express.js +80 -0
  56. package/dist/templates/main-express.js.map +1 -0
  57. package/dist/templates/main-fastify.d.ts +3 -0
  58. package/dist/templates/main-fastify.d.ts.map +1 -0
  59. package/dist/templates/main-fastify.js +73 -0
  60. package/dist/templates/main-fastify.js.map +1 -0
  61. package/dist/templates/main-hono.d.ts +3 -0
  62. package/dist/templates/main-hono.d.ts.map +1 -0
  63. package/dist/templates/main-hono.js +83 -0
  64. package/dist/templates/main-hono.js.map +1 -0
  65. package/dist/templates/migrate-script.d.ts +2 -0
  66. package/dist/templates/migrate-script.d.ts.map +1 -0
  67. package/dist/templates/migrate-script.js +18 -0
  68. package/dist/templates/migrate-script.js.map +1 -0
  69. package/dist/templates/migration.d.ts +2 -0
  70. package/dist/templates/migration.d.ts.map +1 -0
  71. package/dist/templates/migration.js +17 -0
  72. package/dist/templates/migration.js.map +1 -0
  73. package/dist/templates/package-json.d.ts +3 -0
  74. package/dist/templates/package-json.d.ts.map +1 -0
  75. package/dist/templates/package-json.js +149 -0
  76. package/dist/templates/package-json.js.map +1 -0
  77. package/dist/templates/schema.d.ts +3 -0
  78. package/dist/templates/schema.d.ts.map +1 -0
  79. package/dist/templates/schema.js +108 -0
  80. package/dist/templates/schema.js.map +1 -0
  81. package/dist/templates/test-setup.d.ts +3 -0
  82. package/dist/templates/test-setup.d.ts.map +1 -0
  83. package/dist/templates/test-setup.js +12 -0
  84. package/dist/templates/test-setup.js.map +1 -0
  85. package/dist/templates/tsconfig.d.ts +2 -0
  86. package/dist/templates/tsconfig.d.ts.map +1 -0
  87. package/dist/templates/tsconfig.js +21 -0
  88. package/dist/templates/tsconfig.js.map +1 -0
  89. package/package.json +16 -9
  90. package/dist/generators.d.ts +0 -7
  91. package/dist/generators.js +0 -328
@@ -0,0 +1,853 @@
1
+ export function claudeMdTemplate(config) {
2
+ const frameworkName = config.framework === 'express' ? 'Express' : 'Hono';
3
+ const lines = [];
4
+ // Title & What This Is
5
+ lines.push(`# ${config.name}`);
6
+ lines.push('');
7
+ lines.push('## What This Is');
8
+ lines.push(`Built with \`@objectifthunes/sandstone-sdk\` — a hexagonal-architecture backend toolkit.`);
9
+ lines.push(`Framework: ${frameworkName}. Database: PostgreSQL.`);
10
+ lines.push('');
11
+ // Architecture
12
+ lines.push('## Architecture');
13
+ lines.push('');
14
+ lines.push('- `src/infrastructure.ts` — all adapter wiring (database, auth, email, etc.)');
15
+ lines.push(`- \`src/main.ts\` — HTTP server entry point (${frameworkName})`);
16
+ if (config.graphql) {
17
+ lines.push('- `src/schema.ts` — GraphQL schema definition');
18
+ }
19
+ lines.push('- `migrations/` — SQL migrations (.up.sql / .down.sql)');
20
+ lines.push('- `scripts/migrate.ts` — migration runner script');
21
+ if (config.tests) {
22
+ lines.push('- `tests/` — test setup with in-memory adapters');
23
+ }
24
+ lines.push('');
25
+ // Stack table
26
+ lines.push('## Stack');
27
+ lines.push('');
28
+ lines.push('| Concern | Adapter | Import path |');
29
+ lines.push('|---------|---------|-------------|');
30
+ if (config.devMode) {
31
+ lines.push('| Database | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
32
+ lines.push('| Logger | Console (dev) | `@objectifthunes/sandstone-sdk/dev` |');
33
+ lines.push('| Tokens | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
34
+ if (config.email) {
35
+ lines.push('| Email | Console (dev) | `@objectifthunes/sandstone-sdk/dev` |');
36
+ }
37
+ if (config.sms) {
38
+ lines.push('| SMS | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
39
+ }
40
+ if (config.push) {
41
+ lines.push('| Push | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
42
+ }
43
+ if (config.realtime) {
44
+ lines.push('| Realtime | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
45
+ }
46
+ if (config.search) {
47
+ lines.push('| Search | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
48
+ }
49
+ if (config.queue) {
50
+ lines.push('| Queue | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
51
+ }
52
+ if (config.scheduler) {
53
+ lines.push('| Scheduler | In-memory tick source (dev) | `@objectifthunes/sandstone-sdk/dev` |');
54
+ }
55
+ if (config.flags) {
56
+ lines.push('| Feature Flags | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
57
+ }
58
+ if (config.tracing) {
59
+ lines.push('| Tracing | No-op (dev) | `@objectifthunes/sandstone-sdk/dev` |');
60
+ }
61
+ if (config.audit) {
62
+ lines.push('| Audit | In-memory (dev) | built-in |');
63
+ }
64
+ if (config.authz) {
65
+ lines.push('| Authorization | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
66
+ }
67
+ if (config.cache) {
68
+ lines.push('| Cache | In-memory (dev) | `@objectifthunes/sandstone-sdk/dev` |');
69
+ }
70
+ if (config.dashboard) {
71
+ lines.push('| Dashboard | Built-in | built-in |');
72
+ }
73
+ if (config.auth === 'otp') {
74
+ lines.push('| Auth | OTP (magic link / code) | built-in |');
75
+ }
76
+ if (config.graphql) {
77
+ lines.push('| GraphQL | graphql-js | built-in |');
78
+ }
79
+ }
80
+ else {
81
+ if (config.database === 'supabase') {
82
+ lines.push('| Database | Supabase (PostgreSQL) | `@objectifthunes/sandstone-sdk/supabase` |');
83
+ }
84
+ else {
85
+ lines.push('| Database | PostgreSQL (`pg`) | `@objectifthunes/sandstone-sdk/pg` |');
86
+ }
87
+ lines.push('| Logger | Pino | `@objectifthunes/sandstone-sdk/pino` |');
88
+ lines.push('| Tokens | JOSE (JWT) | `@objectifthunes/sandstone-sdk/jose` |');
89
+ if (config.email === 'resend') {
90
+ lines.push('| Email | Resend | `@objectifthunes/sandstone-sdk/resend` |');
91
+ }
92
+ else if (config.email === 'nodemailer') {
93
+ lines.push('| Email | Nodemailer (SMTP) | `@objectifthunes/sandstone-sdk/nodemailer` |');
94
+ }
95
+ if (config.auth === 'otp') {
96
+ lines.push('| Auth | OTP (magic link / code) | built-in |');
97
+ }
98
+ else if (config.auth === 'password+otp') {
99
+ lines.push('| Auth | Password + OTP | built-in + `@objectifthunes/sandstone-sdk/argon2` |');
100
+ }
101
+ else if (config.auth === 'oauth+otp') {
102
+ lines.push('| Auth | OAuth (Google) + OTP | built-in + `@objectifthunes/sandstone-sdk/oauth-google` |');
103
+ }
104
+ else if (config.auth === 'all') {
105
+ lines.push('| Auth | Password + OTP + OAuth | built-in + `@objectifthunes/sandstone-sdk/argon2` + `@objectifthunes/sandstone-sdk/oauth-google` |');
106
+ }
107
+ if (config.payments === 'stripe') {
108
+ lines.push('| Payments | Stripe | `@objectifthunes/sandstone-sdk/stripe` |');
109
+ }
110
+ else if (config.payments === 'revenuecat') {
111
+ lines.push('| Payments | RevenueCat | `@objectifthunes/sandstone-sdk/revenuecat` |');
112
+ }
113
+ else if (config.payments === 'paddle') {
114
+ lines.push('| Payments | Paddle | `@objectifthunes/sandstone-sdk/paddle` |');
115
+ }
116
+ else if (config.payments === 'lemonsqueezy') {
117
+ lines.push('| Payments | Lemon Squeezy | `@objectifthunes/sandstone-sdk/lemonsqueezy` |');
118
+ }
119
+ if (config.storage === 's3') {
120
+ lines.push('| Storage | AWS S3 | `@objectifthunes/sandstone-sdk/s3` |');
121
+ }
122
+ else if (config.storage === 'r2') {
123
+ lines.push('| Storage | Cloudflare R2 | `@objectifthunes/sandstone-sdk/r2` |');
124
+ }
125
+ else if (config.storage === 'local') {
126
+ lines.push('| Storage | Local filesystem | `@objectifthunes/sandstone-sdk/local-storage` |');
127
+ }
128
+ if (config.cache === 'upstash') {
129
+ lines.push('| Cache | Upstash Redis | `@objectifthunes/sandstone-sdk/upstash` |');
130
+ }
131
+ else if (config.cache === 'redis') {
132
+ lines.push('| Cache | Redis (ioredis) | `@objectifthunes/sandstone-sdk/redis` |');
133
+ }
134
+ else if (config.cache === 'memory') {
135
+ lines.push('| Cache | In-memory LRU | `@objectifthunes/sandstone-sdk/memory` |');
136
+ }
137
+ if (config.sms === 'twilio') {
138
+ lines.push('| SMS | Twilio | `@objectifthunes/sandstone-sdk/twilio` |');
139
+ }
140
+ else if (config.sms === 'sns') {
141
+ lines.push('| SMS | AWS SNS | `@objectifthunes/sandstone-sdk/sns` |');
142
+ }
143
+ if (config.push === 'fcm') {
144
+ lines.push('| Push | Firebase Cloud Messaging | `@objectifthunes/sandstone-sdk/fcm` |');
145
+ }
146
+ else if (config.push === 'apns') {
147
+ lines.push('| Push | Apple Push Notification service | `@objectifthunes/sandstone-sdk/apns` |');
148
+ }
149
+ else if (config.push === 'both') {
150
+ lines.push('| Push | FCM + APNs | `@objectifthunes/sandstone-sdk/fcm` + `@objectifthunes/sandstone-sdk/apns` |');
151
+ }
152
+ if (config.realtime === 'socketio') {
153
+ lines.push('| Realtime | Socket.io | `@objectifthunes/sandstone-sdk/socketio` |');
154
+ }
155
+ else if (config.realtime === 'pusher') {
156
+ lines.push('| Realtime | Pusher Channels | `@objectifthunes/sandstone-sdk/pusher` |');
157
+ }
158
+ else if (config.realtime === 'ably') {
159
+ lines.push('| Realtime | Ably | `@objectifthunes/sandstone-sdk/ably` |');
160
+ }
161
+ if (config.search === 'meilisearch') {
162
+ lines.push('| Search | Meilisearch | `@objectifthunes/sandstone-sdk/meilisearch` |');
163
+ }
164
+ else if (config.search === 'typesense') {
165
+ lines.push('| Search | Typesense | `@objectifthunes/sandstone-sdk/typesense` |');
166
+ }
167
+ else if (config.search === 'pg-search') {
168
+ lines.push('| Search | PostgreSQL full-text | `@objectifthunes/sandstone-sdk/pg-search` |');
169
+ }
170
+ if (config.queue === 'bullmq') {
171
+ lines.push('| Queue | BullMQ (Redis) | `@objectifthunes/sandstone-sdk/bullmq` |');
172
+ }
173
+ else if (config.queue === 'pg-boss') {
174
+ lines.push('| Queue | pg-boss (PostgreSQL) | `@objectifthunes/sandstone-sdk/pg-boss` |');
175
+ }
176
+ if (config.scheduler) {
177
+ lines.push('| Scheduler | node-cron | `@objectifthunes/sandstone-sdk/node-cron` |');
178
+ }
179
+ if (config.flags === 'pg-flags') {
180
+ lines.push('| Feature Flags | PostgreSQL | `@objectifthunes/sandstone-sdk/pg-flags` |');
181
+ }
182
+ else if (config.flags === 'env-flags') {
183
+ lines.push('| Feature Flags | Environment variables | `@objectifthunes/sandstone-sdk/env-flags` |');
184
+ }
185
+ else if (config.flags === 'launchdarkly') {
186
+ lines.push('| Feature Flags | LaunchDarkly | `@objectifthunes/sandstone-sdk/launchdarkly` |');
187
+ }
188
+ if (config.tracing) {
189
+ lines.push('| Tracing | OpenTelemetry | `@objectifthunes/sandstone-sdk/otel` |');
190
+ }
191
+ if (config.audit) {
192
+ lines.push('| Audit | Built-in (DB-backed) | built-in |');
193
+ }
194
+ if (config.authz === 'pg-authz') {
195
+ lines.push('| Authorization | PostgreSQL | `@objectifthunes/sandstone-sdk/pg-authz` |');
196
+ }
197
+ else if (config.authz === 'casbin') {
198
+ lines.push('| Authorization | Casbin | `@objectifthunes/sandstone-sdk/casbin` |');
199
+ }
200
+ if (config.dashboard) {
201
+ lines.push('| Dashboard | Built-in | built-in |');
202
+ }
203
+ if (config.graphql) {
204
+ lines.push('| GraphQL | graphql-js | built-in |');
205
+ }
206
+ }
207
+ lines.push('');
208
+ // Key Files
209
+ lines.push('## Key Files');
210
+ lines.push('');
211
+ lines.push('### `src/infrastructure.ts`');
212
+ lines.push('Wires all adapters together and exports the `app` instance created by `createApp()`.');
213
+ lines.push('This is the single place to swap adapters — change a provider here and the rest of the codebase is unaffected.');
214
+ lines.push('');
215
+ lines.push('### `src/main.ts`');
216
+ lines.push(`Starts the ${frameworkName} HTTP server. Runs migrations on startup, registers route handlers via the framework adapter, and handles graceful shutdown on SIGTERM.`);
217
+ lines.push('');
218
+ if (config.graphql) {
219
+ lines.push('### `src/schema.ts`');
220
+ lines.push('Defines the GraphQL schema using `graphql-js`. Add new types, queries, and mutations here.');
221
+ lines.push('');
222
+ }
223
+ lines.push('### `migrations/`');
224
+ lines.push('Plain SQL files named `NNN_description.up.sql` / `.down.sql`. Run with `npm run db:migrate`.');
225
+ lines.push('');
226
+ if (config.tests) {
227
+ lines.push('### `tests/setup.ts`');
228
+ lines.push('Vitest test setup. Uses in-memory adapters so tests run without external services.');
229
+ lines.push('');
230
+ }
231
+ if (config.dashboard) {
232
+ lines.push('- Dashboard available at `/_dashboard` — shows port wiring, health, queue stats, flags, audit trail');
233
+ lines.push('');
234
+ }
235
+ // SDK Reference
236
+ lines.push('## SDK Reference');
237
+ lines.push('');
238
+ lines.push('**Core concepts:**');
239
+ lines.push('');
240
+ lines.push('- **Port** — an interface that defines a capability (e.g., `EmailPort`, `StoragePort`). The SDK ships ports for every concern.');
241
+ lines.push('- **Adapter** — a concrete implementation of a port (e.g., `createResendTransport` implements `EmailPort`).');
242
+ lines.push('- **`createApp(options)`** — assembles all adapters into a single `app` object that exposes typed handler factories, the `db` client, `logger`, and a `shutdown()` method.');
243
+ lines.push('- **`runMigrations(db)`** — applies pending SQL migrations from `migrations/`.');
244
+ lines.push('');
245
+ lines.push('**Error types** (import from `@objectifthunes/sandstone-sdk`):');
246
+ lines.push('');
247
+ lines.push('- `NotFoundError` — resource not found (maps to HTTP 404)');
248
+ lines.push('- `UnauthorizedError` — missing/invalid credentials (maps to HTTP 401)');
249
+ lines.push('- `ForbiddenError` — insufficient permissions (maps to HTTP 403)');
250
+ lines.push('- `ValidationError` — invalid input (maps to HTTP 422)');
251
+ lines.push('- `ConflictError` — duplicate resource (maps to HTTP 409)');
252
+ lines.push('');
253
+ lines.push('**Docs:** https://maxoujs.github.io/sandstone-sdk/');
254
+ lines.push('');
255
+ // Common Tasks
256
+ lines.push('## Common Tasks');
257
+ lines.push('');
258
+ lines.push('- **Add a new entity:** create a migration in `migrations/`, add a repository using `app.db`, wire it into a handler.');
259
+ if (config.graphql) {
260
+ lines.push('- **Add a GraphQL type/query/mutation:** edit `src/schema.ts` and add the resolver logic.');
261
+ }
262
+ if (config.auth) {
263
+ lines.push('- **Configure authentication:** adjust the `auth` options in `src/infrastructure.ts`.');
264
+ }
265
+ if (config.email) {
266
+ lines.push('- **Change email provider:** swap the transport import in `src/infrastructure.ts` and update `.env`.');
267
+ }
268
+ if (config.payments) {
269
+ lines.push('- **Change payment provider:** swap the provider import in `src/infrastructure.ts` and update `.env`.');
270
+ }
271
+ if (config.storage) {
272
+ lines.push('- **Change storage provider:** swap the storage import in `src/infrastructure.ts` and update `.env`.');
273
+ }
274
+ if (config.sms) {
275
+ lines.push("- **Send an SMS:** `await app.sms!.send({ to: '+1234567890', body: 'Hello' })`");
276
+ }
277
+ if (config.push) {
278
+ lines.push("- **Send a push notification:** `await app.push!.send(deviceToken, { title: 'Hello', body: 'World' })`");
279
+ }
280
+ if (config.realtime) {
281
+ lines.push("- **Publish a realtime event:** `await app.realtime!.publish('channel', 'event', { data })`");
282
+ }
283
+ if (config.search) {
284
+ lines.push("- **Index documents:** `await app.search!.index('products', [{ id: '1', document: { name: 'Widget' } }])`");
285
+ lines.push("- **Search:** `await app.search!.search('products', 'widget')`");
286
+ }
287
+ if (config.queue) {
288
+ lines.push("- **Enqueue a job:** `await app.queue!.enqueue('emails', { to: 'user@test.com' })`");
289
+ }
290
+ if (config.flags) {
291
+ lines.push("- **Check a feature flag:** `await app.flags!.isEnabled('new-ui', { userId: '123' })`");
292
+ }
293
+ if (config.audit) {
294
+ lines.push("- **Log an audit entry:** `await app.audit!.log({ actor: userId, action: 'update', resource: 'post', resourceId: postId })`");
295
+ }
296
+ if (config.authz) {
297
+ lines.push("- **Check permission:** `await app.authz!.can('user:123', 'edit', 'post', postId)`");
298
+ lines.push("- **Grant permission:** `await app.authz!.grant('user:123', 'edit', 'post')`");
299
+ }
300
+ if (config.dashboard) {
301
+ lines.push('- **Access dashboard:** visit `http://localhost:3000/_dashboard`');
302
+ }
303
+ if (config.tracing) {
304
+ lines.push("- **Create a trace span:** `await app.tracer.withSpan('operation', async (span) => { ... })`");
305
+ }
306
+ lines.push('- **Run migrations:** `npm run db:migrate`');
307
+ lines.push('- **Add a migration:** create `migrations/NNN_description.up.sql` (and `.down.sql`).');
308
+ lines.push('');
309
+ // Commands
310
+ lines.push('## Commands');
311
+ lines.push('');
312
+ lines.push('```sh');
313
+ lines.push('npm run dev # start dev server with hot-reload');
314
+ lines.push('npm run build # compile TypeScript to dist/');
315
+ lines.push('npm start # run compiled output');
316
+ lines.push('npm run lint # type-check without emitting');
317
+ lines.push('npm run db:migrate # run pending SQL migrations');
318
+ if (config.tests) {
319
+ lines.push('npm test # run unit tests (vitest)');
320
+ lines.push('npm run test:watch # run tests in watch mode');
321
+ }
322
+ if (config.e2e) {
323
+ lines.push('npm run test:e2e # run end-to-end tests');
324
+ }
325
+ lines.push('```');
326
+ lines.push('');
327
+ // Environment Variables
328
+ lines.push('## Environment Variables');
329
+ lines.push('');
330
+ if (config.devMode) {
331
+ lines.push('This project uses `./dev` imports — **no env vars required** to start.');
332
+ lines.push('');
333
+ lines.push('| Variable | Required | Description |');
334
+ lines.push('|----------|----------|-------------|');
335
+ lines.push('| `PORT` | No | HTTP port (default: 3000) |');
336
+ lines.push('| `LOG_LEVEL` | No | Log level (default: `info`) |');
337
+ lines.push('');
338
+ }
339
+ else {
340
+ lines.push('Copy `.env.example` to `.env` and fill in the values.');
341
+ lines.push('');
342
+ lines.push('| Variable | Required | Description |');
343
+ lines.push('|----------|----------|-------------|');
344
+ lines.push('| `PORT` | No | HTTP port (default: 3000) |');
345
+ lines.push('| `NODE_ENV` | No | `development` or `production` |');
346
+ lines.push('| `LOG_LEVEL` | No | Pino log level (default: `info`) |');
347
+ if (config.database === 'supabase') {
348
+ lines.push('| `DATABASE_URL` | **Yes** | Supabase PostgreSQL connection string |');
349
+ }
350
+ else {
351
+ lines.push('| `DATABASE_URL` | **Yes** | PostgreSQL connection string |');
352
+ }
353
+ lines.push('| `JWT_SECRET` | **Yes** | Secret for signing JWT tokens (min 32 chars) |');
354
+ if (config.email === 'resend') {
355
+ lines.push('| `RESEND_API_KEY` | **Yes** | Resend API key |');
356
+ lines.push('| `EMAIL_FROM` | No | Sender address (default: `noreply@example.com`) |');
357
+ }
358
+ else if (config.email === 'nodemailer') {
359
+ lines.push('| `SMTP_HOST` | No | SMTP host (default: `localhost`) |');
360
+ lines.push('| `SMTP_PORT` | No | SMTP port (default: `1025`) |');
361
+ lines.push('| `SMTP_USER` | No | SMTP username |');
362
+ lines.push('| `SMTP_PASS` | No | SMTP password |');
363
+ lines.push('| `EMAIL_FROM` | No | Sender address (default: `noreply@example.com`) |');
364
+ }
365
+ if (config.auth === 'oauth+otp' || config.auth === 'all') {
366
+ lines.push('| `GOOGLE_CLIENT_ID` | **Yes** | Google OAuth client ID |');
367
+ lines.push('| `GOOGLE_CLIENT_SECRET` | **Yes** | Google OAuth client secret |');
368
+ lines.push('| `GOOGLE_REDIRECT_URI` | **Yes** | Google OAuth redirect URI |');
369
+ }
370
+ if (config.payments === 'stripe') {
371
+ lines.push('| `STRIPE_SECRET_KEY` | **Yes** | Stripe secret key |');
372
+ lines.push('| `STRIPE_WEBHOOK_SECRET` | **Yes** | Stripe webhook signing secret |');
373
+ }
374
+ else if (config.payments === 'revenuecat') {
375
+ lines.push('| `REVENUECAT_API_KEY` | **Yes** | RevenueCat API key |');
376
+ lines.push('| `REVENUECAT_WEBHOOK_SECRET` | **Yes** | RevenueCat webhook secret |');
377
+ }
378
+ else if (config.payments === 'paddle') {
379
+ lines.push('| `PADDLE_API_KEY` | **Yes** | Paddle API key |');
380
+ lines.push('| `PADDLE_WEBHOOK_SECRET` | **Yes** | Paddle webhook signing secret |');
381
+ lines.push('| `PADDLE_ENVIRONMENT` | No | `sandbox` or `production` (default: `sandbox`) |');
382
+ }
383
+ else if (config.payments === 'lemonsqueezy') {
384
+ lines.push('| `LEMONSQUEEZY_API_KEY` | **Yes** | Lemon Squeezy API key |');
385
+ lines.push('| `LEMONSQUEEZY_WEBHOOK_SECRET` | **Yes** | Lemon Squeezy webhook secret |');
386
+ }
387
+ if (config.storage === 's3') {
388
+ lines.push('| `S3_BUCKET` | **Yes** | S3 bucket name |');
389
+ lines.push('| `S3_REGION` | **Yes** | AWS region |');
390
+ lines.push('| `S3_ACCESS_KEY_ID` | **Yes** | AWS access key ID |');
391
+ lines.push('| `S3_SECRET_ACCESS_KEY` | **Yes** | AWS secret access key |');
392
+ }
393
+ else if (config.storage === 'r2') {
394
+ lines.push('| `R2_ACCOUNT_ID` | **Yes** | Cloudflare account ID |');
395
+ lines.push('| `R2_ACCESS_KEY_ID` | **Yes** | R2 access key ID |');
396
+ lines.push('| `R2_SECRET_ACCESS_KEY` | **Yes** | R2 secret access key |');
397
+ lines.push('| `R2_BUCKET` | **Yes** | R2 bucket name |');
398
+ }
399
+ else if (config.storage === 'local') {
400
+ lines.push('| `LOCAL_STORAGE_DIR` | No | Upload directory (default: `./uploads`) |');
401
+ }
402
+ if (config.cache === 'upstash') {
403
+ lines.push('| `UPSTASH_REDIS_URL` | **Yes** | Upstash Redis REST URL |');
404
+ lines.push('| `UPSTASH_REDIS_TOKEN` | **Yes** | Upstash Redis REST token |');
405
+ }
406
+ else if (config.cache === 'redis') {
407
+ lines.push('| `REDIS_URL` | **Yes** | Redis connection URL |');
408
+ }
409
+ if (config.sms === 'twilio') {
410
+ lines.push('| `TWILIO_ACCOUNT_SID` | **Yes** | Twilio account SID |');
411
+ lines.push('| `TWILIO_AUTH_TOKEN` | **Yes** | Twilio auth token |');
412
+ lines.push('| `TWILIO_FROM_NUMBER` | **Yes** | Twilio sender phone number (E.164) |');
413
+ }
414
+ else if (config.sms === 'sns') {
415
+ lines.push('| `AWS_REGION` | **Yes** | AWS region for SNS |');
416
+ lines.push('| `AWS_ACCESS_KEY_ID` | **Yes** | AWS access key ID |');
417
+ lines.push('| `AWS_SECRET_ACCESS_KEY` | **Yes** | AWS secret access key |');
418
+ }
419
+ if (config.push === 'fcm' || config.push === 'both') {
420
+ lines.push('| `FCM_PROJECT_ID` | **Yes** | Firebase project ID |');
421
+ lines.push('| `FCM_SERVICE_ACCOUNT_KEY` | **Yes** | Firebase service account JSON key (base64) |');
422
+ }
423
+ if (config.push === 'apns' || config.push === 'both') {
424
+ lines.push('| `APNS_KEY_ID` | **Yes** | APNs key ID |');
425
+ lines.push('| `APNS_TEAM_ID` | **Yes** | Apple Developer team ID |');
426
+ lines.push('| `APNS_PRIVATE_KEY` | **Yes** | APNs private key (.p8, base64) |');
427
+ lines.push('| `APNS_BUNDLE_ID` | **Yes** | App bundle ID |');
428
+ }
429
+ if (config.realtime === 'pusher') {
430
+ lines.push('| `PUSHER_APP_ID` | **Yes** | Pusher app ID |');
431
+ lines.push('| `PUSHER_KEY` | **Yes** | Pusher key |');
432
+ lines.push('| `PUSHER_SECRET` | **Yes** | Pusher secret |');
433
+ lines.push('| `PUSHER_CLUSTER` | **Yes** | Pusher cluster (e.g., `us2`) |');
434
+ }
435
+ else if (config.realtime === 'ably') {
436
+ lines.push('| `ABLY_API_KEY` | **Yes** | Ably API key |');
437
+ }
438
+ if (config.search === 'meilisearch') {
439
+ lines.push('| `MEILISEARCH_URL` | **Yes** | Meilisearch host URL |');
440
+ lines.push('| `MEILISEARCH_API_KEY` | **Yes** | Meilisearch API key |');
441
+ }
442
+ else if (config.search === 'typesense') {
443
+ lines.push('| `TYPESENSE_URL` | **Yes** | Typesense host URL |');
444
+ lines.push('| `TYPESENSE_API_KEY` | **Yes** | Typesense API key |');
445
+ }
446
+ if (config.queue === 'bullmq') {
447
+ lines.push('| `BULLMQ_REDIS_URL` | **Yes** | Redis URL for BullMQ |');
448
+ }
449
+ if (config.flags === 'launchdarkly') {
450
+ lines.push('| `LAUNCHDARKLY_SDK_KEY` | **Yes** | LaunchDarkly SDK key |');
451
+ }
452
+ if (config.tracing) {
453
+ lines.push('| `OTEL_EXPORTER_OTLP_ENDPOINT` | No | OpenTelemetry collector endpoint |');
454
+ lines.push('| `OTEL_SERVICE_NAME` | No | Service name for traces |');
455
+ }
456
+ if (config.authz === 'casbin') {
457
+ lines.push('| `CASBIN_MODEL_PATH` | **Yes** | Path to Casbin model file |');
458
+ lines.push('| `CASBIN_POLICY_PATH` | **Yes** | Path to Casbin policy file |');
459
+ }
460
+ if (config.dashboard) {
461
+ lines.push('| `DASHBOARD_SECRET` | No | Shared secret to protect the dashboard |');
462
+ }
463
+ lines.push('');
464
+ }
465
+ // Active Port Interfaces
466
+ lines.push('## Active Port Interfaces');
467
+ lines.push('');
468
+ lines.push('### DatabaseClient');
469
+ lines.push('```ts');
470
+ lines.push('db.query<T>(text: string, values?: unknown[]): Promise<QueryResult<T>>');
471
+ lines.push('db.transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>');
472
+ lines.push('db.healthCheck(): Promise<void>');
473
+ lines.push('db.close(): Promise<void>');
474
+ lines.push('```');
475
+ lines.push('');
476
+ lines.push('### Logger');
477
+ lines.push('```ts');
478
+ lines.push('logger.info(msg: string, data?: Record<string, unknown>): void');
479
+ lines.push('logger.warn/error/debug(msg: string, data?: Record<string, unknown>): void');
480
+ lines.push('logger.child(bindings: Record<string, unknown>): Logger');
481
+ lines.push('```');
482
+ lines.push('');
483
+ lines.push('### TokenSigner');
484
+ lines.push('```ts');
485
+ lines.push('tokens.sign(payload: TokenPayload, expiresIn: string): Promise<string>');
486
+ lines.push('tokens.verify(token: string): Promise<TokenPayload>');
487
+ lines.push('tokens.signPair(payload: TokenPayload, accessTTL: string, refreshTTL: string): Promise<TokenPair>');
488
+ lines.push('```');
489
+ lines.push('');
490
+ if (config.email) {
491
+ lines.push('### EmailTransport');
492
+ lines.push('```ts');
493
+ lines.push('transport.send(message: EmailMessage): Promise<{ messageId: string }>');
494
+ lines.push('// EmailMessage: { to, subject, html, text?, from?, replyTo?, attachments? }');
495
+ lines.push('```');
496
+ lines.push('');
497
+ }
498
+ if (config.payments) {
499
+ lines.push('### PaymentProvider');
500
+ lines.push('```ts');
501
+ lines.push('payments.customers.create(params): Promise<PaymentCustomer>');
502
+ lines.push('payments.customers.get(id): Promise<PaymentCustomer | null>');
503
+ lines.push('payments.customers.update(id, params): Promise<PaymentCustomer>');
504
+ lines.push('payments.customers.delete(id): Promise<void>');
505
+ lines.push('payments.subscriptions.create(params): Promise<PaymentSubscription>');
506
+ lines.push('payments.subscriptions.get(id): Promise<PaymentSubscription | null>');
507
+ lines.push('payments.subscriptions.cancel(id, params?): Promise<PaymentSubscription>');
508
+ lines.push('payments.subscriptions.listByCustomer(customerId): Promise<PaymentSubscription[]>');
509
+ lines.push('payments.invoices.get(id): Promise<PaymentInvoice | null>');
510
+ lines.push('payments.invoices.listByCustomer(customerId): Promise<PaymentInvoice[]>');
511
+ lines.push('payments.webhooks.verify(body, headers): WebhookEvent');
512
+ lines.push('```');
513
+ lines.push('');
514
+ }
515
+ if (config.storage) {
516
+ lines.push('### StorageProvider');
517
+ lines.push('```ts');
518
+ lines.push('storage.upload(params: { key, body, contentType }): Promise<StorageObject>');
519
+ lines.push('storage.download(key): Promise<{ body: ReadableStream, contentType? }>');
520
+ lines.push('storage.delete(key): Promise<void>');
521
+ lines.push('storage.getSignedUrl(key, expiresIn): Promise<string>');
522
+ lines.push('storage.exists(key): Promise<boolean>');
523
+ lines.push('```');
524
+ lines.push('');
525
+ }
526
+ if (config.cache) {
527
+ lines.push('### CacheProvider');
528
+ lines.push('```ts');
529
+ lines.push('cache.get<T>(key: string): Promise<T | null>');
530
+ lines.push('cache.set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>');
531
+ lines.push('cache.delete(key: string): Promise<void>');
532
+ lines.push('cache.flush(): Promise<void>');
533
+ lines.push('cache.increment(key: string, ttlSeconds?: number): Promise<number>');
534
+ lines.push('```');
535
+ lines.push('');
536
+ }
537
+ if (config.auth === 'password+otp' || config.auth === 'all') {
538
+ lines.push('### PasswordHasher');
539
+ lines.push('```ts');
540
+ lines.push('hasher.hash(password: string): Promise<string>');
541
+ lines.push('hasher.verify(password: string, hash: string): Promise<boolean>');
542
+ lines.push('```');
543
+ lines.push('');
544
+ }
545
+ if (config.auth === 'oauth+otp' || config.auth === 'all') {
546
+ lines.push('### OAuthProvider');
547
+ lines.push('```ts');
548
+ lines.push('oauth.getAuthUrl(params: { redirectUri, state?, scopes? }): string');
549
+ lines.push('oauth.handleCallback(params: { code, redirectUri }): Promise<OAuthProfile>');
550
+ lines.push('// OAuthProfile: { id, email, name?, avatarUrl?, raw }');
551
+ lines.push('```');
552
+ lines.push('');
553
+ }
554
+ if (config.sms) {
555
+ lines.push('### SmsTransport');
556
+ lines.push('```ts');
557
+ lines.push('sms.send(message: SmsMessage): Promise<{ messageId: string }>');
558
+ lines.push('// SmsMessage: { to: string, body: string, from?: string }');
559
+ lines.push('```');
560
+ lines.push('');
561
+ }
562
+ if (config.push) {
563
+ lines.push('### PushTransport');
564
+ lines.push('```ts');
565
+ lines.push('push.send(token: string, payload: PushPayload): Promise<PushResult>');
566
+ lines.push('push.sendMany(tokens: string[], payload: PushPayload): Promise<PushResult[]>');
567
+ lines.push('// PushPayload: { title, body, data?, badge?, sound?, imageUrl? }');
568
+ lines.push('```');
569
+ lines.push('');
570
+ }
571
+ if (config.realtime) {
572
+ lines.push('### RealtimeProvider');
573
+ lines.push('```ts');
574
+ lines.push('realtime.publish(channel: string, event: string, data: unknown): Promise<void>');
575
+ lines.push('realtime.getChannels(): Promise<RealtimeChannel[]>');
576
+ lines.push('realtime.getSubscriberCount(channel: string): Promise<number>');
577
+ lines.push('```');
578
+ lines.push('');
579
+ }
580
+ if (config.search) {
581
+ lines.push('### SearchProvider');
582
+ lines.push('```ts');
583
+ lines.push('search.index<T>(indexName: string, documents: SearchDocument<T>[]): Promise<IndexResult>');
584
+ lines.push('search.search<T>(indexName: string, query: string, options?: SearchOptions): Promise<SearchResult<T>>');
585
+ lines.push('search.delete(indexName: string, documentIds: string[]): Promise<void>');
586
+ lines.push('```');
587
+ lines.push('');
588
+ }
589
+ if (config.queue) {
590
+ lines.push('### QueueProvider');
591
+ lines.push('```ts');
592
+ lines.push('queue.enqueue<T>(queue: string, payload: T, options?: JobOptions): Promise<Job<T>>');
593
+ lines.push('queue.process<T>(queue: string, handler: JobHandler<T>, options?: ProcessorOptions): Promise<void>');
594
+ lines.push('queue.getJob<T>(jobId: string): Promise<Job<T> | null>');
595
+ lines.push('queue.cancel(jobId: string): Promise<void>');
596
+ lines.push('```');
597
+ lines.push('');
598
+ }
599
+ if (config.flags) {
600
+ lines.push('### FeatureFlagProvider');
601
+ lines.push('```ts');
602
+ lines.push('flags.isEnabled(flag: string, context?: FlagContext): Promise<boolean>');
603
+ lines.push('flags.getValue<T>(flag: string, defaultValue: T, context?: FlagContext): Promise<T>');
604
+ lines.push('flags.getAllFlags(context?: FlagContext): Promise<Record<string, unknown>>');
605
+ lines.push('```');
606
+ lines.push('');
607
+ }
608
+ if (config.tracing) {
609
+ lines.push('### Tracer');
610
+ lines.push('```ts');
611
+ lines.push('tracer.startSpan(name: string, options?: SpanOptions): Span');
612
+ lines.push('tracer.withSpan<T>(name: string, fn: (span: Span) => Promise<T>): Promise<T>');
613
+ lines.push('```');
614
+ lines.push('');
615
+ }
616
+ if (config.authz) {
617
+ lines.push('### AuthorizationProvider');
618
+ lines.push('```ts');
619
+ lines.push('authz.can(subject: string, action: string, resource: string, resourceId?: string): Promise<boolean>');
620
+ lines.push('authz.authorize(subject: string, action: string, resource: string, resourceId?: string): Promise<void>');
621
+ lines.push('authz.grant(subject: string, action: string, resource: string, resourceId?: string): Promise<void>');
622
+ lines.push('authz.revoke(subject: string, action: string, resource: string, resourceId?: string): Promise<void>');
623
+ lines.push('authz.listPermissions(subject: string): Promise<Permission[]>');
624
+ lines.push('```');
625
+ lines.push('');
626
+ }
627
+ if (config.audit) {
628
+ lines.push('### AuditLogger');
629
+ lines.push('```ts');
630
+ lines.push('audit.log(entry: AuditEntry): Promise<AuditRecord>');
631
+ lines.push('audit.query(filters: AuditQueryFilters): Promise<AuditRecord[]>');
632
+ lines.push('audit.count(filters: AuditQueryFilters): Promise<number>');
633
+ lines.push('```');
634
+ lines.push('');
635
+ }
636
+ // Recipe: Add a New Entity
637
+ lines.push('## Recipe: Add a New Entity');
638
+ lines.push('');
639
+ lines.push('1. Create `migrations/NNN_create_<table>.up.sql`:');
640
+ lines.push('```sql');
641
+ lines.push('CREATE TABLE <table_name> (');
642
+ lines.push(' id UUID PRIMARY KEY DEFAULT gen_random_uuid(),');
643
+ lines.push(' -- your columns here');
644
+ lines.push(' created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),');
645
+ lines.push(' updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()');
646
+ lines.push(');');
647
+ lines.push('```');
648
+ lines.push('');
649
+ lines.push('2. Create `migrations/NNN_create_<table>.down.sql`:');
650
+ lines.push('```sql');
651
+ lines.push('DROP TABLE IF EXISTS <table_name>;');
652
+ lines.push('```');
653
+ lines.push('');
654
+ lines.push('3. Define a TypeScript interface for the entity');
655
+ lines.push('');
656
+ lines.push('4. Create a repository:');
657
+ lines.push('```ts');
658
+ lines.push("const repo = createRepository<MyEntity>('table_name', { primaryKey: 'id', softDelete: true });");
659
+ lines.push('```');
660
+ lines.push('');
661
+ lines.push('5. All repo methods take `db: Queryable` as first arg (explicit dependency injection):');
662
+ lines.push('```ts');
663
+ lines.push('await repo.findById(db, id);');
664
+ lines.push('await repo.create(db, { name, email });');
665
+ lines.push('await repo.paginate(db, { page: 1, perPage: 20 });');
666
+ lines.push('// Inside a transaction:');
667
+ lines.push('await db.transaction(async (tx) => {');
668
+ lines.push(' await repo.create(tx, data);');
669
+ lines.push(' await otherRepo.update(tx, id, changes);');
670
+ lines.push('});');
671
+ lines.push('```');
672
+ lines.push('');
673
+ if (config.graphql) {
674
+ lines.push('6. Add a GraphQL type to `src/schema.ts` using `buildObjectType()`');
675
+ lines.push('');
676
+ lines.push('7. Create a dataloader for batched lookups: `createLoaderFromRepo(repo, db)`');
677
+ lines.push('');
678
+ }
679
+ // Error Types
680
+ lines.push('## Error Types');
681
+ lines.push('');
682
+ lines.push('| Error | Code | HTTP | When to throw |');
683
+ lines.push('|---|---|---|---|');
684
+ lines.push('| `NotFoundError(resource, id?)` | NOT_FOUND | 404 | Resource not found |');
685
+ lines.push('| `UnauthorizedError(msg?)` | UNAUTHORIZED | 401 | Missing/invalid auth |');
686
+ lines.push('| `ForbiddenError(msg?)` | FORBIDDEN | 403 | Insufficient permissions |');
687
+ lines.push('| `ValidationError(msg, details?)` | VALIDATION_ERROR | 400 | Bad input data |');
688
+ lines.push('| `ConflictError(msg, details?)` | CONFLICT | 409 | Duplicate resource |');
689
+ lines.push('| `RateLimitError(retryAfter?)` | RATE_LIMITED | 429 | Too many requests |');
690
+ lines.push('| `InternalError(msg?)` | INTERNAL_ERROR | 500 | Unexpected failure |');
691
+ lines.push('');
692
+ lines.push('All errors accept a `details` object. Include `fix` for LLM diagnostics:');
693
+ lines.push('```ts');
694
+ lines.push("throw new ValidationError('Bad input', { field: 'email', fix: 'Provide a valid email address' });");
695
+ lines.push('```');
696
+ lines.push('');
697
+ // Testing Recipe
698
+ if (config.tests) {
699
+ lines.push('## Testing');
700
+ lines.push('');
701
+ lines.push('Test setup is in `tests/setup.ts`. It creates in-memory adapters:');
702
+ lines.push('```ts');
703
+ lines.push("import { createTestApp } from './tests/setup.js';");
704
+ lines.push('');
705
+ lines.push('const { app, db, email } = await createTestApp();');
706
+ lines.push('// app is a real App instance with in-memory adapters');
707
+ lines.push('// db, email, tokens, logger are direct references for assertions');
708
+ lines.push('```');
709
+ lines.push('');
710
+ lines.push('Run tests: `npm test`');
711
+ lines.push('Watch mode: `npm run test:watch`');
712
+ lines.push('');
713
+ }
714
+ // Switching to Production (devMode only)
715
+ if (config.devMode) {
716
+ lines.push('## Switching to Production');
717
+ lines.push('');
718
+ lines.push('This project was scaffolded in **prototype mode**: all adapters come from `@objectifthunes/sandstone-sdk/dev` and require zero config. When you are ready to deploy for real, follow these steps:');
719
+ lines.push('');
720
+ lines.push('### 1. Swap imports in `src/infrastructure.ts`');
721
+ lines.push('');
722
+ lines.push('Replace every `@objectifthunes/sandstone-sdk/dev` import with the real adapter subpath:');
723
+ lines.push('');
724
+ lines.push('| `./dev` factory | Real import path | Config needed |');
725
+ lines.push('|-----------------|------------------|---------------|');
726
+ lines.push('| `createPgClient()` | `@objectifthunes/sandstone-sdk/pg` | `{ connectionString: process.env.DATABASE_URL! }` |');
727
+ lines.push('| `createPinoLogger()` | `@objectifthunes/sandstone-sdk/pino` | `{ level: process.env.LOG_LEVEL ?? \'info\' }` |');
728
+ lines.push('| `createJoseSigner()` | `@objectifthunes/sandstone-sdk/jose` | `{ secret: process.env.JWT_SECRET! }` |');
729
+ lines.push('| `createNodemailerTransport()` | `@objectifthunes/sandstone-sdk/nodemailer` | SMTP options or swap for `resend` |');
730
+ lines.push('');
731
+ lines.push('### 2. Add environment variables');
732
+ lines.push('');
733
+ lines.push('Copy `.env.example` and add the required values:');
734
+ lines.push('');
735
+ lines.push('```sh');
736
+ lines.push('DATABASE_URL=postgresql://postgres:postgres@localhost:5432/' + config.name);
737
+ lines.push('JWT_SECRET=change-me-to-a-random-secret-at-least-32-chars');
738
+ lines.push('# plus any email / payment / storage keys');
739
+ lines.push('```');
740
+ lines.push('');
741
+ lines.push('### 3. Add docker-compose for Postgres');
742
+ lines.push('');
743
+ lines.push('Create a `docker-compose.yml` for local development:');
744
+ lines.push('');
745
+ lines.push('```yaml');
746
+ lines.push('services:');
747
+ lines.push(' postgres:');
748
+ lines.push(' image: postgres:16-alpine');
749
+ lines.push(' ports:');
750
+ lines.push(" - '5432:5432'");
751
+ lines.push(' environment:');
752
+ lines.push(` POSTGRES_DB: ${config.name}`);
753
+ lines.push(' POSTGRES_USER: postgres');
754
+ lines.push(' POSTGRES_PASSWORD: postgres');
755
+ lines.push(' volumes:');
756
+ lines.push(' - pgdata:/var/lib/postgresql/data');
757
+ lines.push('volumes:');
758
+ lines.push(' pgdata:');
759
+ lines.push('```');
760
+ lines.push('');
761
+ lines.push('Then run `docker compose up -d` before `npm run dev`.');
762
+ lines.push('');
763
+ }
764
+ // Custom Adapters
765
+ lines.push('## Custom Adapters');
766
+ lines.push('');
767
+ lines.push('The SDK defines port interfaces (contracts) for every infrastructure concern. You can build a custom adapter for any port by implementing the interface and swapping it in `src/infrastructure.ts`.');
768
+ lines.push('');
769
+ lines.push('### Port Interfaces');
770
+ lines.push('');
771
+ lines.push('| Port | Interface | Methods |');
772
+ lines.push('|------|-----------|---------|');
773
+ lines.push('| Database | `DatabaseClient` | `query(text, values?)`, `transaction(fn)`, `healthCheck()`, `close()`, `getPoolStats?()` |');
774
+ lines.push('| Logging | `Logger` | `info(msg, data?)`, `warn(msg, data?)`, `error(msg, data?)`, `debug(msg, data?)`, `child(bindings)` |');
775
+ lines.push('| Tokens | `TokenSigner` | `sign(payload, expiresIn)`, `verify(token)`, `signPair(payload, accessTTL, refreshTTL)` |');
776
+ lines.push('| Email | `EmailTransport` | `send(message)` |');
777
+ lines.push('| Payments | `PaymentProvider` | `customers.create/get/update/delete`, `subscriptions.create/get/cancel/listByCustomer`, `invoices.get/listByCustomer`, `webhooks.verify` |');
778
+ lines.push('| Storage | `StorageProvider` | `upload(params)`, `download(key)`, `delete(key)`, `getSignedUrl(key, expiresIn)`, `exists(key)` |');
779
+ lines.push('| Cache | `CacheProvider` | `get(key)`, `set(key, value, ttl?)`, `delete(key)`, `flush()`, `increment(key, ttl?)` |');
780
+ lines.push('| Passwords | `PasswordHasher` | `hash(password)`, `verify(password, hash)` |');
781
+ lines.push('| OAuth | `OAuthProvider` | `getAuthUrl(params)`, `handleCallback(params)` |');
782
+ lines.push('| SMS | `SmsTransport` | `send(message)` |');
783
+ lines.push('| Push | `PushTransport` | `send(token, payload)`, `sendMany(tokens, payload)` |');
784
+ lines.push('| Realtime | `RealtimeProvider` | `publish(channel, event, data)`, `getChannels()`, `getSubscriberCount(channel)` |');
785
+ lines.push('| Search | `SearchProvider` | `index(indexName, documents)`, `search(indexName, query, options?)`, `delete(indexName, ids)` |');
786
+ lines.push('| Queue | `QueueProvider` | `enqueue(queue, payload, options?)`, `process(queue, handler)`, `getJob(id)`, `cancel(id)` |');
787
+ lines.push('| Feature Flags | `FeatureFlagProvider` | `isEnabled(flag, context?)`, `getValue(flag, default, context?)`, `getAllFlags(context?)` |');
788
+ lines.push('| Tracing | `Tracer` | `startSpan(name, options?)`, `withSpan(name, fn)` |');
789
+ lines.push('| Authorization | `AuthorizationProvider` | `can(subject, action, resource)`, `authorize(...)`, `grant(...)`, `revoke(...)`, `listPermissions(subject)` |');
790
+ lines.push('| Audit | `AuditLogger` | `log(entry)`, `query(filters)`, `count(filters)` |');
791
+ lines.push('');
792
+ lines.push('### The Pattern');
793
+ lines.push('');
794
+ lines.push('1. Import the port type: `import type { EmailTransport } from \'@objectifthunes/sandstone-sdk\'`');
795
+ lines.push('2. Write a factory function that returns an object implementing the interface');
796
+ lines.push('3. Swap it into `src/infrastructure.ts`');
797
+ lines.push('');
798
+ lines.push('### Example: Custom Email Adapter (SendGrid)');
799
+ lines.push('');
800
+ lines.push('```ts');
801
+ lines.push('// src/adapters/sendgrid.ts');
802
+ lines.push("import type { EmailTransport, EmailMessage } from '@objectifthunes/sandstone-sdk';");
803
+ lines.push('');
804
+ lines.push('export function createSendGridTransport(apiKey: string): EmailTransport {');
805
+ lines.push(' return {');
806
+ lines.push(' async send(message: EmailMessage) {');
807
+ lines.push(" const response = await fetch('https://api.sendgrid.com/v3/mail/send', {");
808
+ lines.push(" method: 'POST',");
809
+ lines.push(' headers: {');
810
+ lines.push(' Authorization: `Bearer ${apiKey}`,');
811
+ lines.push(" 'Content-Type': 'application/json',");
812
+ lines.push(' },');
813
+ lines.push(' body: JSON.stringify({');
814
+ lines.push(' personalizations: [{');
815
+ lines.push(" to: (Array.isArray(message.to) ? message.to : [message.to]).map(e => ({ email: e })),");
816
+ lines.push(' }],');
817
+ lines.push(" from: { email: message.from ?? 'noreply@example.com' },");
818
+ lines.push(' subject: message.subject,');
819
+ lines.push(" content: [{ type: 'text/html', value: message.html }],");
820
+ lines.push(' }),');
821
+ lines.push(' });');
822
+ lines.push('');
823
+ lines.push(' if (!response.ok) {');
824
+ lines.push(' throw new Error(`SendGrid error: ${response.status}`);');
825
+ lines.push(' }');
826
+ lines.push('');
827
+ lines.push(" return { messageId: response.headers.get('x-message-id') ?? crypto.randomUUID() };");
828
+ lines.push(' },');
829
+ lines.push(' };');
830
+ lines.push('}');
831
+ lines.push('```');
832
+ lines.push('');
833
+ lines.push('Then in `src/infrastructure.ts`:');
834
+ lines.push('');
835
+ lines.push('```ts');
836
+ lines.push("import { createSendGridTransport } from './adapters/sendgrid.js';");
837
+ lines.push('');
838
+ lines.push('const app = await createApp({');
839
+ lines.push(' db,');
840
+ lines.push(' logger,');
841
+ lines.push(' tokens,');
842
+ lines.push(' email: {');
843
+ lines.push(' transport: createSendGridTransport(process.env.SENDGRID_API_KEY!),');
844
+ lines.push(" defaultFrom: 'noreply@myapp.com',");
845
+ lines.push(' },');
846
+ lines.push('});');
847
+ lines.push('```');
848
+ lines.push('');
849
+ lines.push('Full guide with every port interface: https://maxoujs.github.io/sandstone-sdk/guides/custom-adapters/');
850
+ lines.push('');
851
+ return lines.join('\n');
852
+ }
853
+ //# sourceMappingURL=claude-md.js.map