@avelonjs/cli 0.1.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.
@@ -0,0 +1,524 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import { snake, studly, writeln, type ReeveIO } from './io'
5
+
6
+ /** One contract `reeve make:driver` can scaffold. */
7
+ export interface DriverContractSpec {
8
+ /** Conformance suite export from `@avelonjs/conformance/suites`. */
9
+ suite: string
10
+ /** TypeScript type imported from `@avelonjs/core`. */
11
+ driverType: string
12
+ /** Extra type imports from `@avelonjs/core`. */
13
+ extraTypes: readonly string[]
14
+ /** Literal `capabilities` object. */
15
+ capabilities: string
16
+ /** Class method implementations. */
17
+ methods: string
18
+ }
19
+
20
+ const CONTRACTS: Record<string, DriverContractSpec> = {
21
+ database: {
22
+ suite: 'databaseSuite',
23
+ driverType: 'DatabaseDriver',
24
+ extraTypes: ['QueryIR', 'QueryResult', 'MigrationPlan', 'MigrationStatus'],
25
+ capabilities: `{
26
+ transactions: false,
27
+ rowSecurity: false,
28
+ maxRelationDepth: 1,
29
+ fullTextSearch: false,
30
+ upsert: false,
31
+ returning: false,
32
+ windowFunctions: false,
33
+ jsonOperators: false,
34
+ }`,
35
+ methods: ` async execute<TRow = Record<string, unknown>>(_query: QueryIR): Promise<QueryResult<TRow>> {
36
+ unimplemented('execute')
37
+ }
38
+
39
+ async rpc<TResult = unknown>(
40
+ _routine: string,
41
+ _args: Readonly<Record<string, unknown>>,
42
+ ): Promise<TResult> {
43
+ unimplemented('rpc')
44
+ }
45
+
46
+ async plan(): Promise<MigrationPlan> {
47
+ unimplemented('plan')
48
+ }
49
+
50
+ async apply(): Promise<readonly MigrationStatus[]> {
51
+ unimplemented('apply')
52
+ }
53
+
54
+ async rollback(_steps?: number): Promise<readonly MigrationStatus[]> {
55
+ unimplemented('rollback')
56
+ }
57
+
58
+ async status(): Promise<readonly MigrationStatus[]> {
59
+ unimplemented('status')
60
+ }`,
61
+ },
62
+ identity: {
63
+ suite: 'identitySuite',
64
+ driverType: 'IdentityDriver',
65
+ extraTypes: [],
66
+ capabilities: `{
67
+ passwords: false,
68
+ magicLinks: false,
69
+ oauth: false,
70
+ organizations: false,
71
+ mfa: [] as const,
72
+ }`,
73
+ methods: ` async user(): Promise<{ readonly id: string } | null> {
74
+ unimplemented('user')
75
+ }
76
+
77
+ async session(): Promise<{ readonly id: string } | null> {
78
+ unimplemented('session')
79
+ }
80
+
81
+ async signOut(): Promise<void> {
82
+ unimplemented('signOut')
83
+ }`,
84
+ },
85
+ social: {
86
+ suite: 'socialSuite',
87
+ driverType: 'SocialDriver',
88
+ extraTypes: ['SocialIdentity'],
89
+ capabilities: `{
90
+ providers: [] as const,
91
+ }`,
92
+ methods: ` async redirect(_provider: string, _callbackUrl: string, _state?: string): Promise<string> {
93
+ unimplemented('redirect')
94
+ }
95
+
96
+ async callback(
97
+ _provider: string,
98
+ _params: Readonly<Record<string, string>>,
99
+ _callbackUrl: string,
100
+ ): Promise<SocialIdentity> {
101
+ unimplemented('callback')
102
+ }`,
103
+ },
104
+ tokens: {
105
+ suite: 'tokensSuite',
106
+ driverType: 'TokenDriver',
107
+ extraTypes: ['IssuedToken', 'TokenIssueOptions', 'TokenRecord'],
108
+ capabilities: `{
109
+ abilities: false,
110
+ expiration: false,
111
+ }`,
112
+ methods: ` async verify(_plainText: string): Promise<TokenRecord> {
113
+ unimplemented('verify')
114
+ }
115
+
116
+ async issue(_name: string, _options?: TokenIssueOptions): Promise<IssuedToken> {
117
+ unimplemented('issue')
118
+ }
119
+
120
+ async list(): Promise<readonly TokenRecord[]> {
121
+ unimplemented('list')
122
+ }
123
+
124
+ async revoke(_id: string): Promise<void> {
125
+ unimplemented('revoke')
126
+ }`,
127
+ },
128
+ storage: {
129
+ suite: 'storageSuite',
130
+ driverType: 'StorageDriver',
131
+ extraTypes: ['StorageObject'],
132
+ capabilities: `{
133
+ signedUrls: false,
134
+ transforms: [] as const,
135
+ }`,
136
+ methods: ` async put(
137
+ _path: string,
138
+ _contents: Uint8Array | AsyncIterable<Uint8Array>,
139
+ _options?: { readonly contentType?: string },
140
+ ): Promise<StorageObject> {
141
+ unimplemented('put')
142
+ }
143
+
144
+ async get(_path: string): Promise<Uint8Array> {
145
+ unimplemented('get')
146
+ }
147
+
148
+ async delete(_path: string): Promise<void> {
149
+ unimplemented('delete')
150
+ }
151
+
152
+ async exists(_path: string): Promise<boolean> {
153
+ unimplemented('exists')
154
+ }`,
155
+ },
156
+ queue: {
157
+ suite: 'queueSuite',
158
+ driverType: 'QueueDriver',
159
+ extraTypes: ['QueueJob', 'QueueReceipt'],
160
+ capabilities: `{
161
+ delayed: false,
162
+ retries: false,
163
+ deadLetter: false,
164
+ }`,
165
+ methods: ` async enqueue<TPayload>(_job: QueueJob<TPayload>): Promise<string> {
166
+ unimplemented('enqueue')
167
+ }
168
+
169
+ async drain(
170
+ _handler: (receipt: QueueReceipt) => Promise<void>,
171
+ _options?: { readonly queue?: string; readonly limit?: number },
172
+ ): Promise<number> {
173
+ unimplemented('drain')
174
+ }`,
175
+ },
176
+ mail: {
177
+ suite: 'mailSuite',
178
+ driverType: 'MailDriver',
179
+ extraTypes: ['MailMessage', 'MailReceipt'],
180
+ capabilities: `{
181
+ templates: false,
182
+ }`,
183
+ methods: ` async send(_message: MailMessage): Promise<MailReceipt> {
184
+ unimplemented('send')
185
+ }`,
186
+ },
187
+ cache: {
188
+ suite: 'cacheSuite',
189
+ driverType: 'CacheDriver',
190
+ extraTypes: [],
191
+ capabilities: `{
192
+ tags: false,
193
+ locks: false,
194
+ }`,
195
+ methods: ` async get<TValue>(_key: string): Promise<TValue | null> {
196
+ unimplemented('get')
197
+ }
198
+
199
+ async put<TValue>(_key: string, _value: TValue, _ttlSeconds?: number): Promise<void> {
200
+ unimplemented('put')
201
+ }
202
+
203
+ async forget(_key: string): Promise<void> {
204
+ unimplemented('forget')
205
+ }
206
+
207
+ async flush(): Promise<void> {
208
+ unimplemented('flush')
209
+ }`,
210
+ },
211
+ realtime: {
212
+ suite: 'realtimeSuite',
213
+ driverType: 'RealtimeDriver',
214
+ extraTypes: ['RealtimeMessage', 'RealtimeSubscription'],
215
+ capabilities: `{
216
+ presence: false,
217
+ broadcast: false,
218
+ }`,
219
+ methods: ` async subscribe<TPayload = unknown>(
220
+ _channel: string,
221
+ _handler: (message: RealtimeMessage<TPayload>) => void | Promise<void>,
222
+ ): Promise<RealtimeSubscription> {
223
+ unimplemented('subscribe')
224
+ }`,
225
+ },
226
+ search: {
227
+ suite: 'searchSuite',
228
+ driverType: 'SearchDriver',
229
+ extraTypes: ['SearchDocument', 'SearchOptions', 'SearchResult'],
230
+ capabilities: `{
231
+ facets: false,
232
+ }`,
233
+ methods: ` async index(_name: string, _documents: readonly SearchDocument[]): Promise<void> {
234
+ unimplemented('index')
235
+ }
236
+
237
+ async remove(_name: string, _ids: readonly string[]): Promise<void> {
238
+ unimplemented('remove')
239
+ }
240
+
241
+ async query<TDocument extends SearchDocument = SearchDocument>(
242
+ _name: string,
243
+ _query: string,
244
+ _options?: SearchOptions,
245
+ ): Promise<SearchResult<TDocument>> {
246
+ unimplemented('query')
247
+ }`,
248
+ },
249
+ payments: {
250
+ suite: 'paymentsSuite',
251
+ driverType: 'PaymentDriver',
252
+ extraTypes: ['PaymentCustomer'],
253
+ capabilities: `{
254
+ subscriptions: false,
255
+ checkout: false,
256
+ webhooks: false,
257
+ }`,
258
+ methods: ` async createCustomer(_input: {
259
+ readonly email?: string
260
+ readonly metadata?: Readonly<Record<string, string>>
261
+ }): Promise<PaymentCustomer> {
262
+ unimplemented('createCustomer')
263
+ }
264
+
265
+ async customer(_id: string): Promise<PaymentCustomer> {
266
+ unimplemented('customer')
267
+ }`,
268
+ },
269
+ notifications: {
270
+ suite: 'notificationsSuite',
271
+ driverType: 'NotificationDriver',
272
+ extraTypes: ['NotificationChannel', 'NotificationMessage', 'NotificationReceipt'],
273
+ capabilities: `{
274
+ channels: [] as const,
275
+ }`,
276
+ methods: ` async send<TData>(
277
+ _channel: NotificationChannel,
278
+ _message: NotificationMessage<TData>,
279
+ ): Promise<NotificationReceipt> {
280
+ unimplemented('send')
281
+ }`,
282
+ },
283
+ flags: {
284
+ suite: 'flagsSuite',
285
+ driverType: 'FlagDriver',
286
+ extraTypes: ['FlagContext'],
287
+ capabilities: `{
288
+ targeting: false,
289
+ }`,
290
+ methods: ` async evaluate<TValue>(
291
+ _key: string,
292
+ _defaultValue: TValue,
293
+ _context?: FlagContext,
294
+ ): Promise<TValue> {
295
+ unimplemented('evaluate')
296
+ }`,
297
+ },
298
+ logs: {
299
+ suite: 'logsSuite',
300
+ driverType: 'LogDriver',
301
+ extraTypes: ['LogRecord'],
302
+ capabilities: `{
303
+ traces: false,
304
+ }`,
305
+ methods: ` async write(_record: LogRecord): Promise<void> {
306
+ unimplemented('write')
307
+ }`,
308
+ },
309
+ ratelimit: {
310
+ suite: 'ratelimitSuite',
311
+ driverType: 'RateLimitDriver',
312
+ extraTypes: ['RateLimitDecision', 'RateLimitPolicy'],
313
+ capabilities: `{
314
+ algorithms: [] as const,
315
+ }`,
316
+ methods: ` async consume(
317
+ _key: string,
318
+ _policy: RateLimitPolicy,
319
+ _cost?: number,
320
+ ): Promise<RateLimitDecision> {
321
+ unimplemented('consume')
322
+ }
323
+
324
+ async reset(_key: string): Promise<void> {
325
+ unimplemented('reset')
326
+ }`,
327
+ },
328
+ ai: {
329
+ suite: 'aiSuite',
330
+ driverType: 'AiDriver',
331
+ extraTypes: [],
332
+ capabilities: `{
333
+ modes: [] as const,
334
+ streaming: false,
335
+ }`,
336
+ methods: '',
337
+ },
338
+ }
339
+
340
+ /** Contract names `reeve make:driver` accepts. */
341
+ export const DRIVER_CONTRACTS = Object.keys(CONTRACTS).sort()
342
+
343
+ function unimplementedHelper(): string {
344
+ return `/** Throws until you implement the method. Never throw a NotSupportedError; omit the method and declare the capability false instead. */
345
+ function unimplemented(method: string): never {
346
+ throw new Error(\`\${method} is unimplemented\`)
347
+ }
348
+ `
349
+ }
350
+
351
+ function driverSource(contract: string, slug: string, spec: DriverContractSpec): string {
352
+ const className = `${studly(slug)}${studly(contract)}`
353
+ const types = [spec.driverType, ...spec.extraTypes]
354
+ const typeImport = types.join(', ')
355
+ const methods = spec.methods.length > 0 ? `\n${spec.methods}\n` : '\n'
356
+ return `import type { ${typeImport} } from '@avelonjs/core'
357
+
358
+ const capabilities = ${spec.capabilities} as const
359
+
360
+ ${unimplementedHelper()}
361
+ /** ${studly(slug)} ${contract} driver. Capabilities start false; flip a key only when the method exists and conformance is green. */
362
+ export class ${className} implements ${spec.driverType}<typeof capabilities, null> {
363
+ readonly name = '${snake(slug).replace(/_/g, '-')}'
364
+ readonly instance: string
365
+ readonly capabilities = capabilities
366
+
367
+ constructor(instance = 'default') {
368
+ this.instance = instance
369
+ }
370
+
371
+ raw(): null {
372
+ return null
373
+ }
374
+ ${methods}}
375
+
376
+ /** Constructs a ${contract} driver named ${slug}. */
377
+ export function create${className}(instance = 'default'): ${className} {
378
+ return new ${className}(instance)
379
+ }
380
+ `
381
+ }
382
+
383
+ function indexSource(contract: string, slug: string): string {
384
+ const className = `${studly(slug)}${studly(contract)}`
385
+ return `export { create${className}, ${className} } from './driver'
386
+ `
387
+ }
388
+
389
+ function testSource(contract: string, slug: string, spec: DriverContractSpec): string {
390
+ const className = `${studly(slug)}${studly(contract)}`
391
+ const factory = `create${className}`
392
+ return `import { ${spec.suite} } from '@avelonjs/conformance/suites'
393
+ import { ${factory} } from '../src/index'
394
+
395
+ ${spec.suite}({
396
+ name: '${snake(slug).replace(/_/g, '-')} ${contract}',
397
+ create: () => ${factory}(),
398
+ })
399
+ `
400
+ }
401
+
402
+ function packageJson(slug: string): string {
403
+ const name = snake(slug).replace(/_/g, '-')
404
+ return `${JSON.stringify(
405
+ {
406
+ name: `@avelonjs/${name}`,
407
+ version: '0.0.0',
408
+ private: true,
409
+ type: 'module',
410
+ exports: { '.': './src/index.ts' },
411
+ scripts: { test: 'bun test', typecheck: 'tsc --noEmit' },
412
+ dependencies: { '@avelonjs/core': 'workspace:*' },
413
+ devDependencies: {
414
+ '@avelonjs/conformance': 'workspace:*',
415
+ '@types/bun': '1.3.14',
416
+ typescript: '5.9.3',
417
+ },
418
+ },
419
+ null,
420
+ 2,
421
+ )}\n`
422
+ }
423
+
424
+ function tsconfig(): string {
425
+ return `${JSON.stringify(
426
+ {
427
+ compilerOptions: {
428
+ target: 'ES2022',
429
+ module: 'Preserve',
430
+ moduleResolution: 'bundler',
431
+ lib: ['ES2023'],
432
+ strict: true,
433
+ noUncheckedIndexedAccess: true,
434
+ verbatimModuleSyntax: true,
435
+ isolatedModules: true,
436
+ skipLibCheck: true,
437
+ noEmit: true,
438
+ types: ['bun'],
439
+ },
440
+ include: ['src', 'tests'],
441
+ },
442
+ null,
443
+ 2,
444
+ )}\n`
445
+ }
446
+
447
+ async function writeText(io: ReeveIO, relative: string, contents: string): Promise<void> {
448
+ const file = join(io.cwd, relative)
449
+ await mkdir(dirname(file), { recursive: true })
450
+ await writeFile(file, contents)
451
+ writeln(io, relative)
452
+ }
453
+
454
+ function readmeSource(contract: string, slug: string, spec: DriverContractSpec): string {
455
+ const className = `${studly(slug)}${studly(contract)}`
456
+ const factory = `create${className}`
457
+ const pkg = snake(slug).replace(/_/g, '-')
458
+ return `# @avelonjs/${pkg}
459
+
460
+ ${className} is a ${contract} driver. You certify it by running the shared \`${spec.suite}\` from \`@avelonjs/conformance\`. Reach for this package when application code needs ${contract} without importing a vendor SDK.
461
+
462
+ ## Installation
463
+
464
+ \`\`\`sh
465
+ bun add @avelonjs/${pkg}
466
+ \`\`\`
467
+
468
+ Wire it only in \`avelon.config.ts\`. Application folders never import a vendor client.
469
+
470
+ ## Basic Usage
471
+
472
+ \`\`\`ts
473
+ import { ${factory} } from '@avelonjs/${pkg}'
474
+
475
+ export function makeDriver() {
476
+ return ${factory}('default')
477
+ }
478
+ \`\`\`
479
+
480
+ ## Capabilities
481
+
482
+ Declare capabilities as a \`const\` object. Start every key \`false\` or empty. Flip a key to \`true\` only when the method exists and \`${spec.suite}\` is green. Implementing a method while leaving the capability \`false\` is a certification failure: someone will call it, and it will break when the driver is swapped. Never throw a \`NotSupportedError\`; omit the method instead.
483
+
484
+ ## Method Reference
485
+
486
+ | Method / export | Signature | Description |
487
+ |---|---|---|
488
+ | \`${factory}\` | \`(instance?: string) => ${className}\` | Constructs the ${contract} driver. |
489
+ | \`${className}\` | \`class ${className}\` | ${studly(slug)} ${contract} implementation. Capabilities start unavailable. |
490
+
491
+ ## Testing
492
+
493
+ Run the shared suite. It is written from the contract, not from this package.
494
+
495
+ \`\`\`ts
496
+ import { ${spec.suite} } from '@avelonjs/conformance/suites'
497
+ import { ${factory} } from '@avelonjs/${pkg}'
498
+
499
+ ${spec.suite}({
500
+ name: '${pkg} ${contract}',
501
+ create: () => ${factory}(),
502
+ })
503
+ \`\`\`
504
+
505
+ \`\`\`sh
506
+ bun test
507
+ bun run typecheck
508
+ reeve docs:check
509
+ \`\`\`
510
+ `
511
+ }
512
+ export async function makeDriver(io: ReeveIO, contract: string, slug: string): Promise<void> {
513
+ const spec = CONTRACTS[contract]
514
+ if (spec === undefined) {
515
+ throw new Error(`Unknown contract '${contract}'. Contracts: ${DRIVER_CONTRACTS.join(', ')}`)
516
+ }
517
+ const dir = `packages/${snake(slug)}`
518
+ await writeText(io, `${dir}/README.md`, readmeSource(contract, slug, spec))
519
+ await writeText(io, `${dir}/package.json`, packageJson(slug))
520
+ await writeText(io, `${dir}/tsconfig.json`, tsconfig())
521
+ await writeText(io, `${dir}/src/driver.ts`, driverSource(contract, slug, spec))
522
+ await writeText(io, `${dir}/src/index.ts`, indexSource(contract, slug))
523
+ await writeText(io, `${dir}/tests/${contract}.test.ts`, testSource(contract, slug, spec))
524
+ }