@pikku/cli 0.12.0 → 0.12.2
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.
- package/cli.schema.json +1 -1
- package/console-app/assets/index-DRJQtv3c.js +676 -0
- package/console-app/index.html +6 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +30 -3
- package/dist/.pikku/agent/pikku-agent-types.gen.js +13 -0
- package/dist/.pikku/agent/pikku-agent-wirings-meta.gen.js +2 -2
- package/dist/.pikku/agent/pikku-agent-wirings.gen.d.ts +1 -1
- package/dist/.pikku/agent/pikku-agent-wirings.gen.js +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/channel/pikku-channels-meta.gen.js +2 -2
- package/dist/.pikku/channel/pikku-channels.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channels.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +77 -3
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +2 -2
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +280 -10
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +14 -11
- package/dist/.pikku/function/pikku-function-types.gen.js +25 -13
- package/dist/.pikku/function/pikku-functions-meta.gen.js +2 -2
- package/dist/.pikku/function/pikku-functions-meta.gen.json +426 -93
- package/dist/.pikku/function/pikku-functions.gen.js +5 -3
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +2 -2
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-wirings-meta.gen.js +2 -2
- package/dist/.pikku/mcp/pikku-mcp-wirings.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-wirings.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +2 -2
- package/dist/.pikku/pikku-services.gen.d.ts +2 -1
- package/dist/.pikku/pikku-services.gen.js +1 -0
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/pikku-websocket.gen.d.ts +1 -1
- package/dist/.pikku/pikku-websocket.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +2 -2
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-remote-rpc-workers.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +2 -2
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +16 -2
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/scheduler/pikku-schedulers-wirings-meta.gen.js +2 -2
- package/dist/.pikku/scheduler/pikku-schedulers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-schedulers-wirings.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +15 -3
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuGatewayOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuNewAddonInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuNewFunctionInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuNewMiddlewareInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuNewPermissionInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuNewWiringInput.schema.json +1 -0
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +2 -2
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/src/cli.wiring.js +236 -9
- package/dist/src/functions/commands/all.js +13 -8
- package/dist/src/functions/commands/enable.d.ts +4 -0
- package/dist/src/functions/commands/enable.js +39 -0
- package/dist/src/functions/commands/info.d.ts +9 -0
- package/dist/src/functions/commands/info.js +283 -0
- package/dist/src/functions/commands/new-addon.d.ts +34 -0
- package/dist/src/functions/commands/new-addon.js +636 -0
- package/dist/src/functions/commands/new-function.d.ts +10 -0
- package/dist/src/functions/commands/new-function.js +79 -0
- package/dist/src/functions/commands/new-middleware.d.ts +10 -0
- package/dist/src/functions/commands/new-middleware.js +48 -0
- package/dist/src/functions/commands/new-permission.d.ts +10 -0
- package/dist/src/functions/commands/new-permission.js +45 -0
- package/dist/src/functions/commands/new-wiring.d.ts +10 -0
- package/dist/src/functions/commands/new-wiring.js +102 -0
- package/dist/src/functions/commands/pikku-command-bootstrap.js +11 -40
- package/dist/src/functions/commands/versions-check.js +85 -3
- package/dist/src/functions/commands/versions-update.js +1 -1
- package/dist/src/functions/runtimes/fetch/index.js +2 -1
- package/dist/src/functions/runtimes/nextjs/pikku-command-nextjs.js +4 -1
- package/dist/src/functions/runtimes/nextjs/serialize-nextjs-backend-wrapper.js +0 -4
- package/dist/src/functions/runtimes/websocket/pikku-command-websocket-typed.js +2 -1
- package/dist/src/functions/wirings/ai-agent/pikku-command-ai-agent-types.js +3 -2
- package/dist/src/functions/wirings/ai-agent/pikku-command-ai-agent.js +5 -5
- package/dist/src/functions/wirings/ai-agent/pikku-command-public-agent.js +3 -5
- package/dist/src/functions/wirings/ai-agent/serialize-agent-map.d.ts +1 -1
- package/dist/src/functions/wirings/ai-agent/serialize-ai-agent-types.d.ts +1 -1
- package/dist/src/functions/wirings/ai-agent/serialize-ai-agent-types.js +48 -3
- package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +30 -52
- package/dist/src/functions/wirings/channels/pikku-channels.js +1 -1
- package/dist/src/functions/wirings/channels/pikku-command-channel-types.js +2 -2
- package/dist/src/functions/wirings/channels/pikku-command-channels.js +1 -1
- package/dist/src/functions/wirings/channels/serialize-typed-channel-map.d.ts +4 -4
- package/dist/src/functions/wirings/cli/pikku-command-cli.js +1 -1
- package/dist/src/functions/wirings/cli/serialize-channel-cli-client.d.ts +2 -2
- package/dist/src/functions/wirings/cli/serialize-channel-cli.d.ts +1 -1
- package/dist/src/functions/wirings/cli/serialize-local-cli-bootstrap.d.ts +1 -1
- package/dist/src/functions/wirings/console/pikku-command-console-functions.js +4 -5
- package/dist/src/functions/wirings/console/pikku-command-nodes-meta.js +2 -2
- package/dist/src/functions/wirings/console/serialize-console-functions.d.ts +1 -1
- package/dist/src/functions/wirings/console/serialize-console-functions.js +18 -157
- package/dist/src/functions/wirings/functions/pikku-command-addon-types.d.ts +1 -0
- package/dist/src/functions/wirings/functions/pikku-command-addon-types.js +33 -0
- package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +1 -1
- package/dist/src/functions/wirings/functions/pikku-command-function-types.js +4 -4
- package/dist/src/functions/wirings/functions/pikku-command-functions.js +8 -14
- package/dist/src/functions/wirings/functions/schemas.js +1 -1
- package/dist/src/functions/wirings/functions/serialize-addon-types.d.ts +1 -0
- package/dist/src/functions/wirings/functions/{serialize-external-types.js → serialize-addon-types.js} +16 -15
- package/dist/src/functions/wirings/functions/serialize-function-imports.d.ts +3 -3
- package/dist/src/functions/wirings/functions/serialize-function-imports.js +3 -3
- package/dist/src/functions/wirings/functions/serialize-function-types.js +28 -14
- package/dist/src/functions/wirings/functions/serialize-pikku-types-hub.d.ts +1 -1
- package/dist/src/functions/wirings/functions/serialize-pikku-types-hub.js +2 -2
- package/dist/src/functions/wirings/gateway/pikku-command-gateway.d.ts +1 -0
- package/dist/src/functions/wirings/gateway/pikku-command-gateway.js +22 -0
- package/dist/src/functions/wirings/http/pikku-command-http-routes.js +1 -1
- package/dist/src/functions/wirings/http/pikku-http-routes.js +1 -1
- package/dist/src/functions/wirings/http/serialize-typed-http-map.d.ts +3 -3
- package/dist/src/functions/wirings/mcp/pikku-command-mcp-json.js +1 -1
- package/dist/src/functions/wirings/mcp/pikku-command-mcp.js +1 -1
- package/dist/src/functions/wirings/package/pikku-command-package.js +5 -5
- package/dist/src/functions/wirings/package/serialize-package.js +2 -2
- package/dist/src/functions/wirings/permissions/pikku-command-permissions.js +0 -5
- package/dist/src/functions/wirings/queue/pikku-command-queue-service.js +2 -1
- package/dist/src/functions/wirings/queue/serialize-queue-map.d.ts +2 -2
- package/dist/src/functions/wirings/queue/serialize-queue-meta.d.ts +1 -1
- package/dist/src/functions/wirings/rpc/pikku-command-public-rpc.js +3 -5
- package/dist/src/functions/wirings/rpc/pikku-command-remote-rpc.js +3 -5
- package/dist/src/functions/wirings/rpc/pikku-command-rpc-client.js +2 -1
- package/dist/src/functions/wirings/rpc/pikku-command-rpc-map.js +6 -6
- package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +2 -4
- package/dist/src/functions/wirings/rpc/serialize-public-rpc.js +25 -16
- package/dist/src/functions/wirings/rpc/serialize-rpc-wrapper.js +43 -7
- package/dist/src/functions/wirings/rpc/serialize-typed-rpc-map.d.ts +8 -3
- package/dist/src/functions/wirings/rpc/serialize-typed-rpc-map.js +41 -31
- package/dist/src/functions/wirings/scheduler/serialize-scheduler-meta.d.ts +1 -1
- package/dist/src/functions/wirings/secrets/serialize-secrets-types.d.ts +2 -2
- package/dist/src/functions/wirings/triggers/serialize-trigger-meta.d.ts +1 -1
- package/dist/src/functions/wirings/triggers/serialize-trigger-meta.js +2 -2
- package/dist/src/functions/wirings/variables/serialize-variables-types.d.ts +2 -2
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +5 -12
- package/dist/src/functions/wirings/workflow/serialize-workflow-map.d.ts +2 -2
- package/dist/src/functions/wirings/workflow/serialize-workflow-meta.js +2 -2
- package/dist/src/services/cli-logger-forwarder.service.d.ts +4 -3
- package/dist/src/services/cli-logger.service.d.ts +3 -2
- package/dist/src/services.d.ts +4 -3
- package/dist/src/services.js +14 -3
- package/dist/src/utils/check-required-types.d.ts +1 -1
- package/dist/src/utils/contract-versions.d.ts +1 -1
- package/dist/src/utils/file-writer.d.ts +6 -1
- package/dist/src/utils/file-writer.js +14 -1
- package/dist/src/utils/generate-bootstrap-file.d.ts +2 -2
- package/dist/src/utils/openapi/codegen.d.ts +19 -0
- package/dist/src/utils/openapi/codegen.js +288 -0
- package/dist/src/utils/openapi/naming.d.ts +30 -0
- package/dist/src/utils/openapi/naming.js +167 -0
- package/dist/src/utils/openapi/parse-openapi.d.ts +36 -0
- package/dist/src/utils/openapi/parse-openapi.js +196 -0
- package/dist/src/utils/openapi/zod-codegen.d.ts +53 -0
- package/dist/src/utils/openapi/zod-codegen.js +251 -0
- package/dist/src/utils/pikku-cli-config.d.ts +2 -2
- package/dist/src/utils/pikku-cli-config.js +51 -30
- package/dist/src/utils/pikku-files-and-methods.d.ts +1 -1
- package/dist/src/utils/pikku-files-and-methods.js +1 -1
- package/dist/src/utils/serialize-import-map.d.ts +2 -2
- package/dist/src/utils/serialize-import-map.js +1 -1
- package/dist/src/utils/serialize-meta-ts.js +1 -1
- package/dist/src/utils/serialize-schemas.d.ts +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/console-app/assets/index-C19L3UJu.js +0 -637
- package/dist/src/functions/wirings/functions/pikku-command-external-types.d.ts +0 -1
- package/dist/src/functions/wirings/functions/pikku-command-external-types.js +0 -33
- package/dist/src/functions/wirings/functions/serialize-external-types.d.ts +0 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { schemaToZod, schemaVarName, createContext, } from './zod-codegen.js';
|
|
2
|
+
import { generateOperationNames, detectCommonPrefix, } from './naming.js';
|
|
3
|
+
const GENERIC_SUMMARIES = new Set([
|
|
4
|
+
'index', 'show', 'create', 'update', 'destroy', 'delete', 'list',
|
|
5
|
+
]);
|
|
6
|
+
function capitalize(str) {
|
|
7
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
8
|
+
}
|
|
9
|
+
function humanDescription(named, parsed) {
|
|
10
|
+
if (parsed.responseDescription) {
|
|
11
|
+
return capitalize(parsed.responseDescription);
|
|
12
|
+
}
|
|
13
|
+
const summary = parsed.summary?.trim();
|
|
14
|
+
if (summary && !GENERIC_SUMMARIES.has(summary.toLowerCase())) {
|
|
15
|
+
return capitalize(summary);
|
|
16
|
+
}
|
|
17
|
+
if (parsed.description && !GENERIC_SUMMARIES.has(parsed.description.trim().toLowerCase())) {
|
|
18
|
+
return capitalize(parsed.description.trim());
|
|
19
|
+
}
|
|
20
|
+
const words = named.functionName
|
|
21
|
+
.replace(/([A-Z])/g, ' $1')
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
return capitalize(words);
|
|
25
|
+
}
|
|
26
|
+
export function generateAddonFromOpenAPI(spec, vars, flags) {
|
|
27
|
+
const files = {};
|
|
28
|
+
const { name } = vars;
|
|
29
|
+
// Build context for Zod codegen with component schema refs
|
|
30
|
+
const schemaRefs = new Map();
|
|
31
|
+
for (const schemaName of Object.keys(spec.componentSchemas)) {
|
|
32
|
+
schemaRefs.set(schemaName, schemaVarName(schemaName));
|
|
33
|
+
}
|
|
34
|
+
const ctx = createContext(schemaRefs);
|
|
35
|
+
// Generate operation names
|
|
36
|
+
const paths = spec.operations.map((op) => op.path);
|
|
37
|
+
const commonPrefix = detectCommonPrefix(paths);
|
|
38
|
+
const namedOps = generateOperationNames(spec.operations.map((op) => ({
|
|
39
|
+
method: op.method,
|
|
40
|
+
path: op.path,
|
|
41
|
+
operationId: op.operationId,
|
|
42
|
+
})), commonPrefix);
|
|
43
|
+
// Pair named operations with their parsed data
|
|
44
|
+
const opPairs = namedOps.map((named, i) => ({ named, parsed: spec.operations[i] }));
|
|
45
|
+
// Generate types file with shared schemas (only if there are any)
|
|
46
|
+
if (Object.keys(spec.componentSchemas).length > 0) {
|
|
47
|
+
files[`src/${name}.types.ts`] = generateTypesFile(spec, ctx);
|
|
48
|
+
}
|
|
49
|
+
// Generate function files
|
|
50
|
+
const functionExports = [];
|
|
51
|
+
for (const { named, parsed } of opPairs) {
|
|
52
|
+
const funcCtx = createContext(schemaRefs);
|
|
53
|
+
const funcCode = generateFunctionFile(named, parsed, vars, funcCtx);
|
|
54
|
+
files[`src/functions/${named.functionName}.function.ts`] = funcCode;
|
|
55
|
+
functionExports.push(named.functionName);
|
|
56
|
+
}
|
|
57
|
+
// Generate index.ts with all exports
|
|
58
|
+
files['src/index.ts'] = generateIndexFile(functionExports);
|
|
59
|
+
// Generate typed API service class with route map
|
|
60
|
+
files[`src/${name}-api.service.ts`] = generateServiceFile(spec, opPairs, vars, flags);
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
function generateTypesFile(spec, ctx) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
lines.push("import { z } from 'zod'");
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push(`// Shared schemas from ${spec.info.title} v${spec.info.version}`);
|
|
68
|
+
lines.push('');
|
|
69
|
+
for (const [name, schema] of Object.entries(spec.componentSchemas)) {
|
|
70
|
+
const varName = schemaVarName(name);
|
|
71
|
+
const zodCode = schemaToZod(schema, ctx);
|
|
72
|
+
lines.push(`export const ${varName} = ${zodCode}`);
|
|
73
|
+
lines.push(`export type ${name} = z.infer<typeof ${varName}>`);
|
|
74
|
+
lines.push('');
|
|
75
|
+
}
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
function generateFunctionFile(named, parsed, vars, ctx) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
const { camelName } = vars;
|
|
81
|
+
const hasInput = parsed.pathParams.length > 0 ||
|
|
82
|
+
parsed.queryParams.length > 0 ||
|
|
83
|
+
parsed.requestBody;
|
|
84
|
+
const pascalName = named.functionName.charAt(0).toUpperCase() + named.functionName.slice(1);
|
|
85
|
+
const inputName = `${pascalName}Input`;
|
|
86
|
+
const outputName = `${pascalName}Output`;
|
|
87
|
+
lines.push("import { z } from 'zod'");
|
|
88
|
+
lines.push("import { pikkuSessionlessFunc } from '#pikku'");
|
|
89
|
+
lines.push('');
|
|
90
|
+
// Build Input schema (exported for pikku schema discovery)
|
|
91
|
+
if (hasInput) {
|
|
92
|
+
const inputCode = buildInputSchema(parsed, ctx);
|
|
93
|
+
lines.push(`export const ${inputName} = ${inputCode}`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
}
|
|
96
|
+
// Build Output schema (exported for pikku schema discovery)
|
|
97
|
+
if (parsed.responseSchema) {
|
|
98
|
+
const outputCode = schemaToZod(parsed.responseSchema, ctx);
|
|
99
|
+
lines.push(`export const ${outputName} = ${outputCode}`);
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
const description = humanDescription(named, parsed);
|
|
103
|
+
const method = parsed.method.toUpperCase();
|
|
104
|
+
const funcConfig = [];
|
|
105
|
+
funcConfig.push(` description: ${JSON.stringify(description)},`);
|
|
106
|
+
if (hasInput)
|
|
107
|
+
funcConfig.push(` input: ${inputName},`);
|
|
108
|
+
if (parsed.responseSchema)
|
|
109
|
+
funcConfig.push(` output: ${outputName},`);
|
|
110
|
+
const funcParams = hasInput
|
|
111
|
+
? `{ ${camelName} }, data`
|
|
112
|
+
: `{ ${camelName} }`;
|
|
113
|
+
const returnCast = parsed.responseSchema ? ' as any' : '';
|
|
114
|
+
funcConfig.push(` func: async (${funcParams}) => {`, ` return ${camelName}.call('${method}', '${parsed.path}'${hasInput ? ', data' : ''})${returnCast}`, ' },');
|
|
115
|
+
lines.push(`export const ${named.functionName} = pikkuSessionlessFunc({`);
|
|
116
|
+
lines.push(funcConfig.join('\n'));
|
|
117
|
+
lines.push('})');
|
|
118
|
+
lines.push('');
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
function buildInputSchema(parsed, ctx) {
|
|
122
|
+
const props = [];
|
|
123
|
+
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},`);
|
|
129
|
+
}
|
|
130
|
+
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},`);
|
|
136
|
+
}
|
|
137
|
+
if (parsed.requestBody) {
|
|
138
|
+
if (parsed.requestBody.properties) {
|
|
139
|
+
const requiredSet = new Set(parsed.requestBody.required ?? []);
|
|
140
|
+
for (const [key, propSchema] of Object.entries(parsed.requestBody.properties)) {
|
|
141
|
+
const isOptional = !requiredSet.has(key);
|
|
142
|
+
const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional });
|
|
143
|
+
props.push(` ${key}: ${zodCode},`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const bodyZod = schemaToZod(parsed.requestBody, ctx);
|
|
148
|
+
props.push(` body: ${bodyZod},`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return `z.object({\n${props.join('\n')}\n})`;
|
|
152
|
+
}
|
|
153
|
+
function generateIndexFile(functionExports) {
|
|
154
|
+
const lines = [];
|
|
155
|
+
for (const name of functionExports) {
|
|
156
|
+
lines.push(`export { ${name} } from './functions/${name}.function.js'`);
|
|
157
|
+
}
|
|
158
|
+
lines.push('');
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
function generateServiceFile(spec, opPairs, vars, flags) {
|
|
162
|
+
const { name, pascalName, screamingName, displayName } = vars;
|
|
163
|
+
const lines = [];
|
|
164
|
+
const baseUrl = spec.baseUrl || 'https://api.example.com';
|
|
165
|
+
if (flags.oauth) {
|
|
166
|
+
lines.push("import { OAuth2Client } from '@pikku/core/oauth2'");
|
|
167
|
+
lines.push("import type { TypedSecretService } from '#pikku/secrets/pikku-secrets.gen.js'");
|
|
168
|
+
}
|
|
169
|
+
else if (flags.secret) {
|
|
170
|
+
lines.push(`import type { ${pascalName}Secrets } from './${name}.secret.js'`);
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
lines.push(`const BASE_URL = ${JSON.stringify(baseUrl)}`);
|
|
174
|
+
lines.push('');
|
|
175
|
+
if (flags.oauth) {
|
|
176
|
+
lines.push(`export const ${screamingName}_OAUTH2_CONFIG = {`);
|
|
177
|
+
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'],");
|
|
181
|
+
lines.push('}');
|
|
182
|
+
lines.push('');
|
|
183
|
+
}
|
|
184
|
+
// Generate route map from parsed operations
|
|
185
|
+
const routes = {};
|
|
186
|
+
for (const { parsed } of opPairs) {
|
|
187
|
+
const key = `${parsed.method.toUpperCase()} ${parsed.path}`;
|
|
188
|
+
routes[key] = {
|
|
189
|
+
path: parsed.pathParams.map((p) => p.name),
|
|
190
|
+
query: parsed.queryParams.map((p) => p.name),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
lines.push(`const ROUTES: Record<string, { path: string[], query: string[] }> = ${JSON.stringify(routes, null, 2)}`);
|
|
194
|
+
lines.push('');
|
|
195
|
+
// Class declaration
|
|
196
|
+
lines.push(`export class ${pascalName}Service {`);
|
|
197
|
+
if (flags.oauth) {
|
|
198
|
+
lines.push(' private oauth: OAuth2Client');
|
|
199
|
+
lines.push('');
|
|
200
|
+
lines.push(` constructor(secrets: TypedSecretService) {`);
|
|
201
|
+
lines.push(' this.oauth = new OAuth2Client(');
|
|
202
|
+
lines.push(` ${screamingName}_OAUTH2_CONFIG,`);
|
|
203
|
+
lines.push(` '${screamingName}_APP_CREDENTIALS',`);
|
|
204
|
+
lines.push(' secrets');
|
|
205
|
+
lines.push(' )');
|
|
206
|
+
lines.push(' }');
|
|
207
|
+
}
|
|
208
|
+
else if (flags.secret) {
|
|
209
|
+
lines.push(` constructor(private creds: ${pascalName}Secrets) {}`);
|
|
210
|
+
}
|
|
211
|
+
lines.push('');
|
|
212
|
+
// call() method — splits data into path/query/body using route map
|
|
213
|
+
lines.push(' async call<T>(');
|
|
214
|
+
lines.push(" method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',");
|
|
215
|
+
lines.push(' path: string,');
|
|
216
|
+
lines.push(' data?: Record<string, unknown>');
|
|
217
|
+
lines.push(' ): Promise<T> {');
|
|
218
|
+
lines.push(' const route = ROUTES[`${method} ${path}`]');
|
|
219
|
+
lines.push(' let endpoint = path');
|
|
220
|
+
lines.push(' let body: Record<string, unknown> | undefined');
|
|
221
|
+
lines.push(' const query: Record<string, string> = {}');
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push(' if (data && route) {');
|
|
224
|
+
lines.push(' // Interpolate path params');
|
|
225
|
+
lines.push(' for (const param of route.path) {');
|
|
226
|
+
lines.push(' if (data[param] !== undefined) {');
|
|
227
|
+
lines.push(' endpoint = endpoint.replace(`{${param}}`, String(data[param]))');
|
|
228
|
+
lines.push(' }');
|
|
229
|
+
lines.push(' }');
|
|
230
|
+
lines.push(' // Extract query params');
|
|
231
|
+
lines.push(' for (const param of route.query) {');
|
|
232
|
+
lines.push(' if (data[param] !== undefined) {');
|
|
233
|
+
lines.push(' query[param] = String(data[param])');
|
|
234
|
+
lines.push(' }');
|
|
235
|
+
lines.push(' }');
|
|
236
|
+
lines.push(' // Everything else goes into body');
|
|
237
|
+
lines.push(' const pathAndQuery = new Set([...route.path, ...route.query])');
|
|
238
|
+
lines.push(' const remaining = Object.fromEntries(');
|
|
239
|
+
lines.push(' Object.entries(data).filter(([k]) => !pathAndQuery.has(k))');
|
|
240
|
+
lines.push(' )');
|
|
241
|
+
lines.push(' if (Object.keys(remaining).length > 0) {');
|
|
242
|
+
lines.push(' body = remaining');
|
|
243
|
+
lines.push(' }');
|
|
244
|
+
lines.push(' }');
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(' const url = new URL(`${BASE_URL}${endpoint}`)');
|
|
247
|
+
lines.push(' for (const [key, value] of Object.entries(query)) {');
|
|
248
|
+
lines.push(' url.searchParams.set(key, value)');
|
|
249
|
+
lines.push(' }');
|
|
250
|
+
lines.push('');
|
|
251
|
+
if (flags.oauth) {
|
|
252
|
+
lines.push(' const response = await this.oauth.request(url.toString(), {');
|
|
253
|
+
lines.push(' method,');
|
|
254
|
+
lines.push(" headers: { 'Content-Type': 'application/json' },");
|
|
255
|
+
lines.push(' body: body ? JSON.stringify(body) : undefined,');
|
|
256
|
+
lines.push(' })');
|
|
257
|
+
}
|
|
258
|
+
else if (flags.secret) {
|
|
259
|
+
lines.push(' const response = await fetch(url.toString(), {');
|
|
260
|
+
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(' },');
|
|
265
|
+
lines.push(' body: body ? JSON.stringify(body) : undefined,');
|
|
266
|
+
lines.push(' })');
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
lines.push(' const response = await fetch(url.toString(), {');
|
|
270
|
+
lines.push(' method,');
|
|
271
|
+
lines.push(" headers: { 'Content-Type': 'application/json' },");
|
|
272
|
+
lines.push(' body: body ? JSON.stringify(body) : undefined,');
|
|
273
|
+
lines.push(' })');
|
|
274
|
+
}
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push(' if (!response.ok) {');
|
|
277
|
+
lines.push(' const errorText = await response.text()');
|
|
278
|
+
lines.push(` throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)`);
|
|
279
|
+
lines.push(' }');
|
|
280
|
+
lines.push('');
|
|
281
|
+
lines.push(' const text = await response.text()');
|
|
282
|
+
lines.push(" if (!text) return {} as T");
|
|
283
|
+
lines.push(' return JSON.parse(text) as T');
|
|
284
|
+
lines.push(' }');
|
|
285
|
+
lines.push('}');
|
|
286
|
+
lines.push('');
|
|
287
|
+
return lines.join('\n');
|
|
288
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives function/method names from HTTP method + path.
|
|
3
|
+
* Uses operationId when available, otherwise generates from path segments.
|
|
4
|
+
*/
|
|
5
|
+
export declare function singularize(word: string): string;
|
|
6
|
+
export declare function toCamelCase(str: string): string;
|
|
7
|
+
export declare function toPascalCase(str: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Strip common API prefix from paths (e.g. /api/v2/).
|
|
10
|
+
* Uses majority voting — a prefix shared by >75% of paths is stripped,
|
|
11
|
+
* so a few outlier paths (e.g. /oauth/...) don't break detection.
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectCommonPrefix(paths: string[]): string;
|
|
14
|
+
interface OperationForNaming {
|
|
15
|
+
method: string;
|
|
16
|
+
path: string;
|
|
17
|
+
operationId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface NamedOperation {
|
|
20
|
+
method: string;
|
|
21
|
+
path: string;
|
|
22
|
+
functionName: string;
|
|
23
|
+
methodName: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate function names for a list of operations.
|
|
27
|
+
* Handles collision detection and auto-deduplication.
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateOperationNames(operations: OperationForNaming[], commonPrefix: string): NamedOperation[];
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives function/method names from HTTP method + path.
|
|
3
|
+
* Uses operationId when available, otherwise generates from path segments.
|
|
4
|
+
*/
|
|
5
|
+
const IRREGULAR_PLURALS = {
|
|
6
|
+
addresses: 'address',
|
|
7
|
+
statuses: 'status',
|
|
8
|
+
indices: 'index',
|
|
9
|
+
analyses: 'analysis',
|
|
10
|
+
quizzes: 'quiz',
|
|
11
|
+
matrices: 'matrix',
|
|
12
|
+
vertices: 'vertex',
|
|
13
|
+
aliases: 'alias',
|
|
14
|
+
buses: 'bus',
|
|
15
|
+
};
|
|
16
|
+
export function singularize(word) {
|
|
17
|
+
const lower = word.toLowerCase();
|
|
18
|
+
if (IRREGULAR_PLURALS[lower]) {
|
|
19
|
+
const singular = IRREGULAR_PLURALS[lower];
|
|
20
|
+
return word[0] === word[0].toUpperCase()
|
|
21
|
+
? singular.charAt(0).toUpperCase() + singular.slice(1)
|
|
22
|
+
: singular;
|
|
23
|
+
}
|
|
24
|
+
if (lower.endsWith('ies') && lower.length > 4) {
|
|
25
|
+
return word.slice(0, -3) + 'y';
|
|
26
|
+
}
|
|
27
|
+
// -shes, -ches, -xes, -zes, -sses → drop "es"
|
|
28
|
+
if (lower.endsWith('shes') || lower.endsWith('ches') || lower.endsWith('xes') || lower.endsWith('zes') || lower.endsWith('sses')) {
|
|
29
|
+
return word.slice(0, -2);
|
|
30
|
+
}
|
|
31
|
+
// General: drop trailing "s" (covers courses→course, products→product, etc.)
|
|
32
|
+
if (lower.endsWith('s') && !lower.endsWith('ss') && !lower.endsWith('us') && lower.length > 2) {
|
|
33
|
+
return word.slice(0, -1);
|
|
34
|
+
}
|
|
35
|
+
return word;
|
|
36
|
+
}
|
|
37
|
+
export function toCamelCase(str) {
|
|
38
|
+
return str
|
|
39
|
+
.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
|
|
40
|
+
.replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
export function toPascalCase(str) {
|
|
43
|
+
const camel = toCamelCase(str);
|
|
44
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Strip common API prefix from paths (e.g. /api/v2/).
|
|
48
|
+
* Uses majority voting — a prefix shared by >75% of paths is stripped,
|
|
49
|
+
* so a few outlier paths (e.g. /oauth/...) don't break detection.
|
|
50
|
+
*/
|
|
51
|
+
export function detectCommonPrefix(paths) {
|
|
52
|
+
if (paths.length === 0)
|
|
53
|
+
return '';
|
|
54
|
+
const segments = paths.map((p) => p.replace(/^\//, '').split('/'));
|
|
55
|
+
const threshold = Math.ceil(paths.length * 0.75);
|
|
56
|
+
// Find the longest prefix shared by at least `threshold` paths
|
|
57
|
+
let prefixLen = 0;
|
|
58
|
+
for (let i = 0; i < 10; i++) {
|
|
59
|
+
// Count how many paths have the same segment at position i as the first path
|
|
60
|
+
const candidateSeg = segments[0][i];
|
|
61
|
+
if (!candidateSeg || candidateSeg.startsWith('{'))
|
|
62
|
+
break;
|
|
63
|
+
const count = segments.filter((s) => s.length > i + 1 && s[i] === candidateSeg).length;
|
|
64
|
+
if (count >= threshold) {
|
|
65
|
+
prefixLen = i + 1;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (prefixLen === 0)
|
|
72
|
+
return '';
|
|
73
|
+
return '/' + segments[0].slice(0, prefixLen).join('/') + '/';
|
|
74
|
+
}
|
|
75
|
+
/** Convert operationId to camelCase function name */
|
|
76
|
+
function fromOperationId(operationId) {
|
|
77
|
+
// operationId might be snake_case, camelCase, PascalCase, or kebab-case
|
|
78
|
+
return toCamelCase(operationId
|
|
79
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
80
|
+
.replace(/_+/g, '_')
|
|
81
|
+
.replace(/^_|_$/g, ''));
|
|
82
|
+
}
|
|
83
|
+
const METHOD_PREFIXES = {
|
|
84
|
+
get: 'get',
|
|
85
|
+
post: 'create',
|
|
86
|
+
put: 'update',
|
|
87
|
+
patch: 'update',
|
|
88
|
+
delete: 'delete',
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Generate function names for a list of operations.
|
|
92
|
+
* Handles collision detection and auto-deduplication.
|
|
93
|
+
*/
|
|
94
|
+
export function generateOperationNames(operations, commonPrefix) {
|
|
95
|
+
const results = [];
|
|
96
|
+
const usedNames = new Map();
|
|
97
|
+
for (const op of operations) {
|
|
98
|
+
let name;
|
|
99
|
+
if (op.operationId) {
|
|
100
|
+
name = fromOperationId(op.operationId);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
name = deriveNameFromPath(op.method, op.path, commonPrefix);
|
|
104
|
+
}
|
|
105
|
+
// Handle collisions
|
|
106
|
+
const existing = usedNames.get(name) ?? 0;
|
|
107
|
+
if (existing > 0) {
|
|
108
|
+
usedNames.set(name, existing + 1);
|
|
109
|
+
name = `${name}${existing + 1}`;
|
|
110
|
+
}
|
|
111
|
+
usedNames.set(name, (usedNames.get(name) ?? 0) || 1);
|
|
112
|
+
results.push({
|
|
113
|
+
method: op.method,
|
|
114
|
+
path: op.path,
|
|
115
|
+
functionName: name,
|
|
116
|
+
methodName: name,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
function deriveNameFromPath(method, path, commonPrefix) {
|
|
122
|
+
// Strip common prefix
|
|
123
|
+
let cleanPath = path;
|
|
124
|
+
if (commonPrefix && cleanPath.startsWith(commonPrefix)) {
|
|
125
|
+
cleanPath = '/' + cleanPath.slice(commonPrefix.length);
|
|
126
|
+
}
|
|
127
|
+
const segments = cleanPath
|
|
128
|
+
.replace(/^\//, '')
|
|
129
|
+
.split('/')
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
const methodLower = method.toLowerCase();
|
|
132
|
+
const prefix = METHOD_PREFIXES[methodLower] || methodLower;
|
|
133
|
+
// Separate param and non-param segments
|
|
134
|
+
const nonParams = [];
|
|
135
|
+
let hasTrailingParam = false;
|
|
136
|
+
for (let i = 0; i < segments.length; i++) {
|
|
137
|
+
if (segments[i].startsWith('{')) {
|
|
138
|
+
if (i === segments.length - 1) {
|
|
139
|
+
hasTrailingParam = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
nonParams.push(segments[i]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (nonParams.length === 0) {
|
|
147
|
+
return prefix;
|
|
148
|
+
}
|
|
149
|
+
// For GET with trailing param: singularize the last non-param segment
|
|
150
|
+
// For GET without trailing param: keep plural (list)
|
|
151
|
+
// For POST without trailing param: use prefix (create)
|
|
152
|
+
const parts = nonParams.map((seg, i) => {
|
|
153
|
+
const isLast = i === nonParams.length - 1;
|
|
154
|
+
let word = seg;
|
|
155
|
+
if (isLast && hasTrailingParam) {
|
|
156
|
+
word = singularize(word);
|
|
157
|
+
}
|
|
158
|
+
// For GET on collection (no trailing param), use "list" prefix instead of "get"
|
|
159
|
+
return toPascalCase(word);
|
|
160
|
+
});
|
|
161
|
+
// For GET on collection, use "list" instead of "get"
|
|
162
|
+
let finalPrefix = prefix;
|
|
163
|
+
if (methodLower === 'get' && !hasTrailingParam) {
|
|
164
|
+
finalPrefix = 'list';
|
|
165
|
+
}
|
|
166
|
+
return finalPrefix + parts.join('');
|
|
167
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OpenAPISchema } from './zod-codegen.js';
|
|
2
|
+
export interface ParsedSpec {
|
|
3
|
+
info: {
|
|
4
|
+
title: string;
|
|
5
|
+
version: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
};
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
authType: 'bearer' | 'oauth2' | 'apiKey' | 'none';
|
|
10
|
+
operations: ParsedOperation[];
|
|
11
|
+
componentSchemas: Record<string, OpenAPISchema>;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedOperation {
|
|
14
|
+
operationId?: string;
|
|
15
|
+
method: string;
|
|
16
|
+
path: string;
|
|
17
|
+
summary?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
tags: string[];
|
|
20
|
+
pathParams: ParsedParam[];
|
|
21
|
+
queryParams: ParsedParam[];
|
|
22
|
+
requestBody?: OpenAPISchema;
|
|
23
|
+
responseSchema?: OpenAPISchema;
|
|
24
|
+
responseDescription?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ParsedParam {
|
|
27
|
+
name: string;
|
|
28
|
+
required: boolean;
|
|
29
|
+
schema: OpenAPISchema;
|
|
30
|
+
description?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read and parse an OpenAPI spec from a file path.
|
|
34
|
+
* Supports both YAML (.yaml, .yml) and JSON (.json) files.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseOpenAPISpec(filePath: string): Promise<ParsedSpec>;
|