@typeship-ax/cli 0.10.0 → 0.20.0

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 (70) hide show
  1. package/AGENTS.md +9 -4
  2. package/README.md +4 -4
  3. package/api.json +4829 -679
  4. package/api.md +387 -91
  5. package/dist/cli-agent.d.ts +5 -5
  6. package/dist/cli-agent.d.ts.map +1 -1
  7. package/dist/cli-agent.js +14 -10
  8. package/dist/cli.js +151 -75
  9. package/dist/core/http.d.ts +3 -2
  10. package/dist/core/http.d.ts.map +1 -1
  11. package/dist/core/http.js +18 -6
  12. package/dist/errors.d.ts +17 -10
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.js +24 -15
  15. package/dist/index.d.ts +7 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +9 -5
  18. package/dist/ops.d.ts +4 -0
  19. package/dist/ops.d.ts.map +1 -1
  20. package/dist/ops.js +45 -35
  21. package/dist/polling-login.d.ts.map +1 -1
  22. package/dist/polling-login.js +11 -1
  23. package/dist/resources/account.d.ts +4 -4
  24. package/dist/resources/account.d.ts.map +1 -1
  25. package/dist/resources/account.js +10 -5
  26. package/dist/resources/api-keys.d.ts +40 -14
  27. package/dist/resources/api-keys.d.ts.map +1 -1
  28. package/dist/resources/api-keys.js +45 -13
  29. package/dist/resources/definition-revisions.d.ts +35 -18
  30. package/dist/resources/definition-revisions.d.ts.map +1 -1
  31. package/dist/resources/definition-revisions.js +43 -15
  32. package/dist/resources/definitions.d.ts +21 -6
  33. package/dist/resources/definitions.d.ts.map +1 -1
  34. package/dist/resources/definitions.js +16 -3
  35. package/dist/resources/generate.d.ts +44 -15
  36. package/dist/resources/generate.d.ts.map +1 -1
  37. package/dist/resources/generate.js +55 -13
  38. package/dist/resources/generations.d.ts +14 -6
  39. package/dist/resources/generations.d.ts.map +1 -1
  40. package/dist/resources/generations.js +26 -5
  41. package/dist/resources/projects.d.ts +106 -53
  42. package/dist/resources/projects.d.ts.map +1 -1
  43. package/dist/resources/projects.js +78 -31
  44. package/dist/resources/targets.d.ts +240 -40
  45. package/dist/resources/targets.d.ts.map +1 -1
  46. package/dist/resources/targets.js +307 -21
  47. package/dist/schemas.d.ts +1 -0
  48. package/dist/schemas.d.ts.map +1 -1
  49. package/dist/schemas.js +171 -111
  50. package/dist/types.d.ts +1969 -154
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/types.js +87 -3
  53. package/package.json +3 -3
  54. package/src/cli-agent.ts +17 -13
  55. package/src/cli.ts +138 -70
  56. package/src/core/http.ts +17 -6
  57. package/src/errors.ts +25 -15
  58. package/src/index.ts +9 -5
  59. package/src/ops.ts +49 -35
  60. package/src/polling-login.ts +10 -1
  61. package/src/resources/account.ts +11 -4
  62. package/src/resources/api-keys.ts +84 -13
  63. package/src/resources/definition-revisions.ts +74 -16
  64. package/src/resources/definitions.ts +28 -4
  65. package/src/resources/generate.ts +78 -13
  66. package/src/resources/generations.ts +31 -4
  67. package/src/resources/projects.ts +141 -39
  68. package/src/resources/targets.ts +561 -27
  69. package/src/schemas.ts +172 -112
  70. package/src/types.ts +2105 -154
package/src/cli.ts CHANGED
@@ -21,6 +21,8 @@ import { homedir, hostname } from "node:os";
21
21
  import { basename, dirname, join } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { TypeshipClient, formatDebugEvent, type ClientOptions, type DebugEvent } from "./index.js";
24
+ import { validateAgainstSchema, ValidationError, type Violation } from "./core/http.js";
25
+ import { SCHEMAS, DEFS } from "./schemas.js";
24
26
  import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired, type OmittedOpSpec, type OpSpec, type ParamSpec } from "./ops.js";
