@typeship-ax/cli 0.10.0 → 0.21.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 (109) hide show
  1. package/AGENTS.md +10 -5
  2. package/README.md +5 -5
  3. package/api.json +9773 -4501
  4. package/api.md +501 -271
  5. package/dist/api-identity.d.ts.map +1 -1
  6. package/dist/api-identity.js +6 -1
  7. package/dist/cli-agent.d.ts +5 -5
  8. package/dist/cli-agent.d.ts.map +1 -1
  9. package/dist/cli-agent.js +14 -10
  10. package/dist/cli.js +158 -82
  11. package/dist/core/http.d.ts +29 -16
  12. package/dist/core/http.d.ts.map +1 -1
  13. package/dist/core/http.js +108 -25
  14. package/dist/core/pagination.d.ts +8 -8
  15. package/dist/core/pagination.d.ts.map +1 -1
  16. package/dist/core/pagination.js +7 -16
  17. package/dist/errors.d.ts +20 -13
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +29 -20
  20. package/dist/index.d.ts +37 -19
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +42 -18
  23. package/dist/ops.d.ts +5 -1
  24. package/dist/ops.d.ts.map +1 -1
  25. package/dist/ops.js +42 -35
  26. package/dist/polling-login.d.ts.map +1 -1
  27. package/dist/polling-login.js +11 -1
  28. package/dist/resources/api-keys.d.ts +44 -18
  29. package/dist/resources/api-keys.d.ts.map +1 -1
  30. package/dist/resources/api-keys.js +46 -14
  31. package/dist/resources/deliveries.d.ts +46 -0
  32. package/dist/resources/deliveries.d.ts.map +1 -0
  33. package/dist/resources/deliveries.js +70 -0
  34. package/dist/resources/drafts.d.ts +155 -0
  35. package/dist/resources/drafts.d.ts.map +1 -0
  36. package/dist/resources/drafts.js +230 -0
  37. package/dist/resources/files.d.ts +23 -0
  38. package/dist/resources/files.d.ts.map +1 -0
  39. package/dist/resources/files.js +38 -0
  40. package/dist/resources/generate.d.ts +49 -20
  41. package/dist/resources/generate.d.ts.map +1 -1
  42. package/dist/resources/generate.js +56 -14
  43. package/dist/resources/generations.d.ts +70 -18
  44. package/dist/resources/generations.d.ts.map +1 -1
  45. package/dist/resources/generations.js +84 -16
  46. package/dist/resources/organization.d.ts +18 -0
  47. package/dist/resources/organization.d.ts.map +1 -0
  48. package/dist/resources/organization.js +32 -0
  49. package/dist/resources/projects.d.ts +87 -124
  50. package/dist/resources/projects.d.ts.map +1 -1
  51. package/dist/resources/projects.js +70 -173
  52. package/dist/resources/publications.d.ts +46 -0
  53. package/dist/resources/publications.d.ts.map +1 -0
  54. package/dist/resources/publications.js +70 -0
  55. package/dist/resources/releases.d.ts +66 -0
  56. package/dist/resources/releases.d.ts.map +1 -0
  57. package/dist/resources/releases.js +101 -0
  58. package/dist/resources/spec-revisions.d.ts +85 -0
  59. package/dist/resources/spec-revisions.d.ts.map +1 -0
  60. package/dist/resources/spec-revisions.js +116 -0
  61. package/dist/resources/specs.d.ts +72 -0
  62. package/dist/resources/specs.d.ts.map +1 -0
  63. package/dist/resources/specs.js +107 -0
  64. package/dist/resources/targets.d.ts +85 -105
  65. package/dist/resources/targets.d.ts.map +1 -1
  66. package/dist/resources/targets.js +70 -163
  67. package/dist/schemas.d.ts +1 -0
  68. package/dist/schemas.d.ts.map +1 -1
  69. package/dist/schemas.js +194 -139
  70. package/dist/types.d.ts +2626 -1141
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js +90 -11
  73. package/package.json +3 -3
  74. package/src/api-identity.ts +6 -2
  75. package/src/cli-agent.ts +17 -13
  76. package/src/cli.ts +145 -77
  77. package/src/core/http.ts +106 -30
  78. package/src/core/pagination.ts +13 -23
  79. package/src/errors.ts +30 -20
  80. package/src/index.ts +46 -24
  81. package/src/ops.ts +47 -36
  82. package/src/polling-login.ts +10 -1
  83. package/src/resources/api-keys.ts +87 -19
  84. package/src/resources/deliveries.ts +139 -0
  85. package/src/resources/drafts.ts +422 -0
  86. package/src/resources/files.ts +68 -0
  87. package/src/resources/generate.ts +81 -19
  88. package/src/resources/generations.ts +167 -29
  89. package/src/resources/organization.ts +53 -0
  90. package/src/resources/projects.ts +129 -322
  91. package/src/resources/publications.ts +139 -0
  92. package/src/resources/releases.ts +199 -0
  93. package/src/resources/spec-revisions.ts +237 -0
  94. package/src/resources/specs.ts +200 -0
  95. package/src/resources/targets.ts +135 -304
  96. package/src/schemas.ts +195 -140
  97. package/src/types.ts +2793 -1177
  98. package/dist/resources/account.d.ts +0 -18
  99. package/dist/resources/account.d.ts.map +0 -1
  100. package/dist/resources/account.js +0 -27
  101. package/dist/resources/definition-revisions.d.ts +0 -58
  102. package/dist/resources/definition-revisions.d.ts.map +0 -1
  103. package/dist/resources/definition-revisions.js +0 -114
  104. package/dist/resources/definitions.d.ts +0 -35
  105. package/dist/resources/definitions.d.ts.map +0 -1
  106. package/dist/resources/definitions.js +0 -60
  107. package/src/resources/account.ts +0 -46
  108. package/src/resources/definition-revisions.ts +0 -207
  109. package/src/resources/definitions.ts +0 -122
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 { asApiResult, 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,17 +42,17 @@ 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.21.0";
44
46
  const API_VERSION = "1.0.0";
45
47
  const SPEC_FORMAT = "openapi";
46
48
  const IDENTITY_POLICY: IdentityPolicy = {};
47
49
  let LOGIN_IDENTITY: VerifiedIdentity | undefined;
48
- const WHOAMI: { resource: string; method: string } | null = {"resource":"account","method":"retrieve"};
50
+ const WHOAMI: { resource: string; method: string } | null = null;
49
51
  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 Spec, 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 Spec 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
  }
