@typeship-ax/cli 0.23.1 → 0.24.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 (57) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +2 -2
  3. package/api.md +2 -2
  4. package/dist/arguments.d.ts +9 -2
  5. package/dist/arguments.d.ts.map +1 -1
  6. package/dist/arguments.js +17 -6
  7. package/dist/cli-agent.d.ts +42 -1
  8. package/dist/cli-agent.d.ts.map +1 -1
  9. package/dist/cli-agent.js +187 -18
  10. package/dist/cli.js +341 -76
  11. package/dist/fields.d.ts +8 -1
  12. package/dist/fields.d.ts.map +1 -1
  13. package/dist/fields.js +91 -5
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +3 -3
  16. package/dist/ops.d.ts +5 -0
  17. package/dist/ops.d.ts.map +1 -1
  18. package/dist/ops.js +66 -6
  19. package/dist/resources/deliveries.d.ts +1 -1
  20. package/dist/resources/deliveries.d.ts.map +1 -1
  21. package/dist/resources/deliveries.js +1 -1
  22. package/dist/resources/drafts.d.ts +1 -1
  23. package/dist/resources/drafts.d.ts.map +1 -1
  24. package/dist/resources/drafts.js +1 -1
  25. package/dist/resources/generations.d.ts +2 -2
  26. package/dist/resources/generations.d.ts.map +1 -1
  27. package/dist/resources/generations.js +2 -2
  28. package/dist/resources/releases.d.ts +1 -1
  29. package/dist/resources/releases.d.ts.map +1 -1
  30. package/dist/resources/releases.js +1 -1
  31. package/dist/resources/spec-revisions.d.ts +1 -1
  32. package/dist/resources/spec-revisions.d.ts.map +1 -1
  33. package/dist/resources/spec-revisions.js +1 -1
  34. package/dist/resources/targets.d.ts +1 -1
  35. package/dist/resources/targets.d.ts.map +1 -1
  36. package/dist/resources/targets.js +1 -1
  37. package/dist/table.d.ts +28 -0
  38. package/dist/table.d.ts.map +1 -0
  39. package/dist/table.js +167 -0
  40. package/dist/type-docs.d.ts +61 -0
  41. package/dist/type-docs.d.ts.map +1 -0
  42. package/dist/type-docs.js +174 -0
  43. package/package.json +1 -1
  44. package/src/arguments.ts +19 -7
  45. package/src/cli-agent.ts +196 -17
  46. package/src/cli.ts +330 -76
  47. package/src/fields.ts +81 -5
  48. package/src/index.ts +3 -3
  49. package/src/ops.ts +69 -6
  50. package/src/resources/deliveries.ts +2 -2
  51. package/src/resources/drafts.ts +2 -2
  52. package/src/resources/generations.ts +4 -4
  53. package/src/resources/releases.ts +2 -2
  54. package/src/resources/spec-revisions.ts +2 -2
  55. package/src/resources/targets.ts +2 -2
  56. package/src/table.ts +167 -0
  57. package/src/type-docs.ts +205 -0
package/dist/cli.js CHANGED
@@ -21,13 +21,15 @@ import { fileURLToPath } from "node:url";
21
21
  import { TypeshipClient, formatDebugEvent } from "./index.js";
22
22
  import { asApiResult, mediaTypeForPath, validateAgainstSchema, ValidationError } from "./core/http.js";
23
23
  import { SCHEMAS, DEFS } from "./schemas.js";