25
27
  import {
26
28
  MCP_CLIENTS, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, collectionProperty, detectHarness, envelope,
@@ -40,7 +42,7 @@ const BASIC: { envUser: string; envPass: string } | null = null;
40
42
  const EXCLUDED_OPS = 0;
41
43
  /** Generated CLI operations that are intentionally unavailable to MCP. */
42
44
  const MCP_EXCLUDED_OPS = 0;
43
- const VERSION = "0.10.0";
45
+ const VERSION = "0.20.0";
44
46
  const API_VERSION = "1.0.0";
45
47
  const SPEC_FORMAT = "openapi";
46
48
  const IDENTITY_POLICY: IdentityPolicy = {};
@@ -50,7 +52,7 @@ const ENVIRONMENTS: Record<string, string> = {};
50
52
  const HAS_MCP = false;
51
53
  const PKG_NAME = "@typeship-ax/cli";
52
54
  const UPDATE_NOTICE = false;
53
- const API_DESCRIPTION: string | null = "Resolve an OpenAPI or GraphQL Definition, diagnose it, and keep every\nselected SDK, CLI, and MCP Target current.\n\nEvery operation but one requires a bearer credential: an organization\nAPI key from the console, or an OAuth access token carrying the operation's\nread, generate, or write capability and the organization selected during\nconsent. OAuth grants cannot switch organizations after consent. A browser\nsession is not a credential for this API. The exception is POST /generate,\nwhich works anonymously with the free plan's limits.\n";
55
+ const API_DESCRIPTION: string | null = "Resolve an OpenAPI or GraphQL Definition, diagnose it, and keep every\nselected CLI, MCP, and SDK Target current.\n\nEvery operation but one requires a bearer credential: an organization\nAPI key from the console, or an OAuth access token carrying the operation's\nread, generate, or write capability and the organization selected during\nconsent. OAuth grants cannot switch organizations after consent. A browser\nsession is not a credential for this API. The exception is POST /generate,\nwhich works anonymously with the free plan's limits.\n\nExamples use Parcel, a fictional delivery service. Replace its domains,\nrepository names, and resource identifiers with your own. The hosted\npetstore Definition is a runnable sample.\n";
54
56
  const DOCS_URL_DEFAULT: string | null = "https://typeship.dev";
55
57
  const DOCS_INDEX_URL_DEFAULT: string | null = null;
56
58
  const RELAY: { mintUrl: string; project: string } | null = null;
@@ -138,7 +140,8 @@ function parseArgv(argv: string[]): Parsed {
138
140
  setFlag(arg.slice(2, eq), arg.slice(eq + 1));
139
141
  } else if (isBooleanFlag(arg.slice(2), positionals)) {
140
142
  const next = argv[i + 1];
141
- if (next === "true" || next === "false") { setFlag(arg.slice(2), next); i++; }
143
+ const nullable = positionals.length >= 2 && findOp(positionals[0]!, positionals[1]!)?.params.some((p) => p.flag === arg.slice(2) && p.nullable);
144
+ if (next === "true" || next === "false" || (next === "null" && nullable)) { setFlag(arg.slice(2), next); i++; }
142
145
  else flags.set(arg.slice(2), true);
143
146
  } else {
144
147
  const next = argv[i + 1];
@@ -220,25 +223,22 @@ function out(value: unknown): void {
220
223
  let FIELDS: string[][] | null = null;
221
224
 
222
225
  /** Keep only FIELDS of a result: arrays item by item, objects by dotted path; scalars untouched. */
223
- function project(value: unknown): unknown {
224
- if (FIELDS === null) return value;
225
- if (Array.isArray(value)) return value.map(project);
226
+ function project(value: unknown, paths: string[][] | null = FIELDS): unknown {
227
+ if (paths === null) return value;
228
+ if (Array.isArray(value)) return value.map((item) => project(item, paths));
226
229
  if (value === null || typeof value !== "object") return value;
230
+ const groups = new Map<string, string[][]>();
231
+ for (const [key, ...rest] of paths) {
232
+ if (key === undefined) continue;
233
+ const group = groups.get(key);
234
+ if (group) group.push(rest); else groups.set(key, [rest]);
235
+ }
227
236
  const out: Record<string, unknown> = {};
228
- for (const path of FIELDS) {
229
- let cursor: unknown = value;
230
- for (const key of path) {
231
- if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) { cursor = undefined; break; }
232
- cursor = (cursor as Record<string, unknown>)[key];
233
- }
234
- if (cursor === undefined) continue;
235
- let target = out;
236
- for (const key of path.slice(0, -1)) {
237
- const next = target[key];
238
- if (next === undefined || next === null || typeof next !== "object" || Array.isArray(next)) target[key] = {};
239
- target = target[key] as Record<string, unknown>;
240
- }
241
- target[path[path.length - 1]!] = cursor;
237
+ for (const [key, rests] of groups) {
238
+ const child = (value as Record<string, unknown>)[key];
239
+ if (child === undefined) continue;
240
+ if (rests.some((rest) => rest.length === 0)) out[key] = child;
241
+ else if (child !== null && typeof child === "object") out[key] = project(child, rests);
242
242
  }
243
243
  return out;
244
244
  }
@@ -289,11 +289,23 @@ function humanError(body: ReturnType<typeof envelope>, code: number): string {
289
289
  if (detail?.violations !== undefined) {
290
290
  for (const v of (detail.violations as { path?: string; message?: string }[]).slice(0, 8)) lines.push(" " + paintErr("dim", (v.path ? v.path + ": " : "") + (v.message ?? "")));
291
291
  } else if (detail?.body !== undefined) {
292
- // The API's own body, when the message above did not already come from
293
- // it; the request id always, for support tickets.
294
- const compact = typeof detail.body === "string" ? detail.body : JSON.stringify(detail.body);
295
- const firstWords = (issue?.message ?? "").slice(0, 40);
296
- if (compact && compact !== "{}" && !(firstWords && compact.includes(firstWords))) lines.push(" " + paintErr("dim", "API said: " + (compact.length > 300 ? compact.slice(0, 297) + "…" : compact)));
292
+ // Keep every API error and its field visible, even when the headline
293
+ // already used the first message. Unstructured bodies retain a summary.
294
+ const apiErrors = typeof detail.body === "object" && detail.body !== null
295
+ ? (detail.body as { errors?: unknown }).errors : undefined;
296
+ const entries = Array.isArray(apiErrors) ? apiErrors.filter((entry): entry is { message: string; field?: string; in?: string } =>
297
+ entry !== null && typeof entry === "object" && typeof entry.message === "string") : [];
298
+ if (entries.length > 0) {
299
+ for (const entry of entries) {
300
+ const field = typeof entry.field === "string" ? entry.field : "";
301
+ const location = typeof entry.in === "string" ? entry.in + " " : "";
302
+ lines.push(" " + paintErr("dim", (field ? location + field + ": " : "") + entry.message));
303
+ }
304
+ } else {
305
+ const compact = typeof detail.body === "string" ? detail.body : JSON.stringify(detail.body);
306
+ const firstWords = (issue?.message ?? "").slice(0, 40);
307
+ if (compact && compact !== "{}" && !(firstWords && compact.includes(firstWords))) lines.push(" " + paintErr("dim", "API said: " + (compact.length > 300 ? compact.slice(0, 297) + "…" : compact)));
308
+ }
297
309
  const requestId = detail.request_id ?? (detail.body as { request_id?: unknown; requestId?: unknown } | null)?.request_id ?? (detail.body as { requestId?: unknown } | null)?.requestId;
298
310
  if (typeof requestId === "string") lines.push(" " + paintErr("dim", "request id: " + requestId));
299
311
  }
@@ -643,7 +655,6 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
643
655
  ...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
644
656
  ...(CLI_AUTH_URL ? [" " + BIN + " login approve in the browser: a key is minted for you (add --no-browser to print the link instead of opening it)"] : []),
645
657
  " " + BIN + " login --with-token read the credential from stdin (CI)",
646
- " " + BIN + " login --console-check <file> test browser login for your Typeship Console without replacing saved logins",
647
658
  ...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --client-id <id> OAuth " + OAUTH_LOGIN_METHOD + " login" + (OAUTH_CLIENT_ID ? " (a default id is built in)" : "")] : []),
648
659
  ...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --no-browser print the approval URL instead of opening a browser", " " + BIN + " login --device use device authorization when your provider supports it"] : []),
649
660
  ...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
@@ -940,12 +951,11 @@ async function cmdMcp(parsed: Parsed): Promise<void> {
940
951
  " " + BIN + " mcp install --codex Codex CLI (~/.codex/config.toml)",
941
952
  " " + BIN + " mcp install --vscode VS Code (./.vscode/mcp.json)",
942
953
  " " + BIN + " mcp install --windsurf | --gemini | --opencode | --zed | --claude-desktop",
943
- " " + BIN + " mcp install --cursor Cursor (./.cursor/mcp.json; see note below)",
954
+ " " + BIN + " mcp install --cursor Cursor (./.cursor/mcp.json)",
944
955
  " " + BIN + " mcp --url <https://...> use a remote MCP endpoint instead of the local server",
945
956
  " " + BIN + " mcp install --claude --read-only register a read-only server (writes are not callable)",
946
957
  "",
947
958
  (MCP_URL ? "Default entry: the hosted endpoint " + MCP_URL + " with the auth env var as a reference (never a literal key)." : "Default entry: this package's local stdio server, which reads credentials saved by '" + BIN + " login' or the CLI's auth env vars."),
948
- "--all skips Cursor until it speaks MCP 2026-07-28.",
949
959
  ];
950
960
  process.stdout.write(lines.join("\n") + "\n");
951
961
  await flushExit(0);
@@ -1040,7 +1050,7 @@ function commandSummaries(): CommandSummary[] {
1040
1050
  paginated: op.paginated,
1041
1051
  destructive: op.safety === "destructive",
1042
1052
  auth: op.auth,
1043
- flags: op.params.filter((p) => p.kind !== "path").map((p) => ({ flag: p.flag, type: p.type, ...(p.items ? { items: p.items } : {}), ...(p.enum ? { enum: p.enum } : {}), required: p.required, ...(p.description ? { description: p.description.split("\n")[0] } : {}) })),
1053
+ flags: op.params.filter((p) => p.kind !== "path").map((p) => ({ flag: p.flag, type: p.type, ...(p.nullable ? { nullable: true } : {}), ...(p.items ? { items: p.items } : {}), ...(p.enum ? { enum: p.enum } : {}), required: p.required, ...(p.description ? { description: p.description.split("\n")[0] } : {}) })),
1044
1054
  }));
1045
1055
  }
1046
1056
 
@@ -1188,7 +1198,7 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
1188
1198
  }
1189
1199
  const checks: DoctorCheck[] = [];
1190
1200
  const nodeMajor = Number(process.versions.node.split(".")[0]);
1191
- checks.push({ name: "node", ok: nodeMajor >= 18, detail: process.version, ...(nodeMajor >= 18 ? {} : { fix: "Install Node 18 or newer." }) });
1201
+ checks.push({ name: "node", ok: nodeMajor >= 20, detail: process.version, ...(nodeMajor >= 20 ? {} : { fix: "Install Node 20 or newer." }) });
1192
1202
  checks.push({ name: "cli", ok: true, detail: BIN + " " + VERSION + " (" + PKG_NAME + ")" });
1193
1203
  let source: string | null = null;
1194
1204
  let storageProblem: string | undefined;
@@ -1241,7 +1251,7 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
1241
1251
  * init: the one command that connects a machine (or a repo) to this API.
1242
1252
  * Stores a credential when given one, installs the skills repository, writes
1243
1253
  * MCP config for every agent client found, and upserts a marked block into
1244
- * AGENTS.md (CLAUDE.md under Claude Code). Idempotent; --all takes the
1254
+ * AGENTS.md (or an existing CLAUDE.md when AGENTS.md is absent). Idempotent; --all takes the
1245
1255
  * defaults without asking; every part reports and none of them fails init.
1246
1256
  */
1247
1257
  async function cmdInit(parsed: Parsed): Promise<void> {
@@ -1253,7 +1263,7 @@ async function cmdInit(parsed: Parsed): Promise<void> {
1253
1263
  " --all do everything without prompting (implied under an agent)",
1254
1264
  " --no-skills | --no-mcp | --no-agents-md skip a part",
1255
1265
  "",
1256
- "Writes: " + credsPath() + " (credential), the skills directory of each agent on this machine" + (SKILLS_REPO ? " (npx skills add " + SKILLS_REPO + ")" : " (no skills repository configured)") + ", MCP config for detected clients, and a '" + BIN + "' block in ./AGENTS.md.",
1266
+ "Writes: " + credsPath() + " (credential), the skills directory of each agent on this machine" + (SKILLS_REPO ? " (npx skills add " + SKILLS_REPO + ")" : " (no skills repository configured)") + ", MCP config for detected clients, and a '" + BIN + "' block in ./AGENTS.md (or an existing ./CLAUDE.md when AGENTS.md is absent).",
1257
1267
  ].join("\n") + "\n");
1258
1268
  await flushExit(0);
1259
1269
  }