@@ -590,7 +602,7 @@ async function startOAuthBrowserSession(parsed: Parsed, clientId: string, timeou
590
602
  audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
591
603
  organization: requestedLoginOrganization(parsed.flags),
592
604
  }, { signal: controller.signal, timeoutMs, authorize(url) {
593
- process.stderr.write("Sign in to your existing account: " + url + "\n");
605
+ process.stderr.write("Sign in with your existing credentials: " + url + "\n");
594
606
  if (isAgentMode(parsed) || parsed.flags.get("no-browser") === true) process.stderr.write(JSON.stringify({ event: "oauth_browser", authorization_url: url, note: "Open this URL in a browser on the same computer as the CLI." }) + "\n");
595
607
  else openInBrowser(url);
596
608
  } });
@@ -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>"] : []),
@@ -769,7 +780,7 @@ async function cmdWhoami(parsed: Parsed): Promise<void> {
769
780
  if (op) {
770
781
  const client = await makeClient(parsed.flags, op);
771
782
  const target = (client as unknown as Record<string, Record<string, () => Promise<{ ok: boolean; data?: unknown; error?: unknown }>>>)[op.resource]!;
772
- const result = await target[op.method]!();
783
+ const result = await asApiResult(target[op.method]!());
773
784
  if (result.ok) { out(result.data ?? { ok: true }); await flushExit(0); }
774
785
  failApi(result.error, LAST_CLIENT_HAD_CREDENTIAL);
775
786
  }
@@ -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
 
@@ -1164,7 +1174,7 @@ async function cmdAuth(parsed: Parsed): Promise<void> {
1164
1174
  if (op) {
1165
1175
  const client = await makeClient(parsed.flags, op);
1166
1176
  const target = (client as unknown as Record<string, Record<string, () => Promise<{ ok: boolean; data?: unknown; error?: unknown }>>>)[op.resource]!;
1167
- const result = await target[op.method]!();
1177
+ const result = await asApiResult(target[op.method]!());
1168
1178
  if (result.ok) {
1169
1179
  if (identityConfiguration() && savedIdentity?.identity) assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
1170
1180
  report.identity = result.data;
@@ -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;
@@ -1212,7 +1222,7 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
1212
1222
  try {
1213
1223
  const client = await makeClient(parsed.flags, op);
1214
1224
  const target = (client as unknown as Record<string, Record<string, () => Promise<{ ok: boolean; error?: unknown }>>>)[op.resource]!;
1215
- const result = await target[op.method]!();
1225
+ const result = await asApiResult(target[op.method]!());
1216
1226
  checks.push(result.ok ? { name: "identity", ok: true, detail: op.command.join(" ") + " ok" } : { name: "identity", ok: false, detail: classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT }).message, fix: "The credential was rejected; run '" + BIN + " login' with a current one." });
1217
1227
  } catch (e) {
1218
1228
  checks.push({ name: "identity", ok: false, detail: (e as Error).message });
@@ -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.21.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: ", [
@@ -1976,11 +1989,12 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
1976
1989
  ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
1977
1990
  ].join(", ") || "none", width, 21));
