@pikku/cli 0.12.2 → 0.12.3

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 (110) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/index-Ci24-VT-.js +657 -0
  3. package/console-app/assets/{index-0Ui5UudO.css → index-DvrDbftC.css} +1 -1
  4. package/console-app/index.html +2 -2
  5. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  6. package/dist/.pikku/agent/pikku-agent-wirings-meta.gen.js +1 -1
  7. package/dist/.pikku/agent/pikku-agent-wirings.gen.d.ts +1 -1
  8. package/dist/.pikku/agent/pikku-agent-wirings.gen.js +1 -1
  9. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  10. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  11. package/dist/.pikku/channel/pikku-channels-meta.gen.js +1 -1
  12. package/dist/.pikku/channel/pikku-channels.gen.d.ts +1 -1
  13. package/dist/.pikku/channel/pikku-channels.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-channel.d.ts +13 -1
  15. package/dist/.pikku/cli/pikku-cli-channel.js +17 -2
  16. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  17. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  18. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  19. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  20. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  21. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +12 -1
  22. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  23. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  24. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  25. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  26. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  27. package/dist/.pikku/function/pikku-function-types.gen.d.ts +30 -1
  28. package/dist/.pikku/function/pikku-function-types.gen.js +17 -1
  29. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  30. package/dist/.pikku/function/pikku-functions-meta.gen.json +114 -114
  31. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  32. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  33. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  34. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  35. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  36. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  37. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  38. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  39. package/dist/.pikku/mcp/pikku-mcp-wirings-meta.gen.js +1 -1
  40. package/dist/.pikku/mcp/pikku-mcp-wirings.gen.d.ts +1 -1
  41. package/dist/.pikku/mcp/pikku-mcp-wirings.gen.js +1 -1
  42. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  43. package/dist/.pikku/pikku-services.gen.d.ts +3 -1
  44. package/dist/.pikku/pikku-services.gen.js +2 -0
  45. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  46. package/dist/.pikku/pikku-types.gen.js +1 -1
  47. package/dist/.pikku/pikku-websocket.gen.d.ts +1 -1
  48. package/dist/.pikku/pikku-websocket.gen.js +1 -1
  49. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  50. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  51. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  52. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  53. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  54. package/dist/.pikku/rpc/pikku-remote-rpc-workers.gen.js +1 -1
  55. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  56. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  57. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  58. package/dist/.pikku/scheduler/pikku-schedulers-wirings-meta.gen.js +1 -1
  59. package/dist/.pikku/scheduler/pikku-schedulers-wirings.gen.d.ts +1 -1
  60. package/dist/.pikku/scheduler/pikku-schedulers-wirings.gen.js +1 -1
  61. package/dist/.pikku/schemas/register.gen.js +9 -9
  62. package/dist/.pikku/schemas/schemas/ConsoleCommandInput.schema.json +1 -1
  63. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  64. package/dist/.pikku/schemas/schemas/PikkuNewAddonInput.schema.json +1 -1
  65. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  66. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  67. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  68. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  69. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  70. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  71. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  72. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  73. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  74. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  75. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  76. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  77. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  78. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.d.ts +1 -1
  79. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  80. package/dist/src/cli.wiring.js +9 -0
  81. package/dist/src/functions/commands/all.js +33 -11
  82. package/dist/src/functions/commands/console.d.ts +3 -0
  83. package/dist/src/functions/commands/console.js +4 -2
  84. package/dist/src/functions/commands/new-addon.d.ts +3 -0
  85. package/dist/src/functions/commands/new-addon.js +8 -6
  86. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +19 -0
  87. package/dist/src/functions/wirings/channels/pikku-command-channels-map.js +1 -1
  88. package/dist/src/functions/wirings/channels/serialize-typed-channel-map.d.ts +1 -1
  89. package/dist/src/functions/wirings/channels/serialize-typed-channel-map.js +7 -6
  90. package/dist/src/functions/wirings/cli/pikku-command-cli-entry.js +9 -1
  91. package/dist/src/functions/wirings/cli/serialize-channel-cli.js +35 -12
  92. package/dist/src/functions/wirings/console/pikku-command-node-types.js +2 -2
  93. package/dist/src/functions/wirings/console/pikku-command-nodes-meta.js +20 -11
  94. package/dist/src/functions/wirings/console/serialize-console-functions.js +1 -1
  95. package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +1 -1
  96. package/dist/src/functions/wirings/functions/serialize-addon-types.js +23 -1
  97. package/dist/src/functions/wirings/functions/serialize-function-types.js +38 -0
  98. package/dist/src/services.js +1 -1
  99. package/dist/src/utils/file-import-path.js +5 -2
  100. package/dist/src/utils/openapi/codegen.d.ts +1 -0
  101. package/dist/src/utils/openapi/codegen.js +214 -46
  102. package/dist/src/utils/openapi/parse-openapi.d.ts +25 -0
  103. package/dist/src/utils/openapi/parse-openapi.js +119 -9
  104. package/dist/src/utils/openapi/zod-codegen.d.ts +1 -53
  105. package/dist/src/utils/openapi/zod-codegen.js +1 -251
  106. package/dist/src/utils/pikku-cli-config.js +2 -2
  107. package/dist/src/utils/strip-verbose-meta.d.ts +2 -0
  108. package/dist/src/utils/strip-verbose-meta.js +34 -0
  109. package/package.json +4 -3
  110. package/console-app/assets/index-DRJQtv3c.js +0 -676