@@ -1316,7 +1326,7 @@ async function cmdInit(parsed: Parsed): Promise<void> {
1316
1326
  if (parsed.flags.get("no-agents-md") === true) {
1317
1327
  report.agents_md = { status: "skipped" };
1318
1328
  } else {
1319
- const file = agentInstructionsFile(cwd, harness);
1329
+ const file = agentInstructionsFile(cwd);
1320
1330
  const result = upsertAgentBlock(file, BIN + " agent-contract", agentBlock(agentContext(), commandSummaries()));
1321
1331
  report.agents_md = { status: result.updated ? "updated" : "written", file: result.file };
1322
1332
  }
@@ -1803,6 +1813,8 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
1803
1813
 
1804
1814
 
1805
1815
 
1816
+
1817
+
1806
1818
  /** The command that fetches the next page: same positionals, the next page's query flags. */
1807
1819
  function nextCommandFor(op: OpSpec, pathValues: string[], next: Record<string, unknown>): string {
1808
1820
  const parts = [BIN, op.command[0], op.command[1], ...pathValues.map(shellQuote)];
@@ -1869,6 +1881,7 @@ function helpSentence(description: string | undefined): string {
1869
1881
 
1870
1882
  /** Type column text for a param: string, number, string[], a|b|c, enum, object, json, path. */
1871
1883
  function typeLabel(p: ParamSpec): string {
1884
+ if (p.nullable) return typeLabel({ ...p, nullable: false }) + "|null";
1872
1885
  if (p.type === "file") return "path (uploaded)";
1873
1886
  if (p.format && p.type === "string") return p.format;
1874
1887
  const inlineEnum = (values: string[] | undefined) => values && values.join("|").length <= 24 ? values.join("|") : undefined;
@@ -1944,7 +1957,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
1944
1957
  }
1945
1958
  const width = termWidth();
1946
1959
  const lines: string[] = [];
1947
- lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.10.0");
1960
+ lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.20.0");
1948
1961
  lines.push("");
1949
1962
  lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
1950
1963
  lines.push("");
@@ -1968,7 +1981,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
1968
1981
  lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
1969
1982
  }
1970
1983
  lines.push("");
1971
- const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check bodies)" +
1984
+ const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check parameters and JSON bodies)" +
1972
1985
  (AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
1973
1986
  lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
1974
1987
  lines.push(...labeled("Credential env vars: ", [
@@ -1981,6 +1994,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
1981
1994
  lines.push(...labeled("Agents: ", BIN + " agent-guide | " + BIN + " help --json | --mode agent | -y/--yes/--force | --out <dir> (JSON errors: {status, issues[{code}], next_steps})", width, 8));
1982
1995
  lines.push(...labeled("Docs: ", BIN + " docs [<resource> <command> | search <term> | read <page> | --web]", width, 6));
1983
1996
  if (RELAY) lines.push("Webhooks: " + BIN + " webhooks listen --forward-to <url> (local event forwarding)");
1997
+
1984
1998
  if (SUPPORT_URL) lines.push("Feedback: " + BIN + " feedback (opens the provider's issue tracker)");
1985
1999
  if (MCP_EXCLUDED_OPS > 0 && HAS_MCP) {
1986
2000
  lines.push("");
@@ -2021,16 +2035,17 @@ function commandExtras(op: OpSpec): [string, string][] {
2021
2035
  if (op.paginated) extras.push(["--all", "stream every item from every page (NDJSON)"]);
2022
2036
  const collectionField = collectionProperty(op.outputSchema);
2023
2037
  extras.push(["--fields <a,b.c>", "keep only these fields of the result" + (op.paginated ? " (per item)" : collectionField ? " (per item in " + collectionField + ")" : "")]);
2024
- if (bundleProperty(op.outputSchema) !== null) extras.push(["--out <dir>", "write the response's files ({path, content}) into a directory"]);
2038
+ if ((op.fileBundleProperty ?? bundleProperty(op.outputSchema)) !== null) extras.push(["--out <dir>", "write the response's files ({path, content}) into a directory"]);
2025
2039
  if (op.safety === "destructive") extras.push(["--force, -y", "destructive: required without a terminal, skips the prompt with one"]);
2026
2040
  return extras;
2027
2041
  }
2028
2042
 
2029
- /** One runnable example built from the required inputs. */
2043
+ /** One runnable example from required inputs and authored optional values. */
2030
2044
  function exampleLine(op: OpSpec): string {
2031
2045
  const parts = [BIN, op.command[0], op.command[1]];
2032
2046
  for (const p of op.params) {
2033
- if (!p.required) continue;
2047
+ const hasExample = p.type !== "file" && Object.hasOwn(op.exampleArguments, p.name);
2048
+ if (!p.required && !hasExample) continue;
2034
2049
  const generated = p.type === "file" ? undefined : op.exampleArguments[p.name];
2035
2050
  const values = p.type === "array" ? p.items?.enum : p.enum;
2036
2051
  const fallback: unknown = p.type === "file" ? "./file"
@@ -2040,17 +2055,18 @@ function exampleLine(op: OpSpec): string {
2040
2055
  : p.type === "object" ? {}
2041
2056
  : p.type === "array" ? [p.items?.type === "number" ? 1 : "value"]
2042
2057
  : "value";
2043
- const value = generated ?? fallback;
2058
+ const value = hasExample ? generated : fallback;
2044
2059
  const sample = typeof value === "string" ? shellQuote(value)
2045
2060
  : typeof value === "object" ? shellQuote(JSON.stringify(value))
2046
2061
  : String(value);
2047
2062
  if (p.kind === "path") parts.push(sample);
2048
2063
  else parts.push("--" + p.flag, sample);
2049
2064
  }
2050
- const required = op.bodyStyle === "data" && ((op.inputSchema.required as string[] | undefined) ?? []).includes("body");
2051
- if (required) {
2052
- const body = op.exampleArguments.body;
2053
- parts.push(op.bodyKind === "binary" ? "--file ./file" : "--data " + shellQuote(JSON.stringify(body ?? {})));
2065
+ const showBody = op.bodyStyle === "data" && (Object.hasOwn(op.exampleArguments, "body")
2066
+ || ((op.inputSchema.required as string[] | undefined) ?? []).includes("body"));
2067
+ if (showBody) {
2068
+ const body = Object.hasOwn(op.exampleArguments, "body") ? op.exampleArguments.body : {};
2069
+ parts.push(op.bodyKind === "binary" ? "--file ./file" : "--data " + shellQuote(JSON.stringify(body)));
2054
2070
  }
2055
2071
  if (op.safety === "destructive") parts.push("--force");
2056
2072
  return parts.join(" ");
@@ -2079,6 +2095,9 @@ function printOp(op: OpSpec): void {
2079
2095
  lines.push(" " + exampleLine(op));
2080
2096
  if (op.paginated) lines.push(" " + usageLine(op) + " --all | jq -r '.id'");
2081
2097
  const arrayFlag = rows.find((p) => p.type === "array");
2098
+ if (rows.some((p) => p.nullable)) {
2099
+ lines.push("", "Nullable body flags accept null for JSON null. Use --data to send the literal string null.");
2100
+ }
2082
2101
  if (arrayFlag) {
2083
2102
  const sample = arrayFlag.items?.enum ? arrayFlag.items.enum.slice(0, 2).join(",") : arrayFlag.items?.type === "number" ? "1,2" : "a,b";
2084
2103
  lines.push("", "Array flags take a comma list (--" + arrayFlag.flag + " " + sample + "), the flag repeated, or a JSON array.");
@@ -2160,6 +2179,7 @@ function coerceScalar(flag: string, type: "string" | "number" | "boolean" | "obj
2160
2179
  * Objects must be JSON. Enum values are checked locally.
2161
2180
  */
2162
2181
  function coerce(spec: ParamSpec, raw: string | boolean, repeated?: string[]): unknown {
2182
+ if (spec.nullable && raw === "null" && repeated === undefined) return null;
2163
2183
  if (spec.type === "file") {
2164
2184
  if (raw === true) fail(2, "--" + spec.flag + " expects a file path");
2165
2185
  return fileFromPath(spec.flag, String(raw));
@@ -2207,9 +2227,60 @@ function coerce(spec: ParamSpec, raw: string | boolean, repeated?: string[]): un
2207
2227
  return coerceScalar(spec.flag, spec.type === "json" ? "json" : spec.type === "number" ? "number" : "string", spec.enum, text);
2208
2228
  }
2209
2229
 
2230
+ /** Use validation schemas, not the shortened agent-facing schemas: every union
2231
+ * branch and referenced definition must remain valid input. */
2232
+ function validateParameters(op: OpSpec, values: Record<string, unknown>, flags: Map<string, string | boolean>): void {
2233
+ if (flags.get("validate") !== true) return;
2234
+ const schemas = SCHEMAS[op.resource + "." + op.method]?.params;
2235
+ if (!schemas) return;
2236
+ const violations: Violation[] = [];
2237
+ for (const parameter of op.params) {
2238
+ if (parameter.kind === "body") continue;
2239
+ let value = values[parameter.name];
2240
+ if (value === undefined && parameter.global) {
2241
+ const global = GLOBALS.find((entry) => entry.name === parameter.name);
2242
+ const raw = global ? process.env["TYPESHIP_" + global.envSuffix] : undefined;
2243
+ if (raw !== undefined) value = coerce(parameter, raw);
2244
+ }
2245
+ if (value === undefined) continue;
2246
+ if (parameter.kind === "path") value = coerce(parameter, String(value));
2247
+ const key = parameter.kind + "." + parameter.name;
2248
+ validateAgainstSchema(value, schemas[key], key, violations, DEFS);
2249
+ }
2250
+ if (violations.length) failApi(new ValidationError("request", violations, "parameters"), false);
2251
+ }
2252
+
2210
2253
  /** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
2211
2254
  let LAST_CLIENT_HAD_CREDENTIAL = false;
2212
2255
 
2256
+ /** Check the same complete alternatives the request runtime can select, after
2257
+ * per-scheme flag, environment, profile, and OAuth resolution. */
2258
+ function requireOperationCredentials(op: OpSpec, options: ClientOptions & Record<string, unknown>): void {
2259
+ if (op.auth !== "required") return;
2260
+ const supplied = (value: unknown): boolean => typeof value === "function" || (typeof value === "string" && value.length > 0)
2261
+ || (value !== null && typeof value === "object" && "username" in value && "password" in value && typeof value.username === "string" && value.username.length > 0 && typeof value.password === "string" && value.password.length > 0);
2262
+ const named = Object.fromEntries(Object.entries(options.credentials ?? {}).filter(([, value]) => supplied(value))) as NamedCredentials;
2263
+ const available = namedCredentialAvailability(NAMED_SCHEMES, new Set(Object.keys(options).filter((key) => supplied(options[key]))), named);
2264
+ if (op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => available.has(option)))) return;
2265
+ const alternatives = (op.credentialOptions ?? []).filter((alternative) => alternative.length > 0 && alternative.every((option) => option.startsWith("credentials.")));
2266
+ const missing = alternatives.map((alternative) => alternative.filter((option) => !available.has(option)).map((option) => option.slice("credentials.".length)));
2267
+ const names = new Set(missing.flat());
2268
+ const relevant = AUTH_SCALARS.filter((scalar) => [...names].some((name) => NAMED_SCHEMES[name]?.options.includes(scalar.option)));
2269
+ const needsBasic = [...names].some((name) => NAMED_SCHEMES[name]?.options.includes("basicAuth"));
2270
+ failWith({
2271
+ status: "action_required",
2272
+ code: "NO_AUTH",
2273
+ message: op.command.join(" ") + " needs one complete credential alternative (" + wireOf(op) + "). " + (missing.length
2274
+ ? "Missing security schemes: " + missing.map((names) => names.join(" + ")).join(" OR ") + "."
2275
+ : "The declared security requirements have no supported, compatible alternative in this CLI."),
2276
+ nextSteps: alternatives.length ? [
2277
+ ...relevant.map((a) => "Set " + a.env + " in the environment, pass --" + a.flag + " <value>, or run '" + BIN + " login'."),
2278
+ ...(needsBasic && BASIC ? ["Set " + BASIC.envUser + " and " + BASIC.envPass + ", or pass --username and --password."] : []),
2279
+ "Supply all schemes in one alternative through " + "TYPESHIP_CREDENTIALS" + " or --credentials @<JSON-file>: " + alternatives.map((alternative) => alternative.map((option) => option.slice("credentials.".length)).join(" + ")).join(" OR ") + ".",
2280
+ ] : ["Check the operation's security schemes in the API definition and regenerate with a supported, compatible alternative."],
2281
+ });
2282
+ }
2283
+
2213
2284
  function validateCredentialsInput(flags: Map<string, string | boolean>): void {
2214
2285
  const input = flags.get("credentials");
2215
2286
  if (BASIC && flags.has("username") !== flags.has("password")) fail(2, "Supply --username and --password together, or use a complete named Basic credential.");
@@ -2317,18 +2388,17 @@ async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, cand
2317
2388
  if (value !== undefined) options[g.option] = value;
2318
2389
  }
2319
2390
  LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
2320
- // Who is calling: the CLI, under which agent harness, and whether an
2321
- // agent is driving. "agent" means a harness was detected or the caller
2322
- // said so (--mode agent / env); a bare non-TTY run (CI, a pipeline) is
2323
- // "non-interactive", so usage by surface does not count CI as agents.
2324
- // The API can read it back; it carries no secrets.
2391
+ requireOperationCredentials(op, options);
2392
+ // Identify the package and version. Optional harness and caller details
2393
+ // let the API distinguish agent traffic from other non-interactive use.
2325
2394
  const harness = detectHarness();
2326
2395
  const explicitAgent = flags.get("mode") === "agent" || process.env["TYPESHIP_MODE"] === "agent";
2327
2396
  const behaving = agentMode({ flagMode: flags.get("mode"), envMode: process.env["TYPESHIP_MODE"], stdoutIsTTY: process.stdout.isTTY === true, stdinIsTTY: process.stdin.isTTY === true });
2328
- const caller = harness || explicitAgent ? "; agent" : behaving ? "; non-interactive" : "";
2397
+ const caller = harness || explicitAgent ? "agent" : behaving ? "non-interactive" : null;
2398
+ const details = [harness ? "harness=" + harness : null, caller].filter(Boolean).join("; ");
2329
2399
  options.defaultHeaders = {
2330
2400
  ...(options.defaultHeaders as Record<string, string> | undefined),
2331
- "User-Agent": PKG_NAME + "-cli/" + VERSION + " (typeship" + (harness ? "; harness=" + harness : "") + caller + ")",
2401
+ "User-Agent": PKG_NAME + "-cli/" + VERSION + (details ? " (" + details + ")" : ""),
2332
2402
  };
2333
2403
  if (forIdentity) { options.fetch = identityFetch(baseUrl); options.maxRetries = 0; options.timeoutMs = 10_000; }
2334
2404
  return new TypeshipClient(options);
@@ -2393,8 +2463,8 @@ async function main(): Promise<void> {
2393
2463
  fail(2, "--format json is the only format; output is always JSON.");
2394
2464
  }
2395
2465
  if (parsed.flags.has("version") || parsed.positionals[0] === "version") {
2396
- // "typeship 1.0.0 (typeship 1.0.0, ...)" said the name three times; when
2397
- // the API's title is the bin, name the API version as such.
2466
+ // "acme 1.0.0 (acme 1.0.0)" would say the name twice; when the API's
2467
+ // title is the bin, name the API version as such.
2398
2468
  const apiLabel = API_TITLE.toLowerCase().replace(/[^a-z0-9]/g, "") === BIN.toLowerCase().replace(/[^a-z0-9]/g, "") ? "API " + API_VERSION : API_TITLE + " " + API_VERSION;
2399
2469
  process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by typeship)\n");
2400
2470
  await flushExit(0);
@@ -2415,6 +2485,7 @@ async function main(): Promise<void> {
2415
2485
  if (resourceCmd === "upgrade") { await cmdUpgrade(parsed); }
2416
2486
 
2417
2487
 
2488
+
2418
2489
  if (resourceCmd === "docs") { await cmdDocs(parsed); }
2419
2490
  if (resourceCmd === "completion") { await cmdCompletion(parsed); }
2420
2491
  if (resourceCmd === "config") { await cmdConfig(parsed); }
@@ -2532,28 +2603,15 @@ async function main(): Promise<void> {
2532
2603
  }
2533
2604
 
2534
2605
  // --out <dir> materializes a file-shaped response (see cli-agent.ts bundleProperty).
2535
- const bundleField = bundleProperty(op.outputSchema);
2606
+ const bundleField = op.fileBundleProperty ?? bundleProperty(op.outputSchema);
2536
2607
  const collectionField = collectionProperty(op.outputSchema);
2537
2608
  const outDir = typeof parsed.flags.get("out") === "string" ? (parsed.flags.get("out") as string) : undefined;
2538
2609
  if (outDir !== undefined && bundleField === null) {
2539
2610
  fail(2, "--out applies to commands whose response carries files ({path, content}); " + op.command.join(" ") + " does not.");
2540
2611
  }
2541
2612
 
2542
- // The spec says this operation needs a credential and none resolved:
2543
- // say so now, locally, instead of sending a request to learn it.
2544
- if (op.auth === "required" && (AUTH_SCALARS.length > 0 || BASIC || HAS_OAUTH_LOGIN) && credentialSource(parsed.flags) === null) {
2545
- failWith({
2546
- status: "action_required",
2547
- code: "NO_AUTH",
2548
- message: op.command.join(" ") + " needs a credential (" + wireOf(op) + " is authenticated) and none was found.",
2549
- nextSteps: [
2550
- ...AUTH_SCALARS.map((a) => "Set " + a.env + " in the environment, pass --" + a.flag + " <value>, or run '" + BIN + " login'."),
2551
- ...(BASIC ? ["Set " + BASIC.envUser + " and " + BASIC.envPass + ", or pass --username and --password."] : []),
2552
- ...(AUTH_SCALARS.length === 0 && !BASIC ? ["Run '" + BIN + " login'."] : []),
2553
- "'" + BIN + " auth check' shows what the CLI would send.",
2554
- ],
2555
- });
2556
- }
2613
+ validateParameters(op, values, parsed.flags);
2614
+ const client = await makeClient(parsed.flags, op);
2557
2615
 
2558
2616
  // Destructive commands need --force. A person gets asked; an agent gets
2559
2617
  // an action_required envelope with the exact command to run, so nothing
@@ -2573,7 +2631,6 @@ async function main(): Promise<void> {
2573
2631
  if (answer !== "y" && answer !== "yes") failWith({ status: "action_required", code: "CONFIRMATION_REQUIRED", message: "Cancelled.", nextSteps: ["Run again with --force to skip the prompt: " + rerun] });
2574
2632
  }
2575
2633
 
2576
- const client = await makeClient(parsed.flags, op);
2577
2634
  const selectValue = typeof parsed.flags.get("select") === "string" ? (parsed.flags.get("select") as string) : undefined;
2578
2635
  const args = buildArgs(op, values, dataBody, selectValue);
2579
2636
  const target = (client as unknown as Record<string, Record<string, (...a: unknown[]) => unknown>>)[op.resource]!;
@@ -2590,7 +2647,18 @@ async function main(): Promise<void> {
2590
2647
  }
2591
2648
  }
2592
2649
 
2593
- const result = await (callResult as Promise<{ ok: boolean; data?: unknown; error?: unknown; response?: { requestId?: string } }>);
2650
+ let result = await (callResult as Promise<{ ok: boolean; data?: unknown; error?: unknown; response?: { requestId?: string } }>);
2651
+ if (result.ok && op.httpMethod === "POST" && op.path === "/projects/{project_id}/generations") {
2652
+ const batch = result.data as { data: Array<{ id: string }> };
2653
+ const generations = (client as unknown as { generations: { wait(id: string): Promise<{ ok: boolean; data?: unknown; error?: unknown }> } }).generations;
2654
+ const completed: unknown[] = [];
2655
+ for (const generation of batch.data) {
2656
+ const waited = await generations.wait(generation.id);
2657
+ if (!waited.ok) failApi(waited.error, LAST_CLIENT_HAD_CREDENTIAL);
2658
+ completed.push(waited.data);
2659
+ }
2660
+ result = { ...result, data: { ...batch, data: completed } };
2661
+ }
2594
2662
  if (result.ok) {
2595
2663
  if (op.sse) {
2596
2664
  // Server-sent events as NDJSON, one line per event, until the stream ends.
package/src/core/http.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Runtime core. Generated by typeship — https://typeship.dev
3
- * Zero dependencies: built on the platform fetch API (Node 18+, browsers, edge).
3
+ * Zero dependencies: built on the platform fetch API (Node 20+, browsers, edge).
4
4
  */
5
5
 
6
6
  export interface RequestOptions {
@@ -110,13 +110,15 @@ export interface Violation { path: string; message: string }
110
110
  * ApiResult, like every other failure. */
111
111
  export class ValidationError extends Error {
112
112
  readonly direction: "request" | "response";
113
+ readonly target: "body" | "parameters";
113
114
  readonly violations: Violation[];
114
- constructor(direction: "request" | "response", violations: Violation[]) {
115
+ constructor(direction: "request" | "response", violations: Violation[], target: "body" | "parameters" = "body") {
115
116
  const shown = violations.slice(0, 3).map((v) => v.path + " " + v.message).join("; ");
116
- super(direction + " body failed schema validation: " + shown
117
+ super(direction + " " + target + " failed schema validation: " + shown
117
118
  + (violations.length > 3 ? " (+" + (violations.length - 3) + " more)" : ""));
118
119
  this.name = "ValidationError";
119
120
  this.direction = direction;
121
+ this.target = target;
120
122
  this.violations = violations;
121
123
  }
122
124
  }
@@ -153,7 +155,16 @@ export function validateAgainstSchema(value: unknown, schema: unknown, path: str
153
155
 
154
156
  if (Array.isArray(s.allOf)) for (const sub of s.allOf) validateAgainstSchema(value, sub, path, out, defs);
155
157
  const variants = s.anyOf ?? s.oneOf;
156
- if (Array.isArray(variants) && variants.length > 0) {
158
+ const discriminator = s.responseDiscriminator;
159
+ const tag = typeof discriminator?.propertyName === "string" && value && typeof value === "object" && !Array.isArray(value)
160
+ ? (value as Record<string, unknown>)[discriminator.propertyName] : undefined;
161
+ if (typeof tag === "string") {
162
+ if (Object.hasOwn(discriminator.mapping, tag)) {
163
+ validateAgainstSchema(value, discriminator.mapping[tag], path, out, defs);
164
+ }
165
+ // Unknown variants remain raw data. Callers must handle their tag before
166
+ // interpreting fields or taking an action based on workflow state.
167
+ } else if (Array.isArray(variants) && variants.length > 0) {
157
168
  const matched = variants.some((sub: unknown) => {
158
169
  const scratch: Violation[] = [];
159
170
  validateAgainstSchema(value, sub, path, scratch, defs);
@@ -806,8 +817,8 @@ function meta(response: Response, body?: unknown): ResponseMeta {
806
817
  };
807
818
  }
808
819
 
809
- /** AbortSignal.any reached Node in 18.17. Keep the package's Node 18 floor
810
- * honest for earlier 18.x releases and for web runtimes without it. */
820
+ /** AbortSignal.any is missing from some web and edge runtimes; compose the
821
+ * signals by hand there. */
811
822
  function composeSignals(signals: AbortSignal[]): AbortSignal {
812
823
  const nativeAny = (AbortSignal as typeof AbortSignal & { any?: (items: AbortSignal[]) => AbortSignal }).any;
813
824
  if (nativeAny) return nativeAny.call(AbortSignal, signals);
package/src/errors.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // typeship — typed error classes.
2
- // Generated by typeship — https://typeship.dev — do not edit by hand.
2
+ // Generated by typeship — https://typeship.dev
3
3
 
4
4
  import { ApiError, type ResponseMeta } from "./core/http.js";
5
5
  import type { ErrorModel, ErrorModelRead } from "./types.js";
@@ -77,6 +77,16 @@ export class RateLimitedError extends ApiError<429, ErrorModelRead> {
77
77
  }
78
78
  }
79
79
 
80
+ /**
81
+ * An unexpected error prevented the request from completing.
82
+ * Raised for HTTP 500 responses.
83
+ */
84
+ export class InternalServerError extends ApiError<500, ErrorModelRead> {
85
+ constructor(body: ErrorModelRead, response: ResponseMeta) {
86
+ super("An unexpected error prevented the request from completing.", 500, body, response);
87
+ }
88
+ }
89
+
80
90
  /**
81
91
  * Unexpected error.
82
92
  * Raised for "default" responses.
@@ -88,41 +98,41 @@ export class ApiResponseError extends ApiError<number, ErrorModelRead> {
88
98
  }
89
99
 
90
100
  /**
91
- * The plan does not include another project or the requested target configuration.
92
- * Raised for HTTP 402 responses.
101
+ * No such resource in this account.
102
+ * Raised for HTTP 404 responses.
93
103
  */
94
- export class PaymentRequiredError extends ApiError<402, ErrorModelRead> {
104
+ export class NotFoundError extends ApiError<404, ErrorModelRead> {
95
105
  constructor(body: ErrorModelRead, response: ResponseMeta) {
96
- super("The plan does not include another project or the requested target configuration.", 402, body, response);
106
+ super("No such resource in this account.", 404, body, response);
97
107
  }
98
108
  }
99
109
 
100
110
  /**
101
- * Project setup failed unexpectedly; the key reservation is released.
102
- * Raised for HTTP 500 responses.
111
+ * The plan does not include another project or the requested target configuration.
112
+ * Raised for HTTP 402 responses.
103
113
  */
104
- export class InternalServerError extends ApiError<500, ErrorModelRead> {
114
+ export class PaymentRequiredError extends ApiError<402, ErrorModelRead> {
105
115
  constructor(body: ErrorModelRead, response: ResponseMeta) {
106
- super("Project setup failed unexpectedly; the key reservation is released.", 500, body, response);
116
+ super("The plan does not include another project or the requested target configuration.", 402, body, response);
107
117
  }
108
118
  }
109
119
 
110
120
  /**
111
- * No such resource in this account.
112
- * Raised for HTTP 404 responses.
121
+ * The resource changed since the ETag supplied in If-Match. No write was applied.
122
+ * Raised for HTTP 412 responses.
113
123
  */
114
- export class NotFoundError extends ApiError<404, ErrorModelRead> {
124
+ export class PreconditionFailedError extends ApiError<412, ErrorModelRead> {
115
125
  constructor(body: ErrorModelRead, response: ResponseMeta) {
116
- super("No such resource in this account.", 404, body, response);
126
+ super("The resource changed since the ETag supplied in If-Match. No write was applied.", 412, body, response);
117
127
  }
118
128
  }
119
129
 
120
130
  /**
121
- * The Project was saved, but an obsolete release pull request could not be retired.
131
+ * Dependent work failed while completing the request.
122
132
  * Raised for HTTP 502 responses.
123
133
  */
124
134
  export class BadGatewayError extends ApiError<502, ErrorModelRead> {
125
135
  constructor(body: ErrorModelRead, response: ResponseMeta) {
126
- super("The Project was saved, but an obsolete release pull request could not be retired.", 502, body, response);
136
+ super("Dependent work failed while completing the request.", 502, body, response);
127
137
  }
128
138
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // typeship — client entry point.
2
- // Generated by typeship — https://typeship.dev — do not edit by hand.
2
+ // Generated by typeship — https://typeship.dev
3
3
 
4
4
  import {
5
5
  HttpCore,
@@ -22,8 +22,8 @@ import { AccountResource } from "./resources/account.js";
22
22
  import { ApiKeysResource } from "./resources/api-keys.js";
23
23
 
24
24
  /** This package's version, also sent as the `User-Agent`. */
25
- export const VERSION = "0.10.0";
26
- const USER_AGENT = "@typeship-ax/cli/0.10.0 (typeship)";
25
+ export const VERSION = "0.20.0";
26
+ const USER_AGENT = "@typeship-ax/cli/0.20.0";
27
27
 
28
28
  export interface ClientOptions {
29
29
  /** Override the server URL. Default: `https://typeship.dev/api/v1` */
@@ -79,10 +79,10 @@ export interface ClientOptions {
79
79
  }
80
80
 
81
81
  /**
82
- * typeship — v0.10.0
82
+ * typeship — v0.20.0
83
83
  *
84
84
  * Resolve an OpenAPI or GraphQL Definition, diagnose it, and keep every
85
- * selected SDK, CLI, and MCP Target current.
85
+ * selected CLI, MCP, and SDK Target current.
86
86
  *
87
87
  * Every operation but one requires a bearer credential: an organization
88
88
  * API key from the console, or an OAuth access token carrying the operation's
@@ -90,6 +90,10 @@ export interface ClientOptions {
90
90
  * consent. OAuth grants cannot switch organizations after consent. A browser
91
91
  * session is not a credential for this API. The exception is POST /generate,
92
92
  * which works anonymously with the free plan's limits.
93
+ *
94
+ * Examples use Parcel, a fictional delivery service. Replace its domains,
95
+ * repository names, and resource identifiers with your own. The hosted
96
+ * petstore Definition is a runnable sample.
93
97
  */
94
98
  export class TypeshipClient {
95
99
  readonly generate: GenerateResource;