24
- import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
25
- import { MCP_CLIENTS, requiredScopes, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, classifyAuthFailure, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
24
+ import { GLOBALS, INPUT_TYPES, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
25
+ import { MCP_CLIENTS, requiredScopes, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, classifyAuthFailure, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, formatRequestPreview, installSkills, mcpConfigured, pendingClaims, recordClaim, requestPreview, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
26
26
  import { relativeDate } from "./dates.js";
27
27
  import { checkValue } from "./arguments.js";
28
28
  import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
29
29
  import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
30
+ import { renderTable } from "./table.js";
30
31
  import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
32
+ import { argumentPathText, findInputType, inputTypeText, namedTypesIn } from "./type-docs.js";
31
33
  const BIN = "typeship";
32
34
  /** The MCP server's key in client configs: the API's name, even when this
33
35
  * command was renamed away from a vendor's. It reads this command's login. */
@@ -39,6 +41,8 @@ const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESH
39
41
  const HOSTED_MCP_HEADERS = { "Authorization": "Bearer ${TYPESHIP_TOKEN}" };
40
42
  const HOSTED_MCP_NOTE = null;
41
43
  const BASIC = null;
44
+ /** Header and query names the API's key schemes use: --dry-run redacts them. */
45
+ const CREDENTIAL_NAMES = [];
42
46
  /** The spec declares no security, so the token is offered, never required. */
43
47
  const AUTH_UNDECLARED = false;
44
48
  /** Repeated --header "Name: value" flags for this invocation. */
@@ -47,7 +51,7 @@ let HEADER_FLAGS = [];
47
51
  const EXCLUDED_OPS = 0;
48
52
  /** Generated CLI operations that are intentionally unavailable to MCP. */
49
53
  const MCP_EXCLUDED_OPS = 0;
50
- const VERSION = "0.23.1";
54
+ const VERSION = "0.24.0";
51
55
  const API_VERSION = "1.0.0";
52
56
  const SPEC_FORMAT = "openapi";
53
57
  const IDENTITY_POLICY = {};
@@ -92,7 +96,7 @@ const API_TITLE = "Typeship";
92
96
  * API parameter with the same name (`accounts list --cursor <c>`) still
93
97
  * takes its value. Boolean API parameters are recognized once the command
94
98
  * is known. */
95
- const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json"]);
99
+ const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json", "dry-run"]);
96
100
  const BUILTIN_BOOLEAN_FLAGS = {
97
101
  login: ["with-token", "no-browser", "device"],
98
102
  logout: ["local"],
@@ -235,29 +239,44 @@ function out(value) {
235
239
  }
236
240
  /** --fields a,b.c: the dotted paths to keep in API results (null = everything). Set in main(). */
237
241
  let FIELDS = null;
242
+ /** --format table: print API results as text for a person instead of JSON. Opt-in only; set in main(). */
243
+ let TABLE = false;
244
+ /** An API result on stdout: JSON, or the --format table view of the same value. */
245
+ function printResult(value, resource, collectionField = null) {
246
+ if (!TABLE) {
247
+ out(value);
248
+ return;
249
+ }
250
+ process.stdout.write(renderTable(value, { width: process.stdout.columns || 120, heading: (text) => paintOut("bold", text), collectionField, resource }));
251
+ }
238
252
  /** Whether the command being run writes: an --fields mistake on a write
239
253
  * must not tempt anyone into running it again. Set in main(). */
240
254
  let FIELDS_AFTER_WRITE = false;
241
255
  /** Keep only FIELDS of a result: arrays item by item, objects by dotted path;
242
256
  * scalars untouched. A path that matches nothing is an error naming the keys
243
- * that exist, never a silent {}. */
244
- function project(value, perItem) {
257
+ * that exist, never a silent {}; one the response schema declares (an
258
+ * optional key no item has) is simply absent. */
259
+ function project(value, perItem, schema) {
245
260
  if (FIELDS === null)
246
261
  return value;
247
- const unmatched = unmatchedFields(value, FIELDS);
262
+ const unmatched = unmatchedFields(value, FIELDS, schema);
248
263
  if (unmatched.length > 0)
249
264
  failUnmatchedFields(unmatched, perItem, value);
250
265
  return projectFields(value, FIELDS);
251
266
  }
267
+ /** The declared schema of the list a result holds under `key`. */
268
+ function listSchemaOf(schema, key) {
269
+ return schema?.properties?.[key];
270
+ }
252
271
  /** --fields over a stream (--all, events): paths no item has matched yet.
253
272
  * The stream is printed as it arrives, so the check fails at its end. */
254
- function streamFieldsCheck() {
273
+ function streamFieldsCheck(schema) {
255
274
  let pending = null;
256
275
  return {
257
276
  item(value) {
258
277
  if (FIELDS === null)
259
278
  return value;
260
- const unmatched = unmatchedFields(value, FIELDS);
279
+ const unmatched = unmatchedFields(value, FIELDS, schema);
261
280
  pending = pending === null ? unmatched : pending.filter((p) => unmatched.some((u) => u.path === p.path));
262
281
  return projectFields(value, FIELDS);
263
282
  },
@@ -271,10 +290,13 @@ function failUnmatchedFields(unmatched, perItem, result) {
271
290
  return failWith({
272
291
  code: "FIELDS_UNMATCHED",
273
292
  message: unmatchedFieldsMessage(unmatched, perItem),
274
- nextSteps: FIELDS_AFTER_WRITE
275
- ? ["This command has already run; do not run it again to change --fields." + (result !== undefined ? " Its full result is in detail.result." : "")]
276
- : ["Run the command again with --fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or without --fields for the whole result."],
277
- detail: { unmatched, ...(FIELDS_AFTER_WRITE && result !== undefined ? { result } : {}) },
293
+ nextSteps: [
294
+ FIELDS_AFTER_WRITE
295
+ ? "This command has already run; do not run it again to change --fields." + (result !== undefined ? " Its full result is in detail.result." : "")
296
+ : result !== undefined ? "The full result is in detail.result; use it rather than running the command again." : "",
297
+ (FIELDS_AFTER_WRITE ? "Next time, use" : "To project a later run, use") + " --fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or omit --fields for the whole result.",
298
+ ].filter((step) => step !== ""),
299
+ detail: { unmatched, ...(result !== undefined ? { result } : {}) },
278
300
  });
279
301
  }
280
302
  /** Thrown after scheduling exit so sync callers stop; main() swallows it. */
@@ -1152,60 +1174,172 @@ function commandSummaries() {
1152
1174
  }));
1153
1175
  }
1154
1176
  /**
1155
- * help --json: a compact command index. Full schemas live behind the
1156
- * per-operation docs command so discovery does not spend an agent's context
1157
- * window on every response shape before it has chosen an operation.
1177
+ * help --json: discovery as data, bounded so one call cannot fill an agent's
1178
+ * context window on a large API. The default is an index of resources and
1179
+ * command names; "help <resource> --json" pages through one resource's
1180
+ * commands with their methods, paths and summaries; "help <resource>
1181
+ * <command> --json" is one command with its flags; "help --json --all" is
1182
+ * every command with every flag. Full schemas stay behind the docs command.
1158
1183
  */
1159
- function helpJson() {
1184
+ const HELP_INDEX_NAMES = 40;
1185
+ const HELP_INDEX_BYTES = 16_000;
1186
+ const HELP_PAGE_SIZE = 50;
1187
+ function helpHeader(detail) {
1188
+ return { schema_version: "3", detail, name: BIN, version: VERSION, api: API_TITLE, api_version: API_VERSION, spec_format: SPEC_FORMAT };
1189
+ }
1190
+ function helpDiscovery() {
1191
+ return {
1192
+ resource: BIN + " help <resource> --json",
1193
+ command: BIN + " help <resource> <command> --json",
1194
+ search: BIN + " docs search <term> --json",
1195
+ operation: BIN + " docs <resource> <command> --json",
1196
+ all: BIN + " help --json --all",
1197
+ note: "Find a command by resource or search, read its flags with help <resource> <command> --json, and its complete schemas with docs. --all prints every command with every flag at once.",
1198
+ };
1199
+ }
1200
+ function helpCoverage() {
1201
+ return EXCLUDED_OPS > 0 ? { coverage: { generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS } } : {};
1202
+ }
1203
+ function opsByResource() {
1160
1204
  const byResource = new Map();
1161
- for (const c of commandSummaries()) {
1162
- const list = byResource.get(c.resource) ?? [];
1163
- list.push(c);
1164
- byResource.set(c.resource, list);
1205
+ for (const op of OPS) {
1206
+ const list = byResource.get(op.command[0]) ?? [];
1207
+ list.push(op);
1208
+ byResource.set(op.command[0], list);
1165
1209
  }
1210
+ return byResource;
1211
+ }
1212
+ /** One command with its flags: an entry of help --json --all. */
1213
+ function helpEntry(op, summary) {
1166
1214
  return {
1167
- schema_version: "2",
1168
- name: BIN,
1169
- version: VERSION,
1170
- api: API_TITLE,
1171
- api_version: API_VERSION,
1172
- spec_format: SPEC_FORMAT,
1215
+ command: op.command[1],
1216
+ method: summary.method,
1217
+ path: summary.path,
1218
+ ...(summary.summary ? { summary: summary.summary } : {}),
1219
+ paginated: summary.paginated,
1220
+ safety: op.safety,
1221
+ destructive: summary.destructive,
1222
+ auth: summary.auth,
1223
+ positional: op.params.filter((p) => p.kind === "path").map((p) => p.name),
1224
+ flags: summary.flags,
1225
+ details_command: BIN + " docs " + op.command[0] + " " + op.command[1] + " --json",
1226
+ };
1227
+ }
1228
+ function helpAll() {
1229
+ const summaries = commandSummaries();
1230
+ const byResource = opsByResource();
1231
+ return {
1232
+ ...helpHeader("all"),
1173
1233
  usage: BIN + " <resource> <command> [args] [--flags]",
1174
- resources: [...byResource.entries()].map(([resource, commands]) => ({
1234
+ resources: [...byResource.entries()].map(([resource, ops]) => ({
1175
1235
  resource,
1176
- commands: commands.map((c) => {
1177
- const op = OPS.find((o) => o.command[0] === resource && o.command[1] === c.command);
1178
- return {
1179
- command: c.command,
1180
- method: c.method,
1181
- path: c.path,
1182
- ...(c.summary ? { summary: c.summary } : {}),
1183
- paginated: c.paginated,
1184
- safety: op.safety,
1185
- destructive: c.destructive,
1186
- auth: c.auth,
1187
- positional: op.params.filter((p) => p.kind === "path").map((p) => p.name),
1188
- flags: c.flags,
1189
- details_command: BIN + " docs " + resource + " " + c.command + " --json",
1190
- };
1191
- }),
1236
+ commands: ops.map((op) => helpEntry(op, summaries[OPS.indexOf(op)])),
1192
1237
  })),
1193
- ...(EXCLUDED_OPS > 0 ? {
1194
- coverage: {
1195
- generated_operations: OPS.length,
1196
- total_operations: OPS.length + EXCLUDED_OPS,
1197
- },
1198
- } : {}),
1199
- discovery: {
1200
- search: BIN + " docs search <term> --json",
1201
- operation: BIN + " docs <resource> <command> --json",
1202
- note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
1203
- },
1238
+ ...helpCoverage(),
1239
+ discovery: helpDiscovery(),
1204
1240
  builtins: BUILTIN_COMMANDS,
1205
- global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--header 'Name: value'", "--timeout <seconds>", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
1241
+ global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--header 'Name: value'", "--timeout <seconds>", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--dry-run", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
1206
1242
  auth_env_vars: agentContext().authEnvVars,
1207
1243
  };
1208
1244
  }
1245
+ /** Resources and command names. Past HELP_INDEX_BYTES a resource keeps only its count. */
1246
+ function helpIndex() {
1247
+ const all = helpAll();
1248
+ let bytes = 0;
1249
+ let overBudget = false;
1250
+ let truncated = false;
1251
+ const resources = [...opsByResource().entries()].map(([resource, ops]) => {
1252
+ const names = ops.map((op) => op.command[1]);
1253
+ const entry = { resource, command_count: names.length, commands: names.slice(0, HELP_INDEX_NAMES) };
1254
+ const size = JSON.stringify(entry).length;
1255
+ if (overBudget || bytes + size > HELP_INDEX_BYTES) {
1256
+ overBudget = truncated = true;
1257
+ return { resource, command_count: names.length };
1258
+ }
1259
+ bytes += size;
1260
+ if (names.length > HELP_INDEX_NAMES)
1261
+ truncated = true;
1262
+ return entry;
1263
+ });
1264
+ return {
1265
+ ...helpHeader("index"),
1266
+ usage: all.usage,
1267
+ command_count: OPS.length,
1268
+ resource_count: resources.length,
1269
+ resources,
1270
+ ...(truncated ? { truncated: "Some resources list only their first " + HELP_INDEX_NAMES + " command names or only a count. Run " + BIN + " help <resource> --json for a resource's commands." } : {}),
1271
+ ...helpCoverage(),
1272
+ discovery: all.discovery,
1273
+ builtins: all.builtins,
1274
+ global_flags: all.global_flags,
1275
+ auth_env_vars: all.auth_env_vars,
1276
+ };
1277
+ }
1278
+ function helpTarget(resource, method) {
1279
+ const ops = opsByResource().get(resource);
1280
+ if (!ops) {
1281
+ const omitted = omittedCommand(resource, method);
1282
+ if (omitted)
1283
+ failOmitted(omitted);
1284
+ const suggestion = didYouMean(resource, opsByResource().keys());
1285
+ fail(2, "Unknown command: " + resource + "." + (suggestion ? " Did you mean '" + BIN + " help " + suggestion + " --json'?" : ""), undefined, ["Run '" + BIN + " help --json' for the resources and their commands."]);
1286
+ }
1287
+ if (method === undefined)
1288
+ return ops;
1289
+ const op = findOp(resource, method);
1290
+ if (!op) {
1291
+ const omitted = omittedCommand(resource, method);
1292
+ if (omitted)
1293
+ failOmitted(omitted);
1294
+ const suggestion = didYouMean(method, ops.map((o) => o.command[1]));
1295
+ fail(2, "Unknown command: " + resource + " " + method + "." + (suggestion ? " Did you mean '" + BIN + " help " + resource + " " + suggestion + " --json'?" : ""), undefined, ["Run '" + BIN + " help " + resource + " --json' for its commands."]);
1296
+ }
1297
+ return [op];
1298
+ }
1299
+ async function cmdHelpJson(parsed) {
1300
+ const [, resource, method, extra] = parsed.positionals;
1301
+ if (extra !== undefined)
1302
+ fail(2, "help --json takes at most a resource and a command.", undefined, ["Run '" + BIN + " help --json' for the index."]);
1303
+ const all = parsed.flags.get("all") === true;
1304
+ const pageFlag = parsed.flags.get("page");
1305
+ const page = pageFlag === undefined ? 1 : Number(pageFlag);
1306
+ if (!Number.isInteger(page) || page < 1)
1307
+ fail(2, "--page expects a whole number from 1.");
1308
+ if (resource === undefined) {
1309
+ if (pageFlag !== undefined)
1310
+ fail(2, "--page applies to help <resource> --json.");
1311
+ out(all ? helpAll() : helpIndex());
1312
+ await flushExit(0);
1313
+ }
1314
+ const ops = helpTarget(resource, method);
1315
+ const summaries = commandSummaries();
1316
+ if (method !== undefined) {
1317
+ out({ ...helpHeader("command"), resource, ...helpEntry(ops[0], summaries[OPS.indexOf(ops[0])]) });
1318
+ await flushExit(0);
1319
+ }
1320
+ if (all) {
1321
+ out({ ...helpHeader("resource"), resource, command_count: ops.length, commands: ops.map((op) => helpEntry(op, summaries[OPS.indexOf(op)])) });
1322
+ await flushExit(0);
1323
+ }
1324
+ const pages = Math.max(1, Math.ceil(ops.length / HELP_PAGE_SIZE));
1325
+ if (page > pages)
1326
+ fail(2, "--page " + page + " is past the last page (" + pages + ") of " + resource + ".", undefined, ["Run '" + BIN + " help " + resource + " --json' for the first page."]);
1327
+ const commands = ops.slice((page - 1) * HELP_PAGE_SIZE, page * HELP_PAGE_SIZE).map((op) => {
1328
+ const s = summaries[OPS.indexOf(op)];
1329
+ return { command: op.command[1], method: s.method, path: s.path, ...(s.summary ? { summary: s.summary } : {}), paginated: s.paginated, safety: op.safety, positional: op.params.filter((p) => p.kind === "path").map((p) => p.name) };
1330
+ });
1331
+ out({
1332
+ ...helpHeader("resource"),
1333
+ resource,
1334
+ command_count: ops.length,
1335
+ page,
1336
+ pages,
1337
+ commands,
1338
+ ...(page < pages ? { next_command: BIN + " help " + resource + " --json --page " + (page + 1) } : {}),
1339
+ discovery: { command: BIN + " help " + resource + " <command> --json", operation: BIN + " docs " + resource + " <command> --json", all: BIN + " help " + resource + " --json --all" },
1340
+ });
1341
+ await flushExit(0);
1342
+ }
1209
1343
  async function cmdAgentGuide(parsed) {
1210
1344
  if (parsed.help) {
1211
1345
  process.stdout.write(BIN + " agent-guide [--format json] — how an agent should drive this CLI: conventions, first command, docs, MCP, skills, next steps. JSON.\n");
@@ -1581,7 +1715,7 @@ function completionFlagsFor(op) {
1581
1715
  }
1582
1716
  return { flags, values };
1583
1717
  }
1584
- const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--header", "--timeout", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
1718
+ const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--header", "--timeout", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--dry-run", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
1585
1719
  const BUILTIN_WORDS = {
1586
1720
  config: ["list", "get", "set", "unset", "path"],
1587
1721
  completion: ["bash", "zsh", "fish"],
@@ -1742,7 +1876,9 @@ function referenceFor(op, includeSchemas = false) {
1742
1876
  lines.push("", paintOut("bold", label + ":"));
1743
1877
  for (const p of params) {
1744
1878
  const name = p.kind === "path" ? "<" + p.name + ">" : "--" + p.flag;
1745
- lines.push(" " + padPaint("cyan", name, 30) + typeLabel(p) + (p.required ? " " + paintOut("yellow", "(required)") : ""));
1879
+ // An object flag reads as its named type, which docs read explains.
1880
+ const named = objectTypeName(op, p);
1881
+ lines.push(" " + padPaint("cyan", name, 30) + (named ? INPUT_TYPES.args[op.tool][p.name] : typeLabel(p)) + (p.required ? " " + paintOut("yellow", "(required)") : ""));
1746
1882
  const values = p.type === "array" ? p.items?.enum : p.enum;
1747
1883
  if (values && values.join("|").length > 24)
1748
1884
  lines.push(" one of: " + values.join(", "));
@@ -1766,10 +1902,31 @@ function referenceFor(op, includeSchemas = false) {
1766
1902
  lines.push("", paintOut("bold", "Output schema:"), JSON.stringify(op.outputSchema, null, 2));
1767
1903
  }
1768
1904
  else {
1905
+ const nested = op.params.find((p) => objectTypeName(op, p) || p.type === "object" || (p.type === "array" && p.items?.type === "object"));
1906
+ if (nested) {
1907
+ const named = objectTypeName(op, nested);
1908
+ lines.push("", "Nested fields: " + BIN + " docs " + op.command.join(" ") + " --path " + nested.name + " gives a flag's type and all its fields; extend the path (" + nested.name + ".<field>) to go deeper."
1909
+ + (named ? " Named types: " + BIN + " docs read " + named + "." : ""));
1910
+ }
1769
1911
  lines.push("", "Add --schema for the complete input/output schemas, or --json for the machine contract.");
1770
1912
  }
1771
1913
  return lines.join("\n");
1772
1914
  }
1915
+ /** The named input object a flag takes (IssueFilter), when it has one. */
1916
+ function objectTypeName(op, p) {
1917
+ return namedTypesIn(INPUT_TYPES, INPUT_TYPES.args[op.tool]?.[p.name]).find((name) => INPUT_TYPES.types[name].fields);
1918
+ }
1919
+ function docsTypeHint(name) {
1920
+ return BIN + " docs read " + name;
1921
+ }
1922
+ /** docs --path: one nested argument, or a usage error naming the fields. */
1923
+ async function docsPathExit(root, path) {
1924
+ const found = argumentPathText(INPUT_TYPES, root, path, docsTypeHint);
1925
+ if (!found.ok)
1926
+ fail(2, found.message + (found.available.length > 0 ? " Fields: " + found.available.join(", ") + "." : ""));
1927
+ process.stdout.write(found.text + "\n");
1928
+ return flushExit(0);
1929
+ }
1773
1930
  /** Opens a browser for a person; under an agent it prints the URL instead of opening anything. */
1774
1931
  function openInBrowser(url, parsed) {
1775
1932
  if (parsed && nonInteractive(parsed)) {
@@ -1788,11 +1945,12 @@ async function cmdDocs(parsed) {
1788
1945
  "",
1789
1946
  " " + BIN + " docs overview",
1790
1947
  " " + BIN + " docs <resource> <command> operation contract and example",
1948
+ " --path <a.b> one nested argument's type and fields",
1791
1949
  " --schema include input/output JSON Schema",
1792
1950
  " --json print the machine contract as JSON",
1793
1951
  " " + BIN + " docs search <term> search reference and guides",
1794
1952
  " --json / --format json print structured matches and availability",
1795
- " " + BIN + " docs read <page> print a docs-site page in the terminal",
1953
+ " " + BIN + " docs read <page> print a named input type or a docs-site page",
1796
1954
  " " + BIN + " docs --web open the docs site in a browser",
1797
1955
  "",
1798
1956
  "Guides come from the docs site's llms.txt (set with '" + BIN + " config set docs-url <url>').",
@@ -1864,6 +2022,14 @@ async function cmdDocs(parsed) {
1864
2022
  const page = parsed.positionals[2];
1865
2023
  if (page === undefined)
1866
2024
  fail(2, "docs read expects a page path or URL");
2025
+ const typeName = findInputType(INPUT_TYPES, page);
2026
+ if (typeName) {
2027
+ const path = parsed.flags.get("path");
2028
+ if (typeof path === "string" && path.trim())
2029
+ await docsPathExit({ type: typeName }, path);
2030
+ process.stdout.write(inputTypeText(INPUT_TYPES, typeName, docsTypeHint) + "\n");
2031
+ await flushExit(0);
2032
+ }
1867
2033
  let target = page;
1868
2034
  if (!/^https?:\/\//.test(target)) {
1869
2035
  const index = await fetchDocs("llms.txt");
@@ -1903,6 +2069,9 @@ async function cmdDocs(parsed) {
1903
2069
  }, null, 2) + "\n");
1904
2070
  await flushExit(0);
1905
2071
  }
2072
+ const path = parsed.flags.get("path");
2073
+ if (typeof path === "string" && path.trim())
2074
+ await docsPathExit({ tool: op.tool, inputSchema: op.inputSchema, label: op.command.join(" ") }, path);
1906
2075
  process.stdout.write(referenceFor(op, parsed.flags.get("schema") === true) + "\n");
1907
2076
  await flushExit(0);
1908
2077
  }
@@ -2088,7 +2257,7 @@ function printRoot(stream = process.stdout) {
2088
2257
  }
2089
2258
  const width = termWidth();
2090
2259
  const lines = [];
2091
- lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.23.1");
2260
+ lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.24.0");
2092
2261
  lines.push("");
2093
2262
  lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
2094
2263
  lines.push("");
@@ -2111,7 +2280,7 @@ function printRoot(stream = process.stdout) {
2111
2280
  lines.push(...labeled(paintOut("yellow", "Coverage:") + " ", "this build includes " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations; api.json lists the rest", width, 14));
2112
2281
  }
2113
2282
  lines.push("");
2114
- const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --header \"Name: value\", --timeout <seconds>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check parameters and JSON bodies)" +
2283
+ const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --header \"Name: value\", --timeout <seconds>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --format table (results as text), --validate (schema-check parameters and JSON bodies), --dry-run (print the request, send nothing)" +
2115
2284
  (AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
2116
2285
  lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
2117
2286
  lines.push(...labeled("Credential env vars: ", [
@@ -2179,6 +2348,9 @@ function commandExtras(op) {
2179
2348
  extras.push(["--fields <a,b.c>", "keep only these fields of the result" + (op.paginated ? " (per item)" : collectionField ? " (per item in " + collectionField + ")" : "")]);
2180
2349
  if ((op.fileBundleProperty ?? bundleProperty(op.outputSchema)) !== null)
2181
2350
  extras.push(["--out <dir>", "write the response's files ({path, content}) into a directory"]);
2351
+ extras.push(["--dry-run", "print the resolved request (method, URL, headers, body) with credentials redacted; nothing is sent" + (op.safety === "destructive" ? ", so no --force is needed" : "")]);
2352
+ if (!op.rawResponse && !op.sse)
2353
+ extras.push(["--format table", "print the result as a table for reading instead of JSON"]);
2182
2354
  if (op.safety === "destructive")
2183
2355
  extras.push(["--force, -y", "destructive: required without a terminal, skips the prompt with one"]);
2184
2356
  return extras;
@@ -2442,6 +2614,73 @@ function validateParameters(op, values, flags) {
2442
2614
  let LAST_CLIENT_HAD_CREDENTIAL = false;
2443
2615
  /** The last client's Basic credentials, for path arguments that default to the username. */
2444
2616
  let LAST_CLIENT_BASIC = {};
2617
+ /** --dry-run: the client's fetch records the first request and sends
2618
+ * nothing. Set before makeClient; secrets are the credential values it
2619
+ * resolved, redacted wherever they appear in the preview. */
2620
+ let DRY_RUN = null;
2621
+ class DryRunStop extends Error {
2622
+ }
2623
+ function dryRunCapture() {
2624
+ const capture = {
2625
+ secrets: [],
2626
+ fetch: async (input, init) => {
2627
+ if (!capture.request) {
2628
+ const headers = {};
2629
+ new Headers(init?.headers).forEach((value, name) => { headers[name] = value; });
2630
+ capture.request = { method: init?.method ?? "GET", url: typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url, headers, body: init?.body ?? undefined };
2631
+ }
2632
+ throw new DryRunStop("dry run: not sent");
2633
+ },
2634
+ };
2635
+ return capture;
2636
+ }
2637
+ /** Every credential value a client was built with (not a Basic username,
2638
+ * which is an account identifier such as Twilio's AccountSid). */
2639
+ function credentialSecrets(options) {
2640
+ const found = [];
2641
+ const walk = (value, key) => {
2642
+ if (typeof value === "string") {
2643
+ if (key !== "username")
2644
+ found.push(value);
2645
+ return;
2646
+ }
2647
+ if (value && typeof value === "object")
2648
+ for (const [k, v] of Object.entries(value))
2649
+ walk(v, k);
2650
+ };
2651
+ for (const a of AUTH_SCALARS)
2652
+ walk(options[a.option], a.option);
2653
+ walk(options.basicAuth, "basicAuth");
2654
+ walk(options.credentials, "credentials");
2655
+ walk(options.bearerToken, "bearerToken");
2656
+ return found;
2657
+ }
2658
+ /** Drive the command's call until its request reaches the dry-run fetch,
2659
+ * then print that request (JSON in agent mode, text for a person) and exit.
2660
+ * A failure before any request (a --validate violation) is reported as usual. */
2661
+ async function printDryRun(parsed, callResult) {
2662
+ let failure;
2663
+ try {
2664
+ const pending = callResult;
2665
+ if (typeof pending.then === "function")
2666
+ await callResult;
2667
+ else if (typeof pending[Symbol.asyncIterator] === "function")
2668
+ await pending[Symbol.asyncIterator]().next();
2669
+ }
2670
+ catch (e) {
2671
+ failure = e;
2672
+ }
2673
+ const request = DRY_RUN?.request;
2674
+ if (!request) {
2675
+ failApi(failure ?? new Error("--dry-run: the command made no request"), LAST_CLIENT_HAD_CREDENTIAL);
2676
+ }
2677
+ const preview = await requestPreview(request, { sensitiveNames: CREDENTIAL_NAMES, secrets: DRY_RUN.secrets });
2678
+ if (isAgentMode(parsed))
2679
+ out(preview);
2680
+ else
2681
+ process.stdout.write(formatRequestPreview(preview));
2682
+ return await flushExit(0);
2683
+ }
2445
2684
  /** The configured Basic-auth username: the named scheme's, else basicAuth's. */
2446
2685
  function credentialUsername(scheme) {
2447
2686
  const named = LAST_CLIENT_BASIC.credentials?.[scheme];
@@ -2677,6 +2916,12 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
2677
2916
  const extraHeaders = extraRequestHeaders();
2678
2917
  if (Object.keys(extraHeaders).length)
2679
2918
  options.onRequest = (context) => { applyExtraHeaders(context.headers, extraHeaders); };
2919
+ // --dry-run swaps fetch on this client only: an OAuth refresh (and its
2920
+ // identity check) still uses the real transport, the command does not.
2921
+ if (DRY_RUN && !forIdentity) {
2922
+ DRY_RUN.secrets = credentialSecrets(options);
2923
+ return new TypeshipClient({ ...options, fetch: DRY_RUN.fetch, maxRetries: 0 });
2924
+ }
2680
2925
  return new TypeshipClient(options);
2681
2926
  }
2682
2927
  function editDistance(a, b) {
@@ -2735,16 +2980,20 @@ async function main() {
2735
2980
  if (!parsed.help && ["login", "init"].includes(parsed.positionals[0] ?? ""))
2736
2981
  expectedLoginIdentity(parsed.flags);
2737
2982
  if (parsed.positionals[0] === "help") {
2738
- // help --json: the command surface as data (agents read this once).
2983
+ // help [<resource> [<command>]] --json: bounded discovery as data; --all is exhaustive.
2739
2984
  if (parsed.flags.get("json") === true || parsed.flags.get("format") === "json") {
2740
- out(helpJson());
2741
- await flushExit(0);
2985
+ await cmdHelpJson(parsed);
2742
2986
  }
2743
2987
  parsed.positionals.shift();
2744
2988
  parsed.help = true;
2745
2989
  }
2746
- if (parsed.flags.get("format") !== undefined && parsed.flags.get("format") !== "json") {
2747
- fail(2, "--format json is the only format; output is always JSON.");
2990
+ const formatFlag = parsed.flags.get("format");
2991
+ if (formatFlag !== undefined && formatFlag !== "json" && formatFlag !== "table") {
2992
+ fail(2, "--format takes json (the default) or table.");
2993
+ }
2994
+ TABLE = formatFlag === "table";
2995
+ if (TABLE && !parsed.help && (BUILTIN_COMMANDS.includes(parsed.positionals[0] ?? "") || parsed.positionals.length === 0)) {
2996
+ fail(2, "--format table applies to API commands; " + (parsed.positionals[0] ? BIN + " " + parsed.positionals[0] : BIN) + " prints JSON.");
2748
2997
  }
2749
2998
  if (parsed.flags.has("version") || parsed.positionals[0] === "version") {
2750
2999
  // "acme 1.0.0 (acme 1.0.0)" would say the name twice; when the API's
@@ -2874,7 +3123,7 @@ async function main() {
2874
3123
  // Mirrors opReservedFlags() in the generator: API parameters never use these
2875
3124
  // names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
2876
3125
  // check can be exact.
2877
- const RESERVED_FLAGS = new Set(["data", "credentials", "header", "timeout", "all", "select", "base-url", "profile", "debug", "validate", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
3126
+ const RESERVED_FLAGS = new Set(["data", "credentials", "header", "timeout", "all", "select", "base-url", "profile", "debug", "validate", "dry-run", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
2878
3127
  for (const spec of op.params) {
2879
3128
  if (spec.kind === "path")
2880
3129
  continue;
@@ -2971,6 +3220,20 @@ async function main() {
2971
3220
  if (!op.paginated && parsed.flags.get("all") === true) {
2972
3221
  fail(2, op.command.join(" ") + " does not paginate, so --all has nothing to walk. Run it without --all; it returns the whole response.");
2973
3222
  }
3223
+ // --dry-run resolves everything a real call would (arguments, defaults,
3224
+ // credentials, the body encoding) and prints the request instead of
3225
+ // sending it. A destructive command needs no --force: nothing runs.
3226
+ if (parsed.flags.get("dry-run") === true) {
3227
+ if (op.paginated && parsed.flags.get("all") === true)
3228
+ fail(2, "--dry-run previews one request; run it without --all to see the first page's.");
3229
+ DRY_RUN = dryRunCapture();
3230
+ }
3231
+ // A table needs the whole result: streams (--all, events) and raw bodies stay as they are.
3232
+ if (TABLE) {
3233
+ const streams = parsed.flags.get("all") === true || op.sse || (op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value);
3234
+ if (streams || op.rawResponse)
3235
+ fail(2, "--format table needs one complete JSON result; " + op.command.join(" ") + (op.rawResponse ? " returns a raw body." : " streams NDJSON here.") + " Drop --format table" + (parsed.flags.get("all") === true ? " or --all." : "."));
3236
+ }
2974
3237
  const client = await makeClient(parsed.flags, op);
2975
3238
  for (const spec of pathSpecs) {
2976
3239
  if (!spec.credential || values[spec.name] !== undefined)
@@ -2984,7 +3247,7 @@ async function main() {
2984
3247
  // Destructive commands need --force. A person gets asked; an agent gets
2985
3248
  // an action_required envelope with the exact command to run, so nothing
2986
3249
  // is deleted on a guess.
2987
- if (op.safety === "destructive" && !assumeYes(parsed)) {
3250
+ if (op.safety === "destructive" && !assumeYes(parsed) && !DRY_RUN) {
2988
3251
  // Credential flags are replaced by placeholders: the rerun is shown to
2989
3252
  // agents and logged, so it must never repeat a key.
2990
3253
  const secretFlags = new Set([...AUTH_SCALARS.map((a) => "--" + a.flag), "--header"]);
@@ -3016,8 +3279,10 @@ async function main() {
3016
3279
  // as NDJSON as they arrive, instead of failing on the event stream.
3017
3280
  const streaming = op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value;
3018
3281
  const callResult = target[streaming ? op.streamMethod.method : op.method](...args);
3282
+ if (DRY_RUN)
3283
+ await printDryRun(parsed, callResult);
3019
3284
  if (op.paginated && parsed.flags.get("all") === true) {
3020
- const fieldsCheck = streamFieldsCheck();
3285
+ const fieldsCheck = streamFieldsCheck(listSchemaOf(op.outputSchema, "items")?.items);
3021
3286
  try {
3022
3287
  for await (const item of callResult) {
3023
3288
  process.stdout.write(JSON.stringify(fieldsCheck.item(item)) + "\n");
@@ -3051,7 +3316,7 @@ async function main() {
3051
3316
  if (result.ok) {
3052
3317
  if (op.sse || streaming) {
3053
3318
  // Server-sent events as NDJSON, one line per event, until the stream ends.
3054
- const fieldsCheck = streamFieldsCheck();
3319
+ const fieldsCheck = streamFieldsCheck(undefined);
3055
3320
  try {
3056
3321
  for await (const event of result.data) {
3057
3322
  process.stdout.write(JSON.stringify(fieldsCheck.item(event)) + "\n");
@@ -3100,18 +3365,18 @@ async function main() {
3100
3365
  // agent never has to reconstruct the cursor flag.
3101
3366
  const page = result.data;
3102
3367
  const next = page.nextPageParams();
3103
- out({
3104
- items: project(page.items, true),
3368
+ printResult({
3369
+ items: project(page.items, true, listSchemaOf(op.outputSchema, "items")),
3105
3370
  hasMore: next !== null,
3106
3371
  ...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
3107
3372
  ...(page.response.requestId ? { request_id: page.response.requestId } : {}),
3108
- });
3373
+ }, op.command[0]);
3109
3374
  }
3110
3375
  else if (FIELDS !== null && collectionField !== null && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) && Array.isArray(result.data[collectionField])) {
3111
3376
  // Batch-style collection envelopes ({data: [...]}) use item-relative
3112
3377
  // fields, matching paginated results, while retaining envelope metadata.
3113
3378
  const data = result.data;
3114
- out({ ...data, [collectionField]: project(data[collectionField], true) });
3379
+ printResult({ ...data, [collectionField]: project(data[collectionField], true, listSchemaOf(op.outputSchema, collectionField)) }, op.command[0], collectionField);
3115
3380
  }
3116
3381
  else if (outDir !== undefined && bundleField !== null) {
3117
3382
  // --out: a file-shaped response (an array of {path, content}) lands
@@ -3120,10 +3385,10 @@ async function main() {
3120
3385
  const files = data[bundleField] ?? [];
3121
3386
  const written = writeBundle(outDir, files);
3122
3387
  const { [bundleField]: _omitted, ...rest } = data;
3123
- out({ ...project(rest, false), out: written });
3388
+ printResult({ ...project(rest, false, op.outputSchema), out: written }, op.command[0]);
3124
3389
  }
3125
3390
  else {
3126
- out(result.data === undefined || result.data === null ? { ok: true } : project(result.data, Array.isArray(result.data)));
3391
+ printResult(result.data === undefined || result.data === null ? { ok: true } : project(result.data, Array.isArray(result.data), op.outputSchema), op.command[0], collectionField);
3127
3392
  }
3128
3393
  await flushExit(0);
3129
3394
  }