@@ -1,21 +1,48 @@
1
1
  import { schemaToZod, schemaVarName, createContext, } from './zod-codegen.js';
2
2
  import { generateOperationNames, detectCommonPrefix, } from './naming.js';
3
3
  const GENERIC_SUMMARIES = new Set([
4
- 'index', 'show', 'create', 'update', 'destroy', 'delete', 'list',
4
+ 'index',
5
+ 'show',
6
+ 'create',
7
+ 'update',
8
+ 'destroy',
9
+ 'delete',
10
+ 'list',
5
11
  ]);
12
+ /** Map from HTTP status code to pikku error class name */
13
+ const STATUS_TO_ERROR = {
14
+ 400: 'BadRequestError',
15
+ 401: 'UnauthorizedError',
16
+ 403: 'ForbiddenError',
17
+ 404: 'NotFoundError',
18
+ 405: 'MethodNotAllowedError',
19
+ 409: 'ConflictError',
20
+ 422: 'UnprocessableContentError',
21
+ 429: 'TooManyRequestsError',
22
+ 500: 'InternalServerError',
23
+ };
6
24
  function capitalize(str) {
7
25
  return str.charAt(0).toUpperCase() + str.slice(1);
8
26
  }
9
27
  function humanDescription(named, parsed) {
10
- if (parsed.responseDescription) {
11
- return capitalize(parsed.responseDescription);
12
- }
13
28
  const summary = parsed.summary?.trim();
29
+ const description = parsed.description?.trim();
30
+ // Summary is preferred — it's the operation's intent
14
31
  if (summary && !GENERIC_SUMMARIES.has(summary.toLowerCase())) {
32
+ // If description adds info beyond summary, combine them
33
+ if (description &&
34
+ !GENERIC_SUMMARIES.has(description.toLowerCase()) &&
35
+ description.toLowerCase() !== summary.toLowerCase()) {
36
+ const sep = summary.endsWith('.') ? ' ' : '. ';
37
+ return `${capitalize(summary)}${sep}${capitalize(description)}`;
38
+ }
15
39
  return capitalize(summary);
16
40
  }
17
- if (parsed.description && !GENERIC_SUMMARIES.has(parsed.description.trim().toLowerCase())) {
18
- return capitalize(parsed.description.trim());
41
+ if (description && !GENERIC_SUMMARIES.has(description.toLowerCase())) {
42
+ return capitalize(description);
43
+ }
44
+ if (parsed.responseDescription) {
45
+ return capitalize(parsed.responseDescription);
19
46
  }
20
47
  const words = named.functionName
21
48
  .replace(/([A-Z])/g, ' $1')
@@ -23,6 +50,16 @@ function humanDescription(named, parsed) {
23
50
  .toLowerCase();
24
51
  return capitalize(words);
25
52
  }
53
+ function getErrorClassesForResponses(errorResponses) {
54
+ const classes = [];
55
+ for (const err of errorResponses) {
56
+ const cls = STATUS_TO_ERROR[err.statusCode];
57
+ if (cls && !classes.includes(cls)) {
58
+ classes.push(cls);
59
+ }
60
+ }
61
+ return classes;
62
+ }
26
63
  export function generateAddonFromOpenAPI(spec, vars, flags) {
27
64
  const files = {};
28
65
  const { name } = vars;
@@ -50,7 +87,7 @@ export function generateAddonFromOpenAPI(spec, vars, flags) {
50
87
  const functionExports = [];
51
88
  for (const { named, parsed } of opPairs) {
52
89
  const funcCtx = createContext(schemaRefs);
53
- const funcCode = generateFunctionFile(named, parsed, vars, funcCtx);
90
+ const funcCode = generateFunctionFile(named, parsed, vars, funcCtx, spec, flags);
54
91
  files[`src/functions/${named.functionName}.function.ts`] = funcCode;
55
92
  functionExports.push(named.functionName);
56
93
  }
@@ -58,6 +95,8 @@ export function generateAddonFromOpenAPI(spec, vars, flags) {
58
95
  files['src/index.ts'] = generateIndexFile(functionExports);
59
96
  // Generate typed API service class with route map
60
97
  files[`src/${name}-api.service.ts`] = generateServiceFile(spec, opPairs, vars, flags);
98
+ // Generate variable file for BASE_URL
99
+ files[`src/${name}.variable.ts`] = generateVariableFile(spec, vars);
61
100
  return files;
62
101
  }
63
102
  function generateTypesFile(spec, ctx) {
@@ -75,17 +114,32 @@ function generateTypesFile(spec, ctx) {
75
114
  }
76
115
  return lines.join('\n');
77
116
  }
78
- function generateFunctionFile(named, parsed, vars, ctx) {
117
+ function generateFunctionFile(named, parsed, vars, ctx, spec, flags) {
79
118
  const lines = [];
80
119
  const { camelName } = vars;
120
+ // Tag description as file header
121
+ if (parsed.tags.length > 0) {
122
+ const tag = parsed.tags[0];
123
+ const tagDesc = spec.tagDescriptions[tag];
124
+ if (tagDesc) {
125
+ lines.push(`// ${tag} — ${tagDesc}`);
126
+ lines.push('');
127
+ }
128
+ }
81
129
  const hasInput = parsed.pathParams.length > 0 ||
82
130
  parsed.queryParams.length > 0 ||
131
+ parsed.headerParams.length > 0 ||
83
132
  parsed.requestBody;
84
133
  const pascalName = named.functionName.charAt(0).toUpperCase() + named.functionName.slice(1);
85
134
  const inputName = `${pascalName}Input`;
86
135
  const outputName = `${pascalName}Output`;
136
+ // Determine error imports needed
137
+ const errorClasses = getErrorClassesForResponses(parsed.errorResponses);
87
138
  lines.push("import { z } from 'zod'");
88
139
  lines.push("import { pikkuSessionlessFunc } from '#pikku'");
140
+ if (errorClasses.length > 0) {
141
+ lines.push(`import { ${errorClasses.join(', ')} } from '@pikku/core/errors'`);
142
+ }
89
143
  lines.push('');
90
144
  // Build Input schema (exported for pikku schema discovery)
91
145
  if (hasInput) {
@@ -95,7 +149,7 @@ function generateFunctionFile(named, parsed, vars, ctx) {
95
149
  }
96
150
  // Build Output schema (exported for pikku schema discovery)
97
151
  if (parsed.responseSchema) {
98
- const outputCode = schemaToZod(parsed.responseSchema, ctx);
152
+ const outputCode = buildOutputSchema(parsed.responseSchema, ctx);
99
153
  lines.push(`export const ${outputName} = ${outputCode}`);
100
154
  lines.push('');
101
155
  }
@@ -105,11 +159,19 @@ function generateFunctionFile(named, parsed, vars, ctx) {
105
159
  funcConfig.push(` description: ${JSON.stringify(description)},`);
106
160
  if (hasInput)
107
161
  funcConfig.push(` input: ${inputName},`);
108
- if (parsed.responseSchema)
162
+ if (parsed.responseSchema) {
109
163
  funcConfig.push(` output: ${outputName},`);
110
- const funcParams = hasInput
111
- ? `{ ${camelName} }, data`
112
- : `{ ${camelName} }`;
164
+ }
165
+ else {
166
+ funcConfig.push(' output: z.void(),');
167
+ }
168
+ if (errorClasses.length > 0) {
169
+ funcConfig.push(` errors: [${errorClasses.join(', ')}],`);
170
+ }
171
+ if (flags.mcp) {
172
+ funcConfig.push(' mcp: true,');
173
+ }
174
+ const funcParams = hasInput ? `{ ${camelName} }, data` : `{ ${camelName} }`;
113
175
  const returnCast = parsed.responseSchema ? ' as any' : '';
114
176
  funcConfig.push(` func: async (${funcParams}) => {`, ` return ${camelName}.call('${method}', '${parsed.path}'${hasInput ? ', data' : ''})${returnCast}`, ' },');
115
177
  lines.push(`export const ${named.functionName} = pikkuSessionlessFunc({`);
@@ -121,26 +183,27 @@ function generateFunctionFile(named, parsed, vars, ctx) {
121
183
  function buildInputSchema(parsed, ctx) {
122
184
  const props = [];
123
185
  for (const param of parsed.pathParams) {
124
- const zodCode = schemaToZod(param.schema, ctx, { optional: !param.required });
125
- const desc = param.description
126
- ? `${zodCode}.describe(${JSON.stringify(param.description)})`
127
- : zodCode;
128
- props.push(` ${param.name}: ${desc},`);
186
+ props.push(formatParamProp(param, ctx));
129
187
  }
130
188
  for (const param of parsed.queryParams) {
131
- const zodCode = schemaToZod(param.schema, ctx, { optional: !param.required });
132
- const desc = param.description
133
- ? `${zodCode}.describe(${JSON.stringify(param.description)})`
134
- : zodCode;
135
- props.push(` ${param.name}: ${desc},`);
189
+ props.push(formatParamProp(param, ctx));
190
+ }
191
+ for (const param of parsed.headerParams) {
192
+ props.push(formatParamProp(param, ctx));
136
193
  }
137
194
  if (parsed.requestBody) {
138
195
  if (parsed.requestBody.properties) {
139
196
  const requiredSet = new Set(parsed.requestBody.required ?? []);
140
197
  for (const [key, propSchema] of Object.entries(parsed.requestBody.properties)) {
198
+ // Skip readOnly properties from input
199
+ if (propSchema.readOnly)
200
+ continue;
141
201
  const isOptional = !requiredSet.has(key);
142
202
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional });
143
- props.push(` ${key}: ${zodCode},`);
203
+ const withDesc = propSchema.description
204
+ ? `${zodCode}.describe(${JSON.stringify(propSchema.description)})`
205
+ : zodCode;
206
+ props.push(` ${key}: ${withDesc},`);
144
207
  }
145
208
  }
146
209
  else {
@@ -150,6 +213,32 @@ function buildInputSchema(parsed, ctx) {
150
213
  }
151
214
  return `z.object({\n${props.join('\n')}\n})`;
152
215
  }
216
+ function formatParamProp(param, ctx) {
217
+ const zodCode = schemaToZod(param.schema, ctx, { optional: !param.required });
218
+ let descParts = [];
219
+ if (param.description)
220
+ descParts.push(param.description);
221
+ if (param.example !== undefined)
222
+ descParts.push(`Example: ${JSON.stringify(param.example)}`);
223
+ const desc = descParts.length > 0
224
+ ? `${zodCode}.describe(${JSON.stringify(descParts.join('. '))})`
225
+ : zodCode;
226
+ return ` ${param.name}: ${desc},`;
227
+ }
228
+ function buildOutputSchema(schema, ctx) {
229
+ // For output schemas, filter out writeOnly properties
230
+ if (schema.properties) {
231
+ const filteredProps = {};
232
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
233
+ if (!propSchema.writeOnly) {
234
+ filteredProps[key] = propSchema;
235
+ }
236
+ }
237
+ const filteredSchema = { ...schema, properties: filteredProps };
238
+ return schemaToZod(filteredSchema, ctx);
239
+ }
240
+ return schemaToZod(schema, ctx);
241
+ }
153
242
  function generateIndexFile(functionExports) {
154
243
  const lines = [];
155
244
  for (const name of functionExports) {
@@ -161,7 +250,8 @@ function generateIndexFile(functionExports) {
161
250
  function generateServiceFile(spec, opPairs, vars, flags) {
162
251
  const { name, pascalName, screamingName, displayName } = vars;
163
252
  const lines = [];
164
- const baseUrl = spec.baseUrl || 'https://api.example.com';
253
+ // Always import all error classes used in the switch statement
254
+ const allErrorClasses = new Set(Object.values(STATUS_TO_ERROR));
165
255
  if (flags.oauth) {
166
256
  lines.push("import { OAuth2Client } from '@pikku/core/oauth2'");
167
257
  lines.push("import type { TypedSecretService } from '#pikku/secrets/pikku-secrets.gen.js'");
@@ -169,15 +259,25 @@ function generateServiceFile(spec, opPairs, vars, flags) {
169
259
  else if (flags.secret) {
170
260
  lines.push(`import type { ${pascalName}Secrets } from './${name}.secret.js'`);
171
261
  }
172
- lines.push('');
173
- lines.push(`const BASE_URL = ${JSON.stringify(baseUrl)}`);
262
+ if (allErrorClasses.size > 0) {
263
+ lines.push(`import { ${[...allErrorClasses].sort().join(', ')} } from '@pikku/core/errors'`);
264
+ }
265
+ lines.push(`import type { TypedVariablesService } from '#pikku/variables/pikku-variables.gen.js'`);
174
266
  lines.push('');
175
267
  if (flags.oauth) {
268
+ // Use OAuth2 details from spec if available
269
+ const oauthScheme = Object.values(spec.securitySchemes).find((s) => s.type === 'oauth2');
270
+ const authUrl = oauthScheme?.flows?.authorizationUrl ??
271
+ 'https://example.com/oauth2/authorize';
272
+ const tokenUrl = oauthScheme?.flows?.tokenUrl ?? 'https://example.com/oauth2/token';
273
+ const scopes = oauthScheme?.flows?.scopes
274
+ ? Object.keys(oauthScheme.flows.scopes)
275
+ : ['read', 'write'];
176
276
  lines.push(`export const ${screamingName}_OAUTH2_CONFIG = {`);
177
277
  lines.push(` tokenSecretId: '${screamingName}_TOKENS',`);
178
- lines.push(` authorizationUrl: 'https://example.com/oauth2/authorize',`);
179
- lines.push(` tokenUrl: 'https://example.com/oauth2/token',`);
180
- lines.push(" scopes: ['read', 'write'],");
278
+ lines.push(` authorizationUrl: ${JSON.stringify(authUrl)},`);
279
+ lines.push(` tokenUrl: ${JSON.stringify(tokenUrl)},`);
280
+ lines.push(` scopes: ${JSON.stringify(scopes)},`);
181
281
  lines.push('}');
182
282
  lines.push('');
183
283
  }
@@ -185,19 +285,29 @@ function generateServiceFile(spec, opPairs, vars, flags) {
185
285
  const routes = {};
186
286
  for (const { parsed } of opPairs) {
187
287
  const key = `${parsed.method.toUpperCase()} ${parsed.path}`;
188
- routes[key] = {
288
+ const route = {
189
289
  path: parsed.pathParams.map((p) => p.name),
190
290
  query: parsed.queryParams.map((p) => p.name),
291
+ headers: parsed.headerParams.map((p) => p.name),
191
292
  };
293
+ if (parsed.errorResponses.length > 0) {
294
+ route.errors = {};
295
+ for (const err of parsed.errorResponses) {
296
+ route.errors[err.statusCode] = err.description;
297
+ }
298
+ }
299
+ routes[key] = route;
192
300
  }
193
- lines.push(`const ROUTES: Record<string, { path: string[], query: string[] }> = ${JSON.stringify(routes, null, 2)}`);
301
+ lines.push(`const ROUTES: Record<string, { path: string[], query: string[], headers: string[], errors?: Record<number, string> }> = ${JSON.stringify(routes, null, 2)}`);
194
302
  lines.push('');
195
303
  // Class declaration
196
304
  lines.push(`export class ${pascalName}Service {`);
305
+ lines.push(' private baseUrl: string');
197
306
  if (flags.oauth) {
198
307
  lines.push(' private oauth: OAuth2Client');
199
308
  lines.push('');
200
- lines.push(` constructor(secrets: TypedSecretService) {`);
309
+ lines.push(` constructor(secrets: TypedSecretService, variables: TypedVariablesService) {`);
310
+ lines.push(` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`);
201
311
  lines.push(' this.oauth = new OAuth2Client(');
202
312
  lines.push(` ${screamingName}_OAUTH2_CONFIG,`);
203
313
  lines.push(` '${screamingName}_APP_CREDENTIALS',`);
@@ -206,10 +316,19 @@ function generateServiceFile(spec, opPairs, vars, flags) {
206
316
  lines.push(' }');
207
317
  }
208
318
  else if (flags.secret) {
209
- lines.push(` constructor(private creds: ${pascalName}Secrets) {}`);
319
+ lines.push('');
320
+ lines.push(` constructor(private creds: ${pascalName}Secrets, variables: TypedVariablesService) {`);
321
+ lines.push(` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`);
322
+ lines.push(' }');
323
+ }
324
+ else {
325
+ lines.push('');
326
+ lines.push(` constructor(variables: TypedVariablesService) {`);
327
+ lines.push(` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`);
328
+ lines.push(' }');
210
329
  }
211
330
  lines.push('');
212
- // call() method — splits data into path/query/body using route map
331
+ // call() method — splits data into path/query/headers/body using route map
213
332
  lines.push(' async call<T>(');
214
333
  lines.push(" method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',");
215
334
  lines.push(' path: string,');
@@ -219,6 +338,9 @@ function generateServiceFile(spec, opPairs, vars, flags) {
219
338
  lines.push(' let endpoint = path');
220
339
  lines.push(' let body: Record<string, unknown> | undefined');
221
340
  lines.push(' const query: Record<string, string> = {}');
341
+ lines.push(' const headers: Record<string, string> = {');
342
+ lines.push(" 'Content-Type': 'application/json',");
343
+ lines.push(' }');
222
344
  lines.push('');
223
345
  lines.push(' if (data && route) {');
224
346
  lines.push(' // Interpolate path params');
@@ -233,17 +355,23 @@ function generateServiceFile(spec, opPairs, vars, flags) {
233
355
  lines.push(' query[param] = String(data[param])');
234
356
  lines.push(' }');
235
357
  lines.push(' }');
358
+ lines.push(' // Extract header params');
359
+ lines.push(' for (const param of route.headers) {');
360
+ lines.push(' if (data[param] !== undefined) {');
361
+ lines.push(' headers[param] = String(data[param])');
362
+ lines.push(' }');
363
+ lines.push(' }');
236
364
  lines.push(' // Everything else goes into body');
237
- lines.push(' const pathAndQuery = new Set([...route.path, ...route.query])');
365
+ lines.push(' const pathQueryHeaders = new Set([...route.path, ...route.query, ...route.headers])');
238
366
  lines.push(' const remaining = Object.fromEntries(');
239
- lines.push(' Object.entries(data).filter(([k]) => !pathAndQuery.has(k))');
367
+ lines.push(' Object.entries(data).filter(([k]) => !pathQueryHeaders.has(k))');
240
368
  lines.push(' )');
241
369
  lines.push(' if (Object.keys(remaining).length > 0) {');
242
370
  lines.push(' body = remaining');
243
371
  lines.push(' }');
244
372
  lines.push(' }');
245
373
  lines.push('');
246
- lines.push(' const url = new URL(`${BASE_URL}${endpoint}`)');
374
+ lines.push(' const url = new URL(`${this.baseUrl}${endpoint}`)');
247
375
  lines.push(' for (const [key, value] of Object.entries(query)) {');
248
376
  lines.push(' url.searchParams.set(key, value)');
249
377
  lines.push(' }');
@@ -251,38 +379,78 @@ function generateServiceFile(spec, opPairs, vars, flags) {
251
379
  if (flags.oauth) {
252
380
  lines.push(' const response = await this.oauth.request(url.toString(), {');
253
381
  lines.push(' method,');
254
- lines.push(" headers: { 'Content-Type': 'application/json' },");
382
+ lines.push(' headers,');
255
383
  lines.push(' body: body ? JSON.stringify(body) : undefined,');
256
384
  lines.push(' })');
257
385
  }
258
386
  else if (flags.secret) {
387
+ // Use apiKey details from spec if available
388
+ const apiKeyScheme = Object.values(spec.securitySchemes).find((s) => s.type === 'apiKey');
389
+ if (apiKeyScheme?.name && apiKeyScheme?.in === 'header') {
390
+ lines.push(` headers[${JSON.stringify(apiKeyScheme.name)}] = this.creds.apiKey`);
391
+ }
392
+ else {
393
+ lines.push(' headers.Authorization = `Bearer ${this.creds.apiKey}`');
394
+ }
395
+ lines.push('');
259
396
  lines.push(' const response = await fetch(url.toString(), {');
260
397
  lines.push(' method,');
261
- lines.push(' headers: {');
262
- lines.push(" 'Content-Type': 'application/json',");
263
- lines.push(' Authorization: `Bearer ${this.creds.apiKey}`,');
264
- lines.push(' },');
398
+ lines.push(' headers,');
265
399
  lines.push(' body: body ? JSON.stringify(body) : undefined,');
266
400
  lines.push(' })');
267
401
  }
268
402
  else {
269
403
  lines.push(' const response = await fetch(url.toString(), {');
270
404
  lines.push(' method,');
271
- lines.push(" headers: { 'Content-Type': 'application/json' },");
405
+ lines.push(' headers,');
272
406
  lines.push(' body: body ? JSON.stringify(body) : undefined,');
273
407
  lines.push(' })');
274
408
  }
275
409
  lines.push('');
276
410
  lines.push(' if (!response.ok) {');
277
411
  lines.push(' const errorText = await response.text()');
278
- lines.push(` throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)`);
412
+ lines.push(' const errorMessage = route?.errors?.[response.status] ?? errorText');
413
+ lines.push(' switch (response.status) {');
414
+ lines.push(' case 400: throw new BadRequestError(errorMessage)');
415
+ lines.push(' case 401: throw new UnauthorizedError(errorMessage)');
416
+ lines.push(' case 403: throw new ForbiddenError(errorMessage)');
417
+ lines.push(' case 404: throw new NotFoundError(errorMessage)');
418
+ lines.push(' case 405: throw new MethodNotAllowedError(errorMessage)');
419
+ lines.push(' case 409: throw new ConflictError(errorMessage)');
420
+ lines.push(' case 422: throw new UnprocessableContentError(errorMessage)');
421
+ lines.push(' case 429: throw new TooManyRequestsError(errorMessage)');
422
+ lines.push(' case 500: throw new InternalServerError(errorMessage)');
423
+ lines.push(` default: throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)`);
424
+ lines.push(' }');
279
425
  lines.push(' }');
280
426
  lines.push('');
281
427
  lines.push(' const text = await response.text()');
282
- lines.push(" if (!text) return {} as T");
428
+ lines.push(' if (!text) return {} as T');
283
429
  lines.push(' return JSON.parse(text) as T');
284
430
  lines.push(' }');
285
431
  lines.push('}');
286
432
  lines.push('');
287
433
  return lines.join('\n');
288
434
  }
435
+ function generateVariableFile(spec, vars) {
436
+ const { camelName, screamingName, displayName } = vars;
437
+ const serverUrls = spec.serverUrls.length > 0 ? spec.serverUrls : [];
438
+ const defaultUrl = serverUrls[0];
439
+ const lines = [];
440
+ lines.push("import { z } from 'zod'");
441
+ lines.push("import { wireVariable } from '@pikku/core/variable'");
442
+ lines.push('');
443
+ const schemaVarName = `${camelName}BaseUrlSchema`;
444
+ const urlsLiteral = serverUrls.map((u) => JSON.stringify(u)).join(', ');
445
+ lines.push(`export const ${schemaVarName} = z.enum([${urlsLiteral}]).default(${JSON.stringify(defaultUrl)})`);
446
+ lines.push('');
447
+ lines.push(`wireVariable({`);
448
+ lines.push(` name: '${screamingName}_BASE_URL',`);
449
+ lines.push(` displayName: '${displayName} Base URL',`);
450
+ lines.push(` description: 'The base URL for the ${displayName} API.',`);
451
+ lines.push(` variableId: '${screamingName}_BASE_URL',`);
452
+ lines.push(` schema: ${schemaVarName},`);
453
+ lines.push(`})`);
454
+ lines.push('');
455
+ return lines.join('\n');
456
+ }
@@ -1,4 +1,20 @@
1
1
  import type { OpenAPISchema } from './zod-codegen.js';
2
+ export interface ErrorResponse {
3
+ statusCode: number;
4
+ description: string;
5
+ }
6
+ export interface SecuritySchemeInfo {
7
+ type: 'oauth2' | 'http' | 'apiKey';
8
+ scheme?: string;
9
+ bearerFormat?: string;
10
+ name?: string;
11
+ in?: string;
12
+ flows?: {
13
+ authorizationUrl?: string;
14
+ tokenUrl?: string;
15
+ scopes?: Record<string, string>;
16
+ };
17
+ }
2
18
  export interface ParsedSpec {
3
19
  info: {
4
20
  title: string;
@@ -6,9 +22,12 @@ export interface ParsedSpec {
6
22
  description?: string;
7
23
  };
8
24
  baseUrl: string;
25
+ serverUrls: string[];
9
26
  authType: 'bearer' | 'oauth2' | 'apiKey' | 'none';
10
27
  operations: ParsedOperation[];
11
28
  componentSchemas: Record<string, OpenAPISchema>;
29
+ securitySchemes: Record<string, SecuritySchemeInfo>;
30
+ tagDescriptions: Record<string, string>;
12
31
  }
13
32
  export interface ParsedOperation {
14
33
  operationId?: string;
@@ -19,15 +38,21 @@ export interface ParsedOperation {
19
38
  tags: string[];
20
39
  pathParams: ParsedParam[];
21
40
  queryParams: ParsedParam[];
41
+ headerParams: ParsedParam[];
22
42
  requestBody?: OpenAPISchema;
43
+ requestBodyDescription?: string;
44
+ requestBodyRequired?: boolean;
23
45
  responseSchema?: OpenAPISchema;
24
46
  responseDescription?: string;
47
+ errorResponses: ErrorResponse[];
48
+ deprecated: boolean;
25
49
  }
26
50
  export interface ParsedParam {
27
51
  name: string;
28
52
  required: boolean;
29
53
  schema: OpenAPISchema;
30
54
  description?: string;
55
+ example?: unknown;
31
56
  }
32
57
  /**
33
58
  * Read and parse an OpenAPI spec from a file path.