1978
1991
  lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
1979
- lines.push(...labeled("Account: ", BIN + " login | logout | whoami | auth check (stored at " + credsPath() + ")", width, 9));
1992
+ lines.push(...labeled("Sign-in: ", BIN + " login | logout | whoami | auth check (stored at " + credsPath() + ")", width, 9));
1980
1993
  lines.push(...labeled("Setup: ", BIN + " init (connect this machine)" + " | " + BIN + " config (defaults)" + (HAS_MCP || MCP_URL ? " | " + BIN + " mcp install --all (agent clients)" : "") + " | " + BIN + " doctor | " + BIN + " upgrade | " + BIN + " completion <shell>", width, 7));
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 Spec 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);
@@ -2368,7 +2438,7 @@ function failOmitted(op: OmittedOpSpec): never {
2368
2438
  return failWith({
2369
2439
  status: "action_required",
2370
2440
  code: "PLAN_LIMIT",
2371
- message: "The command '" + BIN + " " + op.command.join(" ") + "' exists in the API Definition but was omitted from this generated package by its plan limit.",
2441
+ message: "The command '" + BIN + " " + op.command.join(" ") + "' exists in the API Spec but was omitted from this generated package by its plan limit.",
2372
2442
  detail: { operation: op.tool, method: op.httpMethod, path: op.path, generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
2373
2443
  nextSteps: ["Upgrade at https://typeship.dev/pricing and regenerate the package without the operation cap.", "Do not invent or retry an omitted command against this generated package."],
2374
2444
  });
@@ -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 asApiResult(callResult as Promise<unknown>);
2651
+ if (result.ok && op.httpMethod === "POST" && op.path === "/projects/{project_id}/generate") {
2652
+ const batch = result.data as { data: Array<{ id: string }> };
2653
+ const generations = (client as unknown as { generations: { wait(id: string): Promise<unknown> } }).generations;
2654
+ const completed: unknown[] = [];
2655
+ for (const generation of batch.data) {
2656
+ const waited = await asApiResult(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.