Here is an automated code-generator script that pulls the active catalog
specifications from the registry and compiles a strongly-typed TypeScript SDK
(registry-sdk.ts) and declaration file.
This generator inspects both the base OpenAPI HTTP endpoints and the internal
spec.json schemas for every routine, emitting explicit interfaces and method
signatures.
1. The SDK Generator (harness/generate-sdk.js)
Add this script to your harness/ directory:
/ harness/generate-sdk.js const fs = require('fs').promises; const path =
require('path'); const REGISTRY_HOST = process.env.REGISTRY_HOST ||
'http://localhost:3377'; const OUTPUT_FILE = path.join(__dirname,
'../dist/registry-sdk.ts'); function mapJsonTypeToTs(type) { switch (type) {
case 'string': return 'string'; case 'number': return 'number'; case
'boolean': return 'boolean'; case 'array': return 'any[]'; case 'object':
return 'Record<string, any>'; default: return 'any'; } } function
toCamelCase(str) { return str.replace(/_([a-z])/g, (_, letter) =>
letter.toUpperCase()); } function toPascalCase(str) { const camel =
toCamelCase(str); return camel.charAt(0).toUpperCase() + camel.slice(1); }
async function generateSdk() { console.log(`[SDK Gen] Fetching routine
specifications from ${REGISTRY_HOST}...`); let routines = []; try { const res
= await fetch(`${REGISTRY_HOST}/routines`); if (!res.ok) throw new Error(`HTTP
${res.status}`); routines = await res.json(); } catch (err) {
console.error(`[SDK Gen] Failed to fetch routines: ${err.message}`);
process.exit(1); } let tsInterfaces = ''; let clientMethods = ''; for (const
routine of routines) { const interfaceName = `${toPascalCase(routine.name)}Para
ms`; const methodName = toCamelCase(routine.name); let fields = ''; for (const
[paramName, config] of Object.entries(routine.parameters || {})) { const
optionalFlag = config.required ? '' : '?'; const tsType = mapJsonTypeToTs(confi
g.type); const comment = config.default !== undefined ? ` /** @default
${JSON.stringify(config.default)} */
` : ''; fields += ` ${comment}${paramName}${optionalFlag}: ${tsType};
`; } tsInterfaces += ` /** * Parameters for '${routine.name}' *
${routine.description || 'No description provided.'} */ export interface
${interfaceName} { ${fields}} `; clientMethods += ` /** * ${routine.description
|| routine.name} */ async ${methodName}(params: ${interfaceName}):
Promise<any> { return this.runRoutine('${routine.name}', params); } `; } const
sdkSource = `// AUTO-GENERATED BY harness/generate-sdk.
js // Generation Timestamp: ${new Date().toISOString()} ${tsInterfaces} export
interface RoutineRegistryConfig { baseUrl: string; fetchImpl?: typeof fetch; }
export class RoutineRegistryClient { private baseUrl: string; private fetch:
typeof fetch; constructor(config: RoutineRegistryConfig) { this.baseUrl =
config.baseUrl.replace(/\\/+$/, ''); this.fetch = config.fetchImpl ||
globalThis.fetch.bind(globalThis); } private async runRoutine(name: string,
params: Record<string, any>): Promise<any> { const response = await
this.fetch(\`\${this.baseUrl}/routines/\${name}/run\`, { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params),
}); const data = await response.json(); if (!response.ok || !data.success) {
const details = data.validation_errors ? data.validation_errors.join(', ') :
data.error; throw new Error(\`Routine execution failed (\${name}): \${details
|| response.statusText}\`); } return data.result; } async getSpec(name:
string): Promise<any> { const res = await this.fetch(\`\${this.baseUrl}/routine
s/\${name}\`); if (!res.ok) throw new Error(\`Failed to fetch spec for
\${name}\`); return res.json(); } async downloadArtifact(name: string):
Promise<string> { const res = await this.fetch(\`\${this.baseUrl}/routines/\${n
ame}/artifact\`); if (!res.ok) throw new Error(\`Failed to download artifact
for \${name}\`); return res.text(); } // --- Strongly Typed Routine
Invocations --- ${clientMethods} } `; await fs.mkdir(path.dirname(OUTPUT_FILE),
{ recursive: true }); await fs.writeFile(OUTPUT_FILE, sdkSource.trim() + '
', 'utf8'); console.log(`[SDK Gen] Generated ${routines.length} routine
signatures at ${OUTPUT_FILE}`); } generateSdk();
2. Output Example (dist/registry-sdk.ts)
Given the routines registered earlier (calculate_tax and format_log), running
node harness/generate-sdk.js compiles the following client:
/ AUTO-GENERATED BY harness/generate-sdk.js /** * Parameters for
'calculate_tax' */ export interface CalculateTaxParams { subtotal: number; /**
@default "FI" */ region?: string; } /** * Parameters for 'format_log' */
export interface FormatLogParams { /** @default "info" */ level?: string;
message: string; /** @default "edge-01" */ nodeId?: string; } export class
RoutineRegistryClient { // ... setup and core helpers ... async
calculateTax(params: CalculateTaxParams): Promise<any> { return
this.runRoutine('calculate_tax', params); } async formatLog(params:
FormatLogParams): Promise<any> { return this.runRoutine('format_log', params);
} }
3. Usage in a Consumer Project
Consumers instantiate the typed client and call the routines directly as
native methods:
import { RoutineRegistryClient } from './dist/registry-sdk'; const client =
new RoutineRegistryClient({ baseUrl: 'http://localhost:3377' }); // Fully
type-checked: missing 'subtotal' produces a TypeScript compilation error const
taxResult = await client.calculateTax({ subtotal: 150, region: 'UK' }); const
logResult = await client.formatLog({ message: 'Worker initialized
successfully.' });