scripts — Codegen & Tooling
The scripts/ directory is where one JSON Schema becomes four language-native SDKs. A chain of TypeScript files reads api.schema.json (sourced from nodejs/node_modules/@github/copilot/schemas/), normalises it, then emits per-language method stubs, type declarations, and polymorphic converters. This is the mechanism that keeps the four SDKs in sync — adding an RPC method means updating the schema and re-running codegen, not editing four files by hand.
Schema-first TypeScript codegen Committed output
📚 What is this & why does it exist?
A set of TypeScript scripts that read api.schema.json (the authoritative description of every RPC method, parameter, and event) and generate code for each SDK: GeneratedRpc in Node, _generated_rpc.py in Python, generated_rpc.go in Go, and Generated/Rpc.cs + Generated/SessionEvents.cs in .NET. The generators share a common AST representation (utils.ts, postProcess.ts) and a corrections layer for schema quirks.
💡 Real-world analogy
Think of api.schema.json as the architectural blueprint for a skyscraper, and the four emitters as specialist contractors: one frames in steel (Go), one pours concrete (C#), one fits interiors (TypeScript), one runs plumbing (Python). They all read the same blueprint, but each knows the idioms of their trade: where to add a nullable flag, how to spell a method name (camelCase vs snake_case vs PascalCase), whether to emit a tagged union or a polymorphic converter. The corrections dir is the clipboard where the architect clarified "on this corner, ignore the blueprint — use this instead."
🎯 Why This Module Exists
Without codegen, every protocol addition means touching four language repos, writing four type declarations, and hoping nobody typoed a field name. With codegen, a schema edit + one command updates all four SDKs atomically. The decision to commit the generated output rather than generate at build time is deliberate: PR reviewers can see the exact diff (does this method signature break users?); CI doesn't need Node.js to build the Go SDK; debuggers step into real code, not trampolines. The cost — occasional merge conflicts in generated files — is small compared to the reviewability win.
🏗️ Design Decisions
| Decision | Choice Made | Alternatives | Why |
|---|---|---|---|
| Generator language | TypeScript | Python, Go, Java | Node is required for the SDK anyway; TS gives strong typing over the schema structure |
| Schema source | JSON Schema co-located with Node types under @github/copilot/schemas/ | OpenAPI; Protocol Buffers | JSON Schema draft-07 is compact, already in use by TS type gen upstream |
| Output location | Committed into each SDK's source tree | Generate-at-build; external package | Reviewable diffs, no build-time Node dep for non-TS SDKs |
| Corrections mechanism | Patch-style objects in corrections/ | Edit generated output; fork the schema | Keeps schema canonical; corrections are auditable & narrowly scoped |
| Dispatch style per language | Per-language idiomatic: TS unions, Py dataclasses, Go type tags, C# polymorphic converters | Same shape in all four | Each SDK should feel native; force-fitting one pattern breaks ergonomics |
| Number-vs-integer handling | Pre-process to preserve integer constraints | Treat all numbers the same | Go, C# distinguish int64/double; dropping it causes wrong wire encoding |
Nullable required handling | fixNullableRequiredRefsInApiSchema pre-pass | Manual fixes in each emitter | Raw schema's "required but nullable" is ambiguous across languages; normalize once up front |
Codegen Pipeline
flowchart LR SCHEMA[api.schema.json
authoritative] PRE[Pre-processing
postProcessSchema
fixNullableRequiredRefs] CORR[corrections/
patch overrides] AST[Shared AST
utils.ts] subgraph emitters["Per-language emitters"] TS[typescript.ts
→ GeneratedRpc] PY[python.ts
→ _generated_rpc.py] GO[go.ts
→ generated_rpc.go] CS[csharp.ts
→ Generated/Rpc.cs] end SCHEMA --> PRE CORR --> PRE PRE --> AST AST --> TS AST --> PY AST --> GO AST --> CS TS -->|committed| NODE[nodejs/ sources] PY -->|committed| PYTHON[python/ sources] GO -->|committed| GOLANG[go/ sources] CS -->|committed| DOTNET[dotnet/ sources]
🚶 Codegen Walkthrough
Follow npm run codegen end-to-end:
- Entry —
scripts/codegen/index.tsreadsapi.schema.jsonvia a path relative tonodejs/node_modules/@github/copilot/schemas/. - Parse schema — JSON.parse into a typed tree (the TS script has its own types describing the schema shape).
- Apply corrections — merge every object under
corrections/*.json/.tsinto the schema tree (override definitions, add missing descriptions, delete deprecated methods). postProcessSchema— normalise: resolve$refs, mark integers vs numbers, flatten single-variant unions, set explicitnullableflags.fixNullableRequiredRefsInApiSchema— for each required property that is also nullable, ensure the language emitter knows to emitField?/Optional[T]/*Tappropriately.- Build shared AST —
utils.tshelpers turn the schema tree into a method/type/event tree with language-agnostic names. - Emit — each of
typescript.ts,python.ts,go.ts,csharp.tswalks the AST and prints source using a StringBuilder-like pattern. No templating engine — raw string concatenation with per-language quote/escape helpers. - Format — each emitter pipes output through its language's formatter where available (prettier, gofmt, dotnet format, ruff).
- Write — overwrite the target files in the four SDKs.
- Verify — CI runs
npm run codegen -- --check, which regenerates into.tmpand diffs against committed files. Any drift fails the build.
Key Components
| File | Role | Why it matters |
|---|---|---|
scripts/codegen/index.ts | Top-level orchestrator | Reads the schema, dispatches to each emitter. |
scripts/codegen/utils.ts | Schema walkers, name conversion (camel/snake/pascal), quote helpers | Shared by all emitters; the "AST" layer. |
scripts/codegen/postProcess.ts | postProcessSchema, fixNullableRequiredRefsInApiSchema | Normalisation pass; ensures per-language emitters see a consistent tree. |
scripts/codegen/typescript.ts | TS emitter — interfaces + GeneratedRpc client class | Trivial for the host language; acts as the reference output. |
scripts/codegen/python.ts | Python emitter — dataclasses + type unions | Targets Python 3.11+; emits from __future__ import annotations style. |
scripts/codegen/go.ts | Go emitter — structs, methods, type tags; skips event union (hand-written) | Handles camelCase→PascalCase, json:"..." tags, zero-value semantics. |
scripts/codegen/csharp.ts | C# emitter — records, polymorphic converter class, nullable refs | Emits [JsonConverter(...)], [JsonPropertyName(...)], STJ polymorphic discriminators. |
scripts/codegen/corrections/ | Per-method/type overrides | Where "the schema is not quite right for language X" lives. |
scripts/check-api-surface.ts | API-diff guard | Verifies Node public API matches a manifest; catches accidental breaking changes. |
scripts/validate-sdk-cli-versions.ts | Version coherence check | Ensures each SDK advertises the same protocol version as the schema. |
⚡ Algorithm Spotlight
postProcessSchema
A recursive walker that (a) inlines $refs under a size threshold, (b) tags numeric types with {kind: "integer" | "number"} from JSON-Schema's type + format + multipleOf, (c) flattens single-variant oneOf into the parent, (d) canonicalises nullable (explicit type: [T, null] becomes {nullable: true}). Visitor pattern; ~200 lines.
fixNullableRequiredRefsInApiSchema
Specific fix for a recurring schema pattern where a property is listed in required but its type is [T, null]. In TypeScript this is T | null (still required), in Go it is *T, in Python it is Optional[T], in C# it is T?. Without the correction, Go would emit T (no pointer, no nil representation) and silently lose null-carrying information.
Per-language emitter strategy
Each emitter: (1) pre-computes name mappings (camelCase→target convention), (2) emits top-of-file headers, (3) walks types in topological order (so referenced types appear before referencing ones), (4) emits methods, (5) emits events (or skips them if the SDK hand-writes them, as Go does for result_union.go).
⚠ Edge Cases & Pitfalls
| Scenario | Behaviour | Notes |
|---|---|---|
| Schema adds a new event type | All four emitters pick it up; CI diff fails until commit | Ensures no SDK silently drops events |
| Schema changes a field's type | Codegen updates; compilation may fail downstream | Intentional — wire-breaking change should break compile |
| Corrections file is stale | Override applied but schema no longer has that path | A warning is logged; CI shouldn't fail (stale corrections are a common intermediate state) |
| Reserved-word collision | Each emitter keeps a reserved-word list and appends _ | e.g. Python class → class_ |
| Regeneration drift | npm run codegen -- --check in CI catches it | Developer must rerun codegen before committing |
📊 Performance
Codegen is a one-shot tool — runs in ~1.5s locally for all four SDKs. Not a hot path. Optimisation went into reviewability (deterministic output, stable ordering) not throughput.
Deep-Dive Highlights
Codegen Pipeline Focus
End-to-end: schema → AST → per-language emitter → formatter → committed file.
Protocol Negotiation Focus
How the SDKs advertise their generated protocol version and reject older CLIs.
Configuration Reference
| Knob | Where | Purpose |
|---|---|---|
--check | CLI flag | Regenerate into a temp dir and fail on drift |
--language | CLI flag | Regenerate a single language only |
SCHEMA_PATH | env | Override schema source path |
OUT_DIR | env | Write to alternate location (used by CI diff) |
| Reserved-words list | per-emitter constant | Name-collision safe words |
| Name-case mapping | utils.ts | Controls camel/snake/pascal conversion |
| Formatter toggle | per-emitter | Skip formatter if tool missing (local dev) |
| Emit events? | Go emitter | Skip (Go uses hand-written result_union.go) |
| Emit polymorphic converter? | C# emitter | Yes — essential for STJ |
| Emit dataclass slots? | Python emitter | Yes (3.10+) |
Code Examples
utils.ts — name conversion
export function toPascalCase(s: string): string {
return s.replace(/(^|[_-])([a-z])/g, (_, _sep, c) => c.toUpperCase());
}
export function toSnakeCase(s: string): string {
return s.replace(/[A-Z]/g, c => "_" + c.toLowerCase()).replace(/^_/, "");
}
postProcessSchema structure
export function postProcessSchema(schema: JsonSchema): JsonSchema {
schema = inlineSmallRefs(schema);
schema = tagIntegers(schema);
schema = canonicaliseNullable(schema);
schema = flattenSingleUnions(schema);
return schema;
}
csharp.ts — record emission (illustrative)
function emitRecord(t: TypeNode, out: Emitter): void {
out.line(`public sealed record ${pascal(t.name)}(`);
out.indent(() => {
t.fields.forEach((f, i) => {
const comma = i < t.fields.length - 1 ? "," : "";
out.line(`${csType(f.type)} ${pascal(f.name)}${comma}`);
});
});
out.line(`);`);
}
Directory Map
scripts/ source tree
scripts/
├── codegen/
│ ├── index.ts ← Orchestrator entry
│ ├── utils.ts ← Shared helpers + AST walkers
│ ├── postProcess.ts ← Schema normalisation pass
│ ├── typescript.ts ← TS emitter
│ ├── python.ts ← Python emitter
│ ├── go.ts ← Go emitter
│ ├── csharp.ts ← C# emitter
│ └── corrections/ ← Per-method/type patches
├── check-api-surface.ts ← Node API-diff guard
├── validate-sdk-cli-versions.ts ← Version coherence check
├── validate-samples.sh ← Build-check every sample
└── package.json ← devDeps: tsx, prettier, etc.