@adhd/apigen-core 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,433 @@
1
+ import { Operation, JSONSchema } from './descriptor';
2
+
3
+ /**
4
+ * The four canonical transport carriers apigen knows about (SPEC §5/§7/§9.1).
5
+ *
6
+ * A `MountedOperation` may opt in to a subset — `transports` omitted → all.
7
+ */
8
+ export type Transport = 'http' | 'grpc' | 'mcp' | 'cli';
9
+ /**
10
+ * A type-keyed, mutable-during-compose map threaded through every Layer and
11
+ * into the dispatch function as `ctx` (SPEC §8.1).
12
+ *
13
+ * Layers insert typed values with a class or symbol key and downstream layers /
14
+ * the function implementation read them back — the same mental model as
15
+ * `http::Extensions` (Rust) and `AsyncLocalStorage` (Node).
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * // insert (in a Layer):
20
+ * call.ctx.set(Logger, new Logger({ level: 'info' }))
21
+ * // read (in a Layer or dispatch):
22
+ * const log = call.ctx.get(Logger)
23
+ * ```
24
+ */
25
+ export interface Extensions {
26
+ /**
27
+ * Retrieve the value stored under the given class constructor or symbol key.
28
+ * Returns `undefined` when the key has not been set.
29
+ */
30
+ get<T>(key: abstract new (...args: never[]) => T): T | undefined;
31
+ /**
32
+ * Store a value under the given class constructor or symbol key.
33
+ * Overwrites any existing value for that key.
34
+ */
35
+ set<T>(key: abstract new (...args: never[]) => T, value: T): void;
36
+ }
37
+ /**
38
+ * The inbound call descriptor passed to every layer and ultimately to dispatch
39
+ * (SPEC §7.1 / §8.1).
40
+ *
41
+ * `data` contains the bare domain params (envelope dissolved); `envelope`
42
+ * contains transport-native side-channel fields (session, auth tokens, …).
43
+ * `raw` is an escape hatch for transport-specific adapters that need access to
44
+ * the native request object — ordinary plugins should never need it.
45
+ */
46
+ export interface Call {
47
+ /** The operation being invoked (from the merged canonical descriptor). */
48
+ operation: Operation;
49
+ /** Bare domain params (the `data`-wrapper is dissolved; ctx excluded). */
50
+ data: Record<string, unknown>;
51
+ /**
52
+ * Side-channel metadata from the transport-native carrier, keyed as
53
+ * `x-<pluginId>-<field>` (SPEC §9 / §9.1).
54
+ */
55
+ envelope: Record<string, unknown>;
56
+ /**
57
+ * Typed request-extensions map — threaded `mw → mw → fn` (SPEC §8.1).
58
+ * Layers insert context values; downstream layers and the domain function
59
+ * read them back.
60
+ */
61
+ ctx: Extensions;
62
+ /** Which transport delivered this call. */
63
+ transport: Transport;
64
+ /**
65
+ * Cancellation signal — wired to the transport's native cancellation
66
+ * mechanism (HTTP abort, gRPC cancel, MCP cancel, Ctrl-C for CLI).
67
+ * Layers must propagate it to any async work they initiate (SPEC §11).
68
+ */
69
+ signal: AbortSignal;
70
+ /**
71
+ * Transport-native request object — escape hatch for adapters that need
72
+ * raw access to e.g. a Fastify `Request` or an MCP `CallToolRequest`.
73
+ * Ordinary plugins must NOT depend on this; it degrades portability.
74
+ */
75
+ raw?: unknown;
76
+ }
77
+ /**
78
+ * A single chunk emitted by a streaming operation (SPEC §11).
79
+ *
80
+ * The type is intentionally open (`unknown`) because the per-chunk element
81
+ * type is described by `operation.output` (JSON Schema) — static typing is
82
+ * achieved per-plugin via generics when the element type is known.
83
+ */
84
+ export type Chunk = unknown;
85
+ /**
86
+ * The non-streaming result of a Layer invocation — the value that flows
87
+ * back out to the transport adapter.
88
+ */
89
+ export type Result = unknown;
90
+ /**
91
+ * The continuation function passed to a Layer.
92
+ *
93
+ * Calling `next()` invokes the **remaining** layers and ultimately `dispatch`.
94
+ * A Layer may call it at most once per request. Not calling it short-circuits
95
+ * all downstream layers and dispatch (SPEC §8.1 rule 1).
96
+ *
97
+ * The return type is a union to support both unary and streaming operations
98
+ * from a single `LayerCapability.layer` signature (SPEC §11).
99
+ */
100
+ export type Next = () => Promise<Result> | AsyncIterable<Chunk>;
101
+ /**
102
+ * A single emitted file produced by {@link TargetCapability.generate}.
103
+ *
104
+ * `content` is always a UTF-8 string — plugins emit source code, config,
105
+ * or structured text in any language (TS, Python, proto, YAML, …).
106
+ * Nothing in core restricts the language (SPEC [inv:language-agnostic-output]).
107
+ */
108
+ export interface File {
109
+ /** Relative or absolute path where the file should be written. */
110
+ path: string;
111
+ /** UTF-8 string content to write. */
112
+ content: string;
113
+ }
114
+ /**
115
+ * Opaque handle returned by {@link TargetCapability.serve}.
116
+ *
117
+ * The minimal contract is `close()` for graceful shutdown. Plugins may
118
+ * extend this with transport-specific members (e.g. `port`, `url`).
119
+ */
120
+ export interface Server {
121
+ /** Gracefully shut down the server and release all resources. */
122
+ close(): Promise<void>;
123
+ }
124
+ /**
125
+ * The runtime harness injected into {@link TargetCapability.serve}.
126
+ *
127
+ * Provides `invoke` — the composed Layer stack that wraps `dispatch`.
128
+ * Transports call `harness.invoke(op, partialCall)` once per inbound request;
129
+ * the harness threads the active plugins' layers around the operation and
130
+ * returns/streams the result (SPEC §8).
131
+ */
132
+ export interface Harness {
133
+ /**
134
+ * Invoke the full composed-Layer stack for `op` with the given call context.
135
+ * Partial — the harness fills in `operation`, `ctx`, and wires `signal`.
136
+ *
137
+ * Returns a `Promise<Result>` for unary operations or an
138
+ * `AsyncIterable<Chunk>` for streaming operations (SPEC §11).
139
+ */
140
+ invoke(op: Operation, call: Omit<Call, 'operation' | 'ctx'>): Promise<Result> | AsyncIterable<Chunk>;
141
+ }
142
+ /**
143
+ * The merged canonical descriptor: the full set of `Operation`s produced by
144
+ * merging one or more per-language extractors (SPEC §4).
145
+ *
146
+ * Plugins receive a `Descriptor` at generate/serve time; they **must not**
147
+ * modify it — it is the single source of truth.
148
+ */
149
+ export interface Descriptor {
150
+ /** All extracted operations, across all host languages, in insertion order. */
151
+ operations: Operation[];
152
+ /**
153
+ * The host tag of the primary language runtime, e.g. `'ts'`, `'py'`.
154
+ * Plugins may use this to restrict generated code to the primary host.
155
+ */
156
+ host: string;
157
+ /**
158
+ * Optional namespace segment from `--namespace` or the tsconfig root folder.
159
+ * Typically the npm package name or a short domain slug.
160
+ */
161
+ namespace?: string;
162
+ }
163
+ /**
164
+ * **`target`** capability — project the descriptor to a transport or format
165
+ * (SPEC §7.1).
166
+ *
167
+ * A `TargetCapability` is selected by `--type <name>` at CLI time. It
168
+ * optionally both *generates* (emits static files) and *serves* (runs a live
169
+ * server hosting the domain functions in-process).
170
+ *
171
+ * @typeParam Opts - Plugin-specific options, validated via `optionsSchema`.
172
+ *
173
+ * @remarks
174
+ * **v1 compatibility:** The v1 `OutputPlugin` interface (`generate(PluginInput)`)
175
+ * is the direct precursor. v2 `TargetCapability.generate` receives a
176
+ * `Descriptor` instead of `PluginInput` — the richer, host-agnostic contract.
177
+ * v1 plugins migrate by wrapping their `PluginInput` construction in the new
178
+ * `generate` signature. `serve()` is the new addition for live server targets.
179
+ */
180
+ export interface TargetCapability<Opts = Record<string, unknown>> {
181
+ /**
182
+ * Short identifier for this target — the value the user passes to `--type`.
183
+ * Examples: `'mcp'`, `'http-fastify'`, `'http-express'`, `'cli'`, `'proto'`.
184
+ */
185
+ name: string;
186
+ /**
187
+ * Project the descriptor to a set of static files (generate mode).
188
+ *
189
+ * Called once per `apigen generate` invocation with the full merged
190
+ * descriptor and the resolved plugin options. The returned `File[]` is
191
+ * written to the output directory by the CLI.
192
+ *
193
+ * @param descriptor - The merged canonical descriptor (read-only).
194
+ * @param opts - Plugin-specific options (already validated).
195
+ * @returns Array of files to emit (may be empty; never `null`).
196
+ */
197
+ generate(descriptor: Descriptor, opts: Opts): File[] | Promise<File[]>;
198
+ /**
199
+ * Start a live server hosting the domain functions in-process (run mode).
200
+ *
201
+ * Called once per `apigen run` invocation. The transport adapter wires the
202
+ * harness's `invoke()` to the native request/response cycle.
203
+ *
204
+ * Optional — omit for codegen-only plugins (clients, proto, docs).
205
+ *
206
+ * @param descriptor - The merged canonical descriptor (read-only).
207
+ * @param harness - The composed Layer stack; call `harness.invoke()` per request.
208
+ * @param opts - Plugin-specific options (already validated).
209
+ * @returns A {@link Server} handle; the CLI calls `server.close()` on SIGINT/SIGTERM.
210
+ */
211
+ serve?(descriptor: Descriptor, harness: Harness, opts: Opts): Promise<Server>;
212
+ }
213
+ /**
214
+ * **`layer`** capability — wrap operations (the onion) (SPEC §7.1 / §8 / §8.1).
215
+ *
216
+ * A `LayerCapability` is loaded via `--use <plugin>` and is composed by the
217
+ * harness around the `dispatch` call. Hook sugar (`onRequest`/`onResponse`/
218
+ * `onError`) compiles to a `LayerCapability` — one execution model (SPEC §7.1).
219
+ *
220
+ * @remarks
221
+ * **Streaming:** `layer` may return an `AsyncIterable<Chunk>` — making it an
222
+ * `async function*` that wraps `next()` with `for await … yield` — to
223
+ * participate in the full per-chunk stream lifecycle (SPEC §11).
224
+ */
225
+ export interface LayerCapability {
226
+ /**
227
+ * Extra envelope fields this layer needs on the request side — merged into
228
+ * the effective descriptor's envelope schema before serving begins.
229
+ *
230
+ * Keys are bare field names; values are JSON Schema fragments.
231
+ * Example: `{ session: { type: 'string', description: 'session token' } }`.
232
+ */
233
+ envelopeFields?: Record<string, JSONSchema>;
234
+ /**
235
+ * The layer function — owns the continuation.
236
+ *
237
+ * Call `next()` to invoke the remaining layers and `dispatch`. Not calling
238
+ * `next()` short-circuits all downstream layers (SPEC §8.1 rule 1).
239
+ *
240
+ * For streaming operations, return an `AsyncIterable<Chunk>` wrapping the
241
+ * iterable returned by `next()` (SPEC §11).
242
+ *
243
+ * @param call - The inbound call descriptor.
244
+ * @param next - The continuation — call at most once.
245
+ * @returns `Promise<Result>` for unary operations; `AsyncIterable<Chunk>`
246
+ * for streaming operations.
247
+ */
248
+ layer(call: Call, next: Next): Promise<Result> | AsyncIterable<Chunk>;
249
+ }
250
+ /**
251
+ * **`mount`** capability — add synthetic operations to the descriptor
252
+ * (SPEC §7.1 / §7.2b / §7.2c).
253
+ *
254
+ * A `MountCapability` is loaded via `--use <plugin>` and contributes extra
255
+ * `Operation`-like entries (with an in-process `handler`) that flow through the
256
+ * harness and Layer stack exactly like extracted operations. Typical uses:
257
+ * `/meta/openapi`, `/meta/health`, version endpoints.
258
+ */
259
+ export interface MountCapability {
260
+ /**
261
+ * Return the set of synthetic operations this plugin contributes.
262
+ *
263
+ * `MountedOperation` extends `Operation` with an in-process `handler` and
264
+ * an optional `transports` filter (default: all transports).
265
+ *
266
+ * @param descriptor - The current merged descriptor (read-only).
267
+ * @param opts - Plugin-specific options.
268
+ * @returns Array of `MountedOperation`s; may be empty.
269
+ */
270
+ operations(descriptor: Descriptor, opts?: Record<string, unknown>): MountedOperation[];
271
+ }
272
+ /**
273
+ * A synthetic operation contributed by a {@link MountCapability}.
274
+ *
275
+ * Extends the base {@link Operation} with:
276
+ * - `transports` — optional filter to restrict which transports expose this
277
+ * operation (default: all four, matching the host plugin's transport set).
278
+ * - `handler` — the in-process function called when a request arrives.
279
+ * Called with the same {@link Call} context as extracted operations; the
280
+ * returned value is marshalled by the transport adapter.
281
+ */
282
+ export type MountedOperation = Operation & {
283
+ /**
284
+ * Optional transport filter. When omitted the operation is exposed on all
285
+ * transports supported by the active target plugin.
286
+ */
287
+ transports?: Transport[];
288
+ /**
289
+ * The in-process handler for this synthetic operation.
290
+ *
291
+ * Called with the full {@link Call} context (same as extracted operations)
292
+ * after the composed Layer stack has run. The return value is serialised by
293
+ * the transport adapter and may be a `Promise` or an `AsyncIterable` for
294
+ * streaming mounts.
295
+ */
296
+ handler(call: Call): unknown | Promise<unknown> | AsyncIterable<Chunk>;
297
+ };
298
+ /**
299
+ * **`envelope`** capability — declare request/response side-channel fields
300
+ * (SPEC §7.1 / §9 / §9.1).
301
+ *
302
+ * A plugin with an `envelope` capability advertises the transport-agnostic
303
+ * side-channel fields it reads from (request) or writes to (response) without
304
+ * wrapping the operation in a Layer. The harness merges these schemas into the
305
+ * effective descriptor's envelope before serving.
306
+ *
307
+ * Canonical field identity is `(pluginId, field)` (SPEC §9.1); fields declared
308
+ * here are surfaced by each transport adapter per the binding table:
309
+ * - HTTP/gRPC/MCP: `x-<pluginId>-<field>` header/metadata/`_meta` key
310
+ * - CLI: `--<pluginId>-<field>` flag + `APIGEN_<PLUGINID>_<FIELD>` env var
311
+ */
312
+ export interface EnvelopeCapability {
313
+ /**
314
+ * JSON Schema fragments for fields this plugin reads from the incoming
315
+ * transport-native metadata (HTTP headers, gRPC metadata, MCP `_meta`,
316
+ * CLI flags/env).
317
+ *
318
+ * Keys are bare field names (e.g. `'session'`); the adapter prepends
319
+ * `x-<pluginId>-` when surfacing on k/v carriers.
320
+ */
321
+ request?: Record<string, JSONSchema>;
322
+ /**
323
+ * JSON Schema fragments for fields this plugin writes to the outgoing
324
+ * transport-native metadata (response headers, trailers, `_meta`, stderr).
325
+ *
326
+ * Keys follow the same `x-<pluginId>-<field>` convention as `request`.
327
+ */
328
+ response?: Record<string, JSONSchema>;
329
+ }
330
+ /**
331
+ * The v2 plugin interface (SPEC §7.1).
332
+ *
333
+ * Every apigen plugin is an object that satisfies this interface. A plugin
334
+ * declares one or more **capabilities** — the harness fans them out at compose
335
+ * time. All four capability slots are optional; a minimal "noop" plugin omits
336
+ * all of them (useful as a template).
337
+ *
338
+ * @typeParam Opts - Plugin-specific CLI options, validated against
339
+ * `capabilities.target.optionsSchema` (if present) before being passed to
340
+ * `generate` / `serve` / `mount.operations`.
341
+ *
342
+ * @example Logger layer (SPEC §7.2a)
343
+ * ```ts
344
+ * export default {
345
+ * id: 'logger',
346
+ * capabilities: {
347
+ * layer: {
348
+ * layer: async (call, next) => {
349
+ * const t = Date.now()
350
+ * console.error(`→ ${call.operation.id}`)
351
+ * try { const r = await next(); console.error(`← ${call.operation.id} ${Date.now()-t}ms`); return r }
352
+ * catch (e) { console.error(`✗ ${call.operation.id}`); throw e }
353
+ * },
354
+ * },
355
+ * },
356
+ * } satisfies Plugin
357
+ * ```
358
+ *
359
+ * @example OpenAPI mount (SPEC §7.2b)
360
+ * ```ts
361
+ * import { toOpenApi } from '@adhd/apigen-openapi'
362
+ * export default {
363
+ * id: 'openapi',
364
+ * capabilities: {
365
+ * mount: {
366
+ * operations: (d) => [{
367
+ * ...syntheticOp('_meta/openapi', d),
368
+ * handler: () => toOpenApi(d),
369
+ * }],
370
+ * },
371
+ * },
372
+ * } satisfies Plugin
373
+ * ```
374
+ */
375
+ export interface Plugin<Opts = Record<string, unknown>> {
376
+ /**
377
+ * Canonical fully-qualified plugin identifier (SPEC §7.1).
378
+ *
379
+ * Use the package name (e.g. `'@adhd/apigen-ts-plugin-logger'`) or a short
380
+ * slug (e.g. `'logger'`). The CLI accepts either as the `--use` / `--type`
381
+ * argument. The id is also used as the `pluginId` in envelope field naming
382
+ * (`x-<id>-<field>`, SPEC §9.1).
383
+ */
384
+ id: string;
385
+ /**
386
+ * Optional human-readable description shown in `apigen plugins list` output
387
+ * and generated documentation.
388
+ */
389
+ description?: string;
390
+ /**
391
+ * Optional JSON Schema for plugin-specific options.
392
+ *
393
+ * When provided, the CLI validates the `--opt` values supplied via
394
+ * `--use <plugin> --opt key=value` before constructing `opts`.
395
+ */
396
+ optionsSchema?: Record<string, unknown>;
397
+ /**
398
+ * The set of capabilities this plugin contributes. All four are optional.
399
+ *
400
+ * At least one capability is expected in practice; the harness warns
401
+ * (at debug level) when a loaded plugin declares no capabilities.
402
+ */
403
+ capabilities: {
404
+ /**
405
+ * Target capability — project the descriptor to a transport/format and/or
406
+ * host domain functions in-process (SPEC §7.1 / §5).
407
+ *
408
+ * Selected by `--type <plugin>`.
409
+ */
410
+ target?: TargetCapability<Opts>;
411
+ /**
412
+ * Layer capability — wrap all operations in the onion (SPEC §7.1 / §8).
413
+ *
414
+ * Loaded by `--use <plugin>` when the plugin declares this capability.
415
+ */
416
+ layer?: LayerCapability;
417
+ /**
418
+ * Mount capability — add synthetic operations to the descriptor
419
+ * (SPEC §7.1). Typical uses: `/meta/openapi`, `/meta/health`.
420
+ *
421
+ * Loaded by `--use <plugin>`.
422
+ */
423
+ mount?: MountCapability;
424
+ /**
425
+ * Envelope capability — declare request/response side-channel fields
426
+ * (SPEC §7.1 / §9.1). Loaded by `--use <plugin>`.
427
+ *
428
+ * A plugin may combine `envelope` with `layer` to both *declare* the
429
+ * fields it needs and *read/write* them in its layer function.
430
+ */
431
+ envelope?: EnvelopeCapability;
432
+ };
433
+ }
@@ -0,0 +1,2 @@
1
+ /** Recursive fallback schema builder for primitive, array, union, and anonymous object types. */
2
+ export declare function morphFallback(typeText: string, depth: number): Record<string, unknown>;
@@ -0,0 +1,4 @@
1
+ import { Project, SourceFile } from 'ts-morph';
2
+
3
+ /** Attempts ts-json-schema-generator first; falls back to morphFallback for inline/anonymous types. */
4
+ export declare function buildSchema(_project: Project, sf: SourceFile, typeText: string, tsconfig?: string): Promise<Record<string, unknown>>;
package/lib/types.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { Logger } from 'pino';
2
+
3
+ export interface GeneratedSchemas {
4
+ metadata: {
5
+ namespace: string;
6
+ phase: string;
7
+ };
8
+ schemas: Record<string, {
9
+ input: Record<string, unknown>;
10
+ output: Record<string, unknown>;
11
+ hasCtx?: boolean;
12
+ }>;
13
+ }
14
+ export type ComposedSchemas = Record<string, {
15
+ input: Record<string, unknown>;
16
+ output: Record<string, unknown>;
17
+ hasCtx?: boolean;
18
+ }>;
19
+ export type ExportMode = {
20
+ type: 'named';
21
+ } | {
22
+ type: 'default';
23
+ } | {
24
+ type: 'named-object';
25
+ name: string;
26
+ };
27
+ export interface GenerateSchemasOptions {
28
+ sourceFile: string;
29
+ exportMode?: ExportMode;
30
+ namespace?: string;
31
+ phase?: string;
32
+ tsconfig?: string;
33
+ }
34
+ export interface PluginInput {
35
+ packages: Array<{
36
+ id: string;
37
+ schemas: ComposedSchemas;
38
+ importPath: string;
39
+ fns?: Record<string, (...args: unknown[]) => unknown>;
40
+ createClient?: (envelope: Record<string, unknown>) => Promise<unknown>;
41
+ }>;
42
+ outputDir: string;
43
+ options: Record<string, unknown>;
44
+ /**
45
+ * Shared structured logger (pino). Built once by the CLI and threaded through
46
+ * the pipeline + plugins. Always targets stderr or a file — never stdout —
47
+ * so the MCP stdio JSON-RPC channel stays clean. Plugins should fall back to
48
+ * a default stderr logger when this is absent.
49
+ */
50
+ logger?: Logger;
51
+ }
52
+ export interface PluginOutput {
53
+ files: Array<{
54
+ path: string;
55
+ content: string;
56
+ }>;
57
+ postCommands?: string[];
58
+ }
59
+ export interface RunInput extends PluginInput {
60
+ signal?: AbortSignal;
61
+ }
62
+ export interface OutputPlugin {
63
+ id: string;
64
+ description: string;
65
+ optionsSchema?: Record<string, unknown>;
66
+ generate(input: PluginInput): PluginOutput | Promise<PluginOutput>;
67
+ run?(input: RunInput): Promise<void>;
68
+ }
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@adhd/apigen-core",
3
+ "version": "0.1.0",
4
+ "dependencies": {
5
+ "ts-morph": "^23.0.0",
6
+ "ts-json-schema-generator": "^2.3.0",
7
+ "pino": "10.3.1"
8
+ },
9
+ "main": "./index.js",
10
+ "module": "./index.mjs",
11
+ "typings": "./index.d.ts",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ }
15
+ }