toolcraft-openapi 0.0.59 → 0.0.61

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.
@@ -126,6 +126,7 @@ export interface GeneratedCommand {
126
126
  idempotencyHeader?: string;
127
127
  rawResponse?: boolean;
128
128
  confirm: boolean;
129
+ positional: string[];
129
130
  params: GeneratedParam[];
130
131
  paramsSchemaOptions?: GeneratedObjectSchemaOptions;
131
132
  preflightBlocks: GeneratedPreflightBlock[];
@@ -150,6 +151,7 @@ interface GeneratedCommandExample {
150
151
  params: Record<string, unknown>;
151
152
  }
152
153
  interface GeneratedParamDefinitionMetadata {
154
+ additionalProperties?: boolean;
153
155
  defaultValue?: unknown;
154
156
  format?: string;
155
157
  jsonType?: "integer";
@@ -177,7 +179,16 @@ interface GeneratedArrayParamDefinition extends GeneratedParamDefinitionMetadata
177
179
  interface GeneratedJsonParamDefinition extends GeneratedParamDefinitionMetadata {
178
180
  kind: "json";
179
181
  }
180
- export type GeneratedParamDefinition = GeneratedScalarParamDefinition | GeneratedEnumParamDefinition | GeneratedArrayParamDefinition | GeneratedJsonParamDefinition;
182
+ interface GeneratedObjectParamDefinition extends GeneratedParamDefinitionMetadata {
183
+ kind: "object";
184
+ properties: readonly GeneratedObjectPropertyDefinition[];
185
+ }
186
+ interface GeneratedObjectPropertyDefinition {
187
+ name: string;
188
+ optional: boolean;
189
+ definition: GeneratedParamDefinition;
190
+ }
191
+ export type GeneratedParamDefinition = GeneratedScalarParamDefinition | GeneratedEnumParamDefinition | GeneratedArrayParamDefinition | GeneratedObjectParamDefinition | GeneratedJsonParamDefinition;
181
192
  type GeneratedParamScope = "cli" | "mcp" | "sdk";
182
193
  type GeneratedEnumValue = string | number | boolean;
183
194
  export type GeneratedValueReference = {
package/dist/generate.js CHANGED
@@ -21,16 +21,6 @@ const NULL_HELPER_SUPPORT = {
21
21
  query: { array: false, scalar: false }
22
22
  };
23
23
  const TRANSPORT_PARAMS = [
24
- {
25
- paramName: "dryRun",
26
- sourceName: "dryRun",
27
- location: "transport",
28
- description: "Print the HTTP request and exit without sending it.",
29
- scope: ["cli", "sdk"],
30
- global: true,
31
- optional: true,
32
- definition: { kind: "boolean" }
33
- },
34
24
  {
35
25
  paramName: "verbose",
36
26
  sourceName: "verbose",
@@ -57,6 +47,10 @@ const SCHEMA_OPTION_SOURCES = [
57
47
  key: "description",
58
48
  get: (param) => param.description
59
49
  },
50
+ {
51
+ key: "additionalProperties",
52
+ get: (param) => param.definition.additionalProperties
53
+ },
60
54
  {
61
55
  key: "default",
62
56
  get: (param) => param.definition.defaultValue
@@ -275,6 +269,7 @@ function createGeneratedCommand(document, entry) {
275
269
  const noun = createSafeGeneratedNoun(deriveNoun(operation, entry.path, operationId));
276
270
  const verb = deriveVerb(entry.method, entry.path, operation, operationId, noun);
277
271
  const collected = collectParams(document, entry, operation, operationId, auth, response.mode);
272
+ const positional = collectPathPositionals(entry.path, collected.params, operationId);
278
273
  const methodDefaults = METHOD_DEFAULTS[entry.method];
279
274
  const exportName = `${toCamelCase(noun)}${toPascalCase(verb)}Command`;
280
275
  const filePath = `${noun}/${verb}.ts`;
@@ -295,6 +290,7 @@ function createGeneratedCommand(document, entry) {
295
290
  contentType: collected.contentType,
296
291
  multipartBinaryFields: collected.multipartBinaryFields,
297
292
  confirm: methodDefaults?.confirm === true,
293
+ positional,
298
294
  params: collected.params,
299
295
  paramsSchemaOptions: collected.paramsSchemaOptions,
300
296
  preflightBlocks: collected.preflightBlocks,
@@ -303,6 +299,28 @@ function createGeneratedCommand(document, entry) {
303
299
  optionalSections: collected.optionalSections
304
300
  };
305
301
  }
302
+ function collectPathPositionals(pathTemplate, params, operationId) {
303
+ const pathParamNames = new Set(params.filter((param) => param.location === "path").map((param) => param.sourceName));
304
+ const positionals = [];
305
+ for (const segment of pathTemplate.split("/")) {
306
+ if (!segment.startsWith("{") || !segment.endsWith("}")) {
307
+ continue;
308
+ }
309
+ const sourceName = segment.slice(1, -1);
310
+ if (!pathParamNames.has(sourceName)) {
311
+ throw new UserError(`Operation ${JSON.stringify(operationId)} path parameter ${JSON.stringify(sourceName)} is missing from generated params.`);
312
+ }
313
+ const param = params.find((candidate) => candidate.location === "path" && candidate.sourceName === sourceName);
314
+ if (param === undefined) {
315
+ continue;
316
+ }
317
+ if (param.definition.kind === "array" && positionals.length < pathParamNames.size - 1) {
318
+ throw new UserError(`Operation ${JSON.stringify(operationId)} path parameter ${JSON.stringify(sourceName)} is an array and can only be the final positional argument.`);
319
+ }
320
+ positionals.push(param.paramName);
321
+ }
322
+ return positionals;
323
+ }
306
324
  function resolveOperationBaseUrl(operation, operationId) {
307
325
  if (operation.servers === undefined) {
308
326
  return undefined;
@@ -660,7 +678,7 @@ function assertSupportedPathParameterSerialization(parameter, operationId) {
660
678
  throw new UserError(`Operation ${JSON.stringify(operationId)} path parameter ${JSON.stringify(parameter.name)} uses unsupported serialization. Path parameters must use style "simple" with explode false in v1.`);
661
679
  }
662
680
  function createBodyField(document, name, schema, optional, operationId) {
663
- if (isComplexJsonBodySchema(document, schema, operationId, `request body field ${JSON.stringify(name)}`)) {
681
+ if (shouldUseJsonBodyField(document, schema, operationId, `request body field ${JSON.stringify(name)}`)) {
664
682
  return createJsonBodyField({
665
683
  document,
666
684
  name,
@@ -683,6 +701,25 @@ function createBodyField(document, name, schema, optional, operationId) {
683
701
  location: "body"
684
702
  });
685
703
  }
704
+ function shouldUseJsonBodyField(document, schema, operationId, context) {
705
+ if (isUnconstrainedJsonSchema(schema)) {
706
+ return true;
707
+ }
708
+ if (getCompositionKeyword(schema) !== undefined || Array.isArray(schema.type)) {
709
+ return true;
710
+ }
711
+ if (schema.type === "object") {
712
+ return typeof schema.additionalProperties === "object";
713
+ }
714
+ if (schema.properties !== undefined || schema.additionalProperties !== undefined) {
715
+ return true;
716
+ }
717
+ if (schema.type !== "array" || schema.items === undefined) {
718
+ return false;
719
+ }
720
+ const itemSchema = resolveBodySchema(document, schema.items, operationId, `${context} items`);
721
+ return shouldUseJsonBodyField(document, itemSchema, operationId, `${context} items`);
722
+ }
686
723
  function isComplexJsonBodySchema(document, schema, operationId, context) {
687
724
  if (isUnconstrainedJsonSchema(schema)) {
688
725
  return true;
@@ -896,7 +933,10 @@ const FIELD_ASSEMBLERS = {
896
933
  ...options,
897
934
  location: "body"
898
935
  }),
899
- object: (options) => createJsonBodyField(options),
936
+ object: (options) => createScalarParam({
937
+ ...options,
938
+ location: "body"
939
+ }),
900
940
  scalar: (options) => createScalarParam({
901
941
  ...options,
902
942
  location: "body"
@@ -990,6 +1030,15 @@ function createParamDefinition(document, schema, operationId, context) {
990
1030
  ...(schema.nullable === true ? { nullable: true } : {})
991
1031
  };
992
1032
  }
1033
+ if (schema.type === "object") {
1034
+ return {
1035
+ kind: "object",
1036
+ properties: createObjectPropertyDefinitions(document, schema, operationId, context),
1037
+ additionalProperties: schema.additionalProperties !== false,
1038
+ ...(schema.default === undefined ? {} : { defaultValue: schema.default }),
1039
+ ...(schema.nullable === true ? { nullable: true } : {})
1040
+ };
1041
+ }
993
1042
  const scalarDefinition = isOpenApiScalarType(schema.type)
994
1043
  ? SCHEMA_TYPE_TO_KIND[schema.type]
995
1044
  : undefined;
@@ -1021,6 +1070,27 @@ function createParamDefinition(document, schema, operationId, context) {
1021
1070
  }
1022
1071
  throw new UserError(`Operation ${JSON.stringify(operationId)} uses unsupported ${context}. Supported shapes in this milestone are string, number, integer, boolean, enum, and arrays of those values.`);
1023
1072
  }
1073
+ function createObjectPropertyDefinitions(document, schema, operationId, context) {
1074
+ const required = new Set(schema.required ?? []);
1075
+ const properties = [];
1076
+ for (const [name, property] of Object.entries(schema.properties ?? {})) {
1077
+ const propertySchema = resolveBodySchema(document, property, operationId, `${context}.properties.${name}`);
1078
+ if (propertySchema.readOnly === true) {
1079
+ continue;
1080
+ }
1081
+ properties.push({
1082
+ name,
1083
+ optional: !required.has(name),
1084
+ definition: shouldUseJsonBodyField(document, propertySchema, operationId, `${context}.properties.${name}`)
1085
+ ? {
1086
+ kind: "json",
1087
+ ...(propertySchema.nullable === true ? { nullable: true } : {})
1088
+ }
1089
+ : createParamDefinition(document, propertySchema, operationId, `${context}.properties.${name}`)
1090
+ });
1091
+ }
1092
+ return properties;
1093
+ }
1024
1094
  function assertPathTemplateParameters(path, parameters, operationId) {
1025
1095
  const placeholders = new Set(collectPathPlaceholders(path));
1026
1096
  for (const placeholder of placeholders) {
@@ -1433,6 +1503,9 @@ function createCommandFile(options) {
1433
1503
  if (options.confirm) {
1434
1504
  lines.push(" confirm: true,");
1435
1505
  }
1506
+ if (options.positional.length > 0) {
1507
+ lines.push(` positional: ${JSON.stringify(options.positional)},`);
1508
+ }
1436
1509
  lines.push(" params: S.Object({");
1437
1510
  lines.push(...renderParamLines(options.params));
1438
1511
  lines.push(options.paramsSchemaOptions?.additionalProperties === false
@@ -1494,7 +1567,6 @@ function createCommandFile(options) {
1494
1567
  }
1495
1568
  lines.push(" tokenSource,");
1496
1569
  lines.push(" fetch,");
1497
- lines.push(" dryRun: params.dryRun,");
1498
1570
  lines.push(" verbose: params.verbose,");
1499
1571
  if (options.rawResponse === true) {
1500
1572
  lines.push(" rawResponse: params.rawResponse,");
@@ -1600,6 +1672,7 @@ const DEFINITION_RENDERERS = {
1600
1672
  enum: (definition, options) => renderSchemaCall("S.Enum", renderConstArray(definition.enumValues), options),
1601
1673
  json: (_definition) => renderSchemaCall("S.Json"),
1602
1674
  number: (_definition, options) => renderSchemaCall("S.Number", options),
1675
+ object: (definition, options) => renderSchemaCall("S.Object", renderObjectDefinitionShape(definition), options),
1603
1676
  string: (_definition, options) => renderSchemaCall("S.String", options)
1604
1677
  };
1605
1678
  function renderSchemaCall(builder, ...args) {
@@ -1612,6 +1685,16 @@ function renderSchemaOptions(param) {
1612
1685
  function renderConstArray(values) {
1613
1686
  return `${JSON.stringify(values)} as const`;
1614
1687
  }
1688
+ function renderObjectDefinitionShape(definition) {
1689
+ if (definition.properties.length === 0) {
1690
+ return "{}";
1691
+ }
1692
+ return `{ ${definition.properties.map(renderObjectDefinitionProperty).join(", ")} }`;
1693
+ }
1694
+ function renderObjectDefinitionProperty(property) {
1695
+ const schema = renderDefinition(property.definition, undefined, undefined, undefined);
1696
+ return `${renderObjectKey(property.name)}: ${property.optional ? `S.Optional(${schema})` : schema}`;
1697
+ }
1615
1698
  function renderObjectKey(name) {
1616
1699
  if (name === "__proto__") {
1617
1700
  return `[${JSON.stringify(name)}]`;
@@ -1739,7 +1822,7 @@ function createIndexFile(commands) {
1739
1822
  if (groups.length === 0) {
1740
1823
  return {
1741
1824
  path: "index.ts",
1742
- contents: createGeneratedTypeScriptFile(["export {};", ""])
1825
+ contents: createGeneratedTypeScriptFile(["export const generatedCommands = [] as const;", ""])
1743
1826
  };
1744
1827
  }
1745
1828
  const lines = createGeneratedTypeScriptFileLines();
@@ -1759,6 +1842,8 @@ function createIndexFile(commands) {
1759
1842
  lines.push("});");
1760
1843
  lines.push("");
1761
1844
  }
1845
+ lines.push(`export const generatedCommands = [${groups.map(({ noun }) => toCamelCase(noun)).join(", ")}] as const;`);
1846
+ lines.push("");
1762
1847
  return {
1763
1848
  path: "index.ts",
1764
1849
  contents: lines.join("\n")
@@ -1771,11 +1856,11 @@ function createCliFile(theme) {
1771
1856
  "#!/usr/bin/env node",
1772
1857
  ...createGeneratedTypeScriptFileLines(),
1773
1858
  'import { configureTheme, runCLI } from "toolcraft/cli";',
1774
- 'import * as groups from "./index.js";',
1859
+ 'import { generatedCommands } from "./index.js";',
1775
1860
  "",
1776
1861
  `configureTheme({ brand: ${JSON.stringify(theme.brand)}, label: ${JSON.stringify(theme.label)} });`,
1777
1862
  "",
1778
- "await runCLI(Object.values(groups));",
1863
+ "await runCLI([...generatedCommands]);",
1779
1864
  ""
1780
1865
  ].join("\n")
1781
1866
  };
package/dist/http.d.ts CHANGED
@@ -24,7 +24,6 @@ export interface HttpRequestOptions {
24
24
  multipartBinaryFields?: readonly string[];
25
25
  responseMode?: "json" | "text" | "binary";
26
26
  accept?: string;
27
- dryRun?: boolean;
28
27
  verbose?: boolean;
29
28
  signal?: AbortSignal;
30
29
  rawResponse?: boolean;
@@ -41,7 +40,6 @@ export interface HttpRequestOptions {
41
40
  key?: string;
42
41
  createKey?: () => string;
43
42
  };
44
- writeStdout?: (chunk: string) => void;
45
43
  writeStderr?: (chunk: string) => void;
46
44
  }
47
45
  export interface BinaryHttpResponse {
package/dist/http.js CHANGED
@@ -28,13 +28,7 @@ export async function requestJson(options) {
28
28
  }
29
29
  : {};
30
30
  const headers = createHeaders(token, hasBody, { ...options.headers, ...idempotencyHeader }, options.accept, options.bodyMode, options.contentType);
31
- const writeStdout = options.writeStdout ?? process.stdout.write.bind(process.stdout);
32
31
  const writeStderr = options.writeStderr ?? process.stderr.write.bind(process.stderr);
33
- const requestLine = `${method} ${redactSensitiveQueryValues(url)}`;
34
- if (options.dryRun) {
35
- writeStdout(formatDryRunOutput(requestLine, headers, options.body));
36
- return undefined;
37
- }
38
32
  if (options.verbose) {
39
33
  writeStderr(formatTranscriptLines(formatVerboseRequestTranscript(method, url, headers, options.body)));
40
34
  }
@@ -453,20 +447,6 @@ function createHttpErrorRequest(method, url, headers, body) {
453
447
  function serializeHeaders(headers) {
454
448
  return Object.fromEntries(headers.entries());
455
449
  }
456
- function formatDryRunOutput(requestLine, headers, body) {
457
- const lines = [
458
- requestLine,
459
- ...Object.entries(headers).map(([key, value]) => {
460
- const headerValue = redactHeaderValue(key, value);
461
- return `${key}: ${headerValue}`;
462
- }),
463
- ""
464
- ];
465
- if (body !== undefined) {
466
- lines.push(JSON.stringify(body));
467
- }
468
- return `${lines.join("\n")}\n`;
469
- }
470
450
  function formatVerboseRequestTranscript(method, url, headers, body) {
471
451
  const lines = [
472
452
  `→ ${method} ${redactSensitiveQueryValues(url)}`,
@@ -237,6 +237,8 @@ function findDefinitionIssue(value, definition, path) {
237
237
  return undefined;
238
238
  case "number":
239
239
  return findNumberDefinitionIssue(value, definition, path);
240
+ case "object":
241
+ return findObjectDefinitionIssue(value, definition, path);
240
242
  case "string":
241
243
  return findStringDefinitionIssue(value, definition, path);
242
244
  }
@@ -259,6 +261,35 @@ function findArrayDefinitionIssue(value, definition, path) {
259
261
  }
260
262
  return undefined;
261
263
  }
264
+ function findObjectDefinitionIssue(value, definition, path) {
265
+ if (!isJsonObject(value)) {
266
+ return `Expected object at ${formatJsonPath(path)}.`;
267
+ }
268
+ const knownProperties = new Set(definition.properties.map((property) => property.name));
269
+ for (const property of definition.properties) {
270
+ if (!Object.prototype.hasOwnProperty.call(value, property.name)) {
271
+ if (property.optional) {
272
+ continue;
273
+ }
274
+ return `Expected required property ${JSON.stringify(property.name)} at ${formatJsonPath(path)}.`;
275
+ }
276
+ const issue = findDefinitionIssue(value[property.name], property.definition, [
277
+ ...path,
278
+ property.name
279
+ ]);
280
+ if (issue !== undefined) {
281
+ return issue;
282
+ }
283
+ }
284
+ if (definition.additionalProperties === false) {
285
+ for (const key of Object.keys(value)) {
286
+ if (!knownProperties.has(key)) {
287
+ return `Unexpected property ${JSON.stringify(key)} at ${formatJsonPath(path)}.`;
288
+ }
289
+ }
290
+ }
291
+ return undefined;
292
+ }
262
293
  function findNumberDefinitionIssue(value, definition, path) {
263
294
  if (typeof value !== "number" || !Number.isFinite(value)) {
264
295
  return `Expected ${definition.jsonType === "integer" ? "integer" : "number"} at ${formatJsonPath(path)}.`;
@@ -292,6 +323,9 @@ function findStringDefinitionIssue(value, definition, path) {
292
323
  }
293
324
  return undefined;
294
325
  }
326
+ function isJsonObject(value) {
327
+ return typeof value === "object" && value !== null && !Array.isArray(value);
328
+ }
295
329
  function compileDefinitionPattern(pattern) {
296
330
  try {
297
331
  return new RegExp(pattern, "u");
package/dist/runtime.js CHANGED
@@ -62,6 +62,7 @@ function createRuntimeCommand(command) {
62
62
  ...(command.description === undefined ? {} : { description: command.description }),
63
63
  scope: RUNTIME_COMMAND_SCOPE,
64
64
  ...(command.confirm ? { confirm: true } : {}),
65
+ ...(command.positional.length > 0 ? { positional: command.positional } : {}),
65
66
  params: paramsSchema,
66
67
  handler: createRuntimeHandler(command)
67
68
  });
@@ -88,7 +89,6 @@ function createRuntimeHandler(command) {
88
89
  multipartBinaryFields: command.multipartBinaryFields,
89
90
  tokenSource,
90
91
  fetch,
91
- dryRun: params.dryRun,
92
92
  verbose: params.verbose,
93
93
  ...preparedRequestShape
94
94
  });
@@ -112,6 +112,13 @@ const RUNTIME_DEFINITION_BUILDERS = {
112
112
  enum: (definition, options) => options === undefined ? S.Enum(definition.enumValues) : S.Enum(definition.enumValues, options),
113
113
  json: (_definition) => S.Json(),
114
114
  number: (_definition, options) => options === undefined ? S.Number() : S.Number(options),
115
+ object: (definition, options) => {
116
+ const shape = Object.fromEntries(definition.properties.map((property) => {
117
+ const propertySchema = createRuntimeDefinition(property.definition, undefined, undefined, undefined);
118
+ return [property.name, property.optional ? S.Optional(propertySchema) : propertySchema];
119
+ }));
120
+ return options === undefined ? S.Object(shape) : S.Object(shape, options);
121
+ },
115
122
  string: (_definition, options) => options === undefined ? S.String() : S.String(options)
116
123
  };
117
124
  function createRuntimeSchemaOptions(definition, description, shortFlag, scope, global) {
@@ -100,7 +100,7 @@ export function formatColumns(opts) {
100
100
  return [`${firstIndent}${row.left}`];
101
101
  }
102
102
  const rightLines = wrapWords(row.right, rightWidth);
103
- if (row.left.length > leftWidth) {
103
+ if (row.left.length >= leftWidth) {
104
104
  return [
105
105
  `${firstIndent}${row.left}`,
106
106
  ...rightLines.map((line) => `${continuationIndent}${line}`)
@@ -159,7 +159,7 @@ export function formatColumns(opts) {
159
159
  return [`${firstIndent}${row.left}`];
160
160
  }
161
161
  const rightLines = wrapWords(row.right, rightWidth);
162
- if (visibleWidth(row.left) > leftWidth) {
162
+ if (visibleWidth(row.left) >= leftWidth) {
163
163
  return [
164
164
  `${firstIndent}${row.left}`,
165
165
  ...rightLines.map((line) => `${continuationIndent}${line}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-openapi",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,7 +28,7 @@
28
28
  "toolcraft-openapi-generate": "dist/bin/generate.js"
29
29
  },
30
30
  "dependencies": {
31
- "toolcraft": "0.0.59",
31
+ "toolcraft": "0.0.61",
32
32
  "auth-store": "^0.0.1",
33
33
  "fast-string-width": "^3.0.2",
34
34
  "fast-wrap-ansi": "^0.2.0",