@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.
- package/AGENTS.md +2 -2
- package/README.md +2 -2
- package/api.md +2 -2
- package/dist/arguments.d.ts +9 -2
- package/dist/arguments.d.ts.map +1 -1
- package/dist/arguments.js +17 -6
- package/dist/cli-agent.d.ts +42 -1
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +187 -18
- package/dist/cli.js +341 -76
- package/dist/fields.d.ts +8 -1
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +91 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/ops.d.ts +5 -0
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +66 -6
- package/dist/resources/deliveries.d.ts +1 -1
- package/dist/resources/deliveries.d.ts.map +1 -1
- package/dist/resources/deliveries.js +1 -1
- package/dist/resources/drafts.d.ts +1 -1
- package/dist/resources/drafts.d.ts.map +1 -1
- package/dist/resources/drafts.js +1 -1
- package/dist/resources/generations.d.ts +2 -2
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +2 -2
- package/dist/resources/releases.d.ts +1 -1
- package/dist/resources/releases.d.ts.map +1 -1
- package/dist/resources/releases.js +1 -1
- package/dist/resources/spec-revisions.d.ts +1 -1
- package/dist/resources/spec-revisions.d.ts.map +1 -1
- package/dist/resources/spec-revisions.js +1 -1
- package/dist/resources/targets.d.ts +1 -1
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +1 -1
- package/dist/table.d.ts +28 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +167 -0
- package/dist/type-docs.d.ts +61 -0
- package/dist/type-docs.d.ts.map +1 -0
- package/dist/type-docs.js +174 -0
- package/package.json +1 -1
- package/src/arguments.ts +19 -7
- package/src/cli-agent.ts +196 -17
- package/src/cli.ts +330 -76
- package/src/fields.ts +81 -5
- package/src/index.ts +3 -3
- package/src/ops.ts +69 -6
- package/src/resources/deliveries.ts +2 -2
- package/src/resources/drafts.ts +2 -2
- package/src/resources/generations.ts +4 -4
- package/src/resources/releases.ts +2 -2
- package/src/resources/spec-revisions.ts +2 -2
- package/src/resources/targets.ts +2 -2
- package/src/table.ts +167 -0
- package/src/type-docs.ts +205 -0
package/src/cli.ts
CHANGED
|
@@ -22,17 +22,19 @@ import { fileURLToPath } from "node:url";
|
|
|
22
22
|
import { TypeshipClient, formatDebugEvent, type ClientOptions, type DebugEvent } from "./index.js";
|
|
23
23
|
import { asApiResult, mediaTypeForPath, validateAgainstSchema, ValidationError, type Violation } from "./core/http.js";
|
|
24
24
|
import { SCHEMAS, DEFS } from "./schemas.js";
|
|
25
|
-
import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired, type OmittedOpSpec, type OpSpec, type ParamSpec } from "./ops.js";
|
|
25
|
+
import { GLOBALS, INPUT_TYPES, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired, type OmittedOpSpec, type OpSpec, type ParamSpec } from "./ops.js";
|
|
26
26
|
import {
|
|
27
27
|
MCP_CLIENTS, requiredScopes, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, classifyAuthFailure, collectionProperty, detectHarness, envelope,
|
|
28
|
-
exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig,
|
|
28
|
+
exitCodeFor, findMcpClient, formatRequestPreview, installSkills, mcpConfigured, pendingClaims, recordClaim, requestPreview, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig,
|
|
29
29
|
type AgentContext, type CommandSummary, type DoctorCheck, type EnvelopeInput, type IssueCode, type McpEntry, type McpWriteResult,
|
|
30
30
|
} from "./cli-agent.js";
|
|
31
31
|
import { relativeDate } from "./dates.js";
|
|
32
32
|
import { checkValue, type ArgumentIssue } from "./arguments.js";
|
|
33
33
|
import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
|
|
34
34
|
import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
|
|
35
|
+
import { renderTable } from "./table.js";
|
|
35
36
|
import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
|
|
37
|
+
import { argumentPathText, findInputType, inputTypeText, namedTypesIn } from "./type-docs.js";
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
const BIN = "typeship";
|
|
@@ -46,6 +48,8 @@ const AUTH_SCALARS: { option: string; flag: string; env: string }[] = [{"option"
|
|
|
46
48
|
const HOSTED_MCP_HEADERS: Record<string, string> = {"Authorization":"Bearer ${TYPESHIP_TOKEN}"};
|
|
47
49
|
const HOSTED_MCP_NOTE: string | null = null;
|
|
48
50
|
const BASIC: { envUser: string; envPass: string } | null = null;
|
|
51
|
+
/** Header and query names the API's key schemes use: --dry-run redacts them. */
|
|
52
|
+
const CREDENTIAL_NAMES: string[] = [];
|
|
49
53
|
/** The spec declares no security, so the token is offered, never required. */
|
|
50
54
|
const AUTH_UNDECLARED = false;
|
|
51
55
|
/** Repeated --header "Name: value" flags for this invocation. */
|
|
@@ -54,7 +58,7 @@ let HEADER_FLAGS: string[] = [];
|
|
|
54
58
|
const EXCLUDED_OPS = 0;
|
|
55
59
|
/** Generated CLI operations that are intentionally unavailable to MCP. */
|
|
56
60
|
const MCP_EXCLUDED_OPS = 0;
|
|
57
|
-
const VERSION = "0.
|
|
61
|
+
const VERSION = "0.24.0";
|
|
58
62
|
const API_VERSION = "1.0.0";
|
|
59
63
|
const SPEC_FORMAT = "openapi";
|
|
60
64
|
const IDENTITY_POLICY: IdentityPolicy = {};
|
|
@@ -109,7 +113,7 @@ interface Parsed {
|
|
|
109
113
|
* API parameter with the same name (`accounts list --cursor <c>`) still
|
|
110
114
|
* takes its value. Boolean API parameters are recognized once the command
|
|
111
115
|
* is known. */
|
|
112
|
-
const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json"]);
|
|
116
|
+
const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json", "dry-run"]);
|
|
113
117
|
const BUILTIN_BOOLEAN_FLAGS: Record<string, string[]> = {
|
|
114
118
|
login: ["with-token", "no-browser", "device"],
|
|
115
119
|
logout: ["local"],
|
|
@@ -240,28 +244,43 @@ function out(value: unknown): void {
|
|
|
240
244
|
/** --fields a,b.c: the dotted paths to keep in API results (null = everything). Set in main(). */
|
|
241
245
|
let FIELDS: string[][] | null = null;
|
|
242
246
|
|
|
247
|
+
/** --format table: print API results as text for a person instead of JSON. Opt-in only; set in main(). */
|
|
248
|
+
let TABLE = false;
|
|
249
|
+
|
|
250
|
+
/** An API result on stdout: JSON, or the --format table view of the same value. */
|
|
251
|
+
function printResult(value: unknown, resource: string, collectionField: string | null = null): void {
|
|
252
|
+
if (!TABLE) { out(value); return; }
|
|
253
|
+
process.stdout.write(renderTable(value, { width: process.stdout.columns || 120, heading: (text) => paintOut("bold", text), collectionField, resource }));
|
|
254
|
+
}
|
|
255
|
+
|
|
243
256
|
/** Whether the command being run writes: an --fields mistake on a write
|
|
244
257
|
* must not tempt anyone into running it again. Set in main(). */
|
|
245
258
|
let FIELDS_AFTER_WRITE = false;
|
|
246
259
|
|
|
247
260
|
/** Keep only FIELDS of a result: arrays item by item, objects by dotted path;
|
|
248
261
|
* scalars untouched. A path that matches nothing is an error naming the keys
|
|
249
|
-
* that exist, never a silent {}
|
|
250
|
-
|
|
262
|
+
* that exist, never a silent {}; one the response schema declares (an
|
|
263
|
+
* optional key no item has) is simply absent. */
|
|
264
|
+
function project(value: unknown, perItem: boolean, schema: unknown): unknown {
|
|
251
265
|
if (FIELDS === null) return value;
|
|
252
|
-
const unmatched = unmatchedFields(value, FIELDS);
|
|
266
|
+
const unmatched = unmatchedFields(value, FIELDS, schema);
|
|
253
267
|
if (unmatched.length > 0) failUnmatchedFields(unmatched, perItem, value);
|
|
254
268
|
return projectFields(value, FIELDS);
|
|
255
269
|
}
|
|
256
270
|
|
|
271
|
+
/** The declared schema of the list a result holds under `key`. */
|
|
272
|
+
function listSchemaOf(schema: Record<string, unknown> | undefined, key: string): unknown {
|
|
273
|
+
return (schema?.properties as Record<string, unknown> | undefined)?.[key];
|
|
274
|
+
}
|
|
275
|
+
|
|
257
276
|
/** --fields over a stream (--all, events): paths no item has matched yet.
|
|
258
277
|
* The stream is printed as it arrives, so the check fails at its end. */
|
|
259
|
-
function streamFieldsCheck(): { item(value: unknown): unknown; finish(): void } {
|
|
278
|
+
function streamFieldsCheck(schema: unknown): { item(value: unknown): unknown; finish(): void } {
|
|
260
279
|
let pending: ReturnType<typeof unmatchedFields> | null = null;
|
|
261
280
|
return {
|
|
262
281
|
item(value: unknown): unknown {
|
|
263
282
|
if (FIELDS === null) return value;
|
|
264
|
-
const unmatched = unmatchedFields(value, FIELDS);
|
|
283
|
+
const unmatched = unmatchedFields(value, FIELDS, schema);
|
|
265
284
|
pending = pending === null ? unmatched : pending.filter((p) => unmatched.some((u) => u.path === p.path));
|
|
266
285
|
return projectFields(value, FIELDS);
|
|
267
286
|
},
|
|
@@ -275,10 +294,13 @@ function failUnmatchedFields(unmatched: ReturnType<typeof unmatchedFields>, perI
|
|
|
275
294
|
return failWith({
|
|
276
295
|
code: "FIELDS_UNMATCHED",
|
|
277
296
|
message: unmatchedFieldsMessage(unmatched, perItem),
|
|
278
|
-
nextSteps:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
297
|
+
nextSteps: [
|
|
298
|
+
FIELDS_AFTER_WRITE
|
|
299
|
+
? "This command has already run; do not run it again to change --fields." + (result !== undefined ? " Its full result is in detail.result." : "")
|
|
300
|
+
: result !== undefined ? "The full result is in detail.result; use it rather than running the command again." : "",
|
|
301
|
+
(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.",
|
|
302
|
+
].filter((step) => step !== ""),
|
|
303
|
+
detail: { unmatched, ...(result !== undefined ? { result } : {}) },
|
|
282
304
|
});
|
|
283
305
|
}
|
|
284
306
|
|
|
@@ -1139,61 +1161,173 @@ function commandSummaries(): CommandSummary[] {
|
|
|
1139
1161
|
}
|
|
1140
1162
|
|
|
1141
1163
|
/**
|
|
1142
|
-
* help --json:
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1164
|
+
* help --json: discovery as data, bounded so one call cannot fill an agent's
|
|
1165
|
+
* context window on a large API. The default is an index of resources and
|
|
1166
|
+
* command names; "help <resource> --json" pages through one resource's
|
|
1167
|
+
* commands with their methods, paths and summaries; "help <resource>
|
|
1168
|
+
* <command> --json" is one command with its flags; "help --json --all" is
|
|
1169
|
+
* every command with every flag. Full schemas stay behind the docs command.
|
|
1145
1170
|
*/
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1171
|
+
const HELP_INDEX_NAMES = 40;
|
|
1172
|
+
const HELP_INDEX_BYTES = 16_000;
|
|
1173
|
+
const HELP_PAGE_SIZE = 50;
|
|
1174
|
+
|
|
1175
|
+
function helpHeader(detail: "index" | "resource" | "command" | "all"): Record<string, unknown> {
|
|
1176
|
+
return { schema_version: "3", detail, name: BIN, version: VERSION, api: API_TITLE, api_version: API_VERSION, spec_format: SPEC_FORMAT };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function helpDiscovery(): Record<string, string> {
|
|
1180
|
+
return {
|
|
1181
|
+
resource: BIN + " help <resource> --json",
|
|
1182
|
+
command: BIN + " help <resource> <command> --json",
|
|
1183
|
+
search: BIN + " docs search <term> --json",
|
|
1184
|
+
operation: BIN + " docs <resource> <command> --json",
|
|
1185
|
+
all: BIN + " help --json --all",
|
|
1186
|
+
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.",
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function helpCoverage(): Record<string, unknown> {
|
|
1191
|
+
return EXCLUDED_OPS > 0 ? { coverage: { generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS } } : {};
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function opsByResource(): Map<string, OpSpec[]> {
|
|
1195
|
+
const byResource = new Map<string, OpSpec[]>();
|
|
1196
|
+
for (const op of OPS) {
|
|
1197
|
+
const list = byResource.get(op.command[0]) ?? [];
|
|
1198
|
+
list.push(op);
|
|
1199
|
+
byResource.set(op.command[0], list);
|
|
1152
1200
|
}
|
|
1201
|
+
return byResource;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/** One command with its flags: an entry of help --json --all. */
|
|
1205
|
+
function helpEntry(op: OpSpec, summary: CommandSummary): Record<string, unknown> {
|
|
1153
1206
|
return {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1207
|
+
command: op.command[1],
|
|
1208
|
+
method: summary.method,
|
|
1209
|
+
path: summary.path,
|
|
1210
|
+
...(summary.summary ? { summary: summary.summary } : {}),
|
|
1211
|
+
paginated: summary.paginated,
|
|
1212
|
+
safety: op.safety,
|
|
1213
|
+
destructive: summary.destructive,
|
|
1214
|
+
auth: summary.auth,
|
|
1215
|
+
positional: op.params.filter((p) => p.kind === "path").map((p) => p.name),
|
|
1216
|
+
flags: summary.flags,
|
|
1217
|
+
details_command: BIN + " docs " + op.command[0] + " " + op.command[1] + " --json",
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function helpAll(): Record<string, unknown> {
|
|
1222
|
+
const summaries = commandSummaries();
|
|
1223
|
+
const byResource = opsByResource();
|
|
1224
|
+
return {
|
|
1225
|
+
...helpHeader("all"),
|
|
1160
1226
|
usage: BIN + " <resource> <command> [args] [--flags]",
|
|
1161
|
-
resources: [...byResource.entries()].map(([resource,
|
|
1227
|
+
resources: [...byResource.entries()].map(([resource, ops]) => ({
|
|
1162
1228
|
resource,
|
|
1163
|
-
commands:
|
|
1164
|
-
const op = OPS.find((o) => o.command[0] === resource && o.command[1] === c.command)!;
|
|
1165
|
-
return {
|
|
1166
|
-
command: c.command,
|
|
1167
|
-
method: c.method,
|
|
1168
|
-
path: c.path,
|
|
1169
|
-
...(c.summary ? { summary: c.summary } : {}),
|
|
1170
|
-
paginated: c.paginated,
|
|
1171
|
-
safety: op.safety,
|
|
1172
|
-
destructive: c.destructive,
|
|
1173
|
-
auth: c.auth,
|
|
1174
|
-
positional: op.params.filter((p) => p.kind === "path").map((p) => p.name),
|
|
1175
|
-
flags: c.flags,
|
|
1176
|
-
details_command: BIN + " docs " + resource + " " + c.command + " --json",
|
|
1177
|
-
};
|
|
1178
|
-
}),
|
|
1229
|
+
commands: ops.map((op) => helpEntry(op, summaries[OPS.indexOf(op)]!)),
|
|
1179
1230
|
})),
|
|
1180
|
-
...(
|
|
1181
|
-
|
|
1182
|
-
generated_operations: OPS.length,
|
|
1183
|
-
total_operations: OPS.length + EXCLUDED_OPS,
|
|
1184
|
-
},
|
|
1185
|
-
} : {}),
|
|
1186
|
-
discovery: {
|
|
1187
|
-
search: BIN + " docs search <term> --json",
|
|
1188
|
-
operation: BIN + " docs <resource> <command> --json",
|
|
1189
|
-
note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
|
|
1190
|
-
},
|
|
1231
|
+
...helpCoverage(),
|
|
1232
|
+
discovery: helpDiscovery(),
|
|
1191
1233
|
builtins: BUILTIN_COMMANDS,
|
|
1192
|
-
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>")],
|
|
1234
|
+
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>")],
|
|
1193
1235
|
auth_env_vars: agentContext().authEnvVars,
|
|
1194
1236
|
};
|
|
1195
1237
|
}
|
|
1196
1238
|
|
|
1239
|
+
/** Resources and command names. Past HELP_INDEX_BYTES a resource keeps only its count. */
|
|
1240
|
+
function helpIndex(): Record<string, unknown> {
|
|
1241
|
+
const all = helpAll();
|
|
1242
|
+
let bytes = 0;
|
|
1243
|
+
let overBudget = false;
|
|
1244
|
+
let truncated = false;
|
|
1245
|
+
const resources = [...opsByResource().entries()].map(([resource, ops]) => {
|
|
1246
|
+
const names = ops.map((op) => op.command[1]);
|
|
1247
|
+
const entry = { resource, command_count: names.length, commands: names.slice(0, HELP_INDEX_NAMES) };
|
|
1248
|
+
const size = JSON.stringify(entry).length;
|
|
1249
|
+
if (overBudget || bytes + size > HELP_INDEX_BYTES) { overBudget = truncated = true; return { resource, command_count: names.length }; }
|
|
1250
|
+
bytes += size;
|
|
1251
|
+
if (names.length > HELP_INDEX_NAMES) truncated = true;
|
|
1252
|
+
return entry;
|
|
1253
|
+
});
|
|
1254
|
+
return {
|
|
1255
|
+
...helpHeader("index"),
|
|
1256
|
+
usage: all.usage,
|
|
1257
|
+
command_count: OPS.length,
|
|
1258
|
+
resource_count: resources.length,
|
|
1259
|
+
resources,
|
|
1260
|
+
...(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." } : {}),
|
|
1261
|
+
...helpCoverage(),
|
|
1262
|
+
discovery: all.discovery,
|
|
1263
|
+
builtins: all.builtins,
|
|
1264
|
+
global_flags: all.global_flags,
|
|
1265
|
+
auth_env_vars: all.auth_env_vars,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function helpTarget(resource: string, method: string | undefined): OpSpec[] {
|
|
1270
|
+
const ops = opsByResource().get(resource);
|
|
1271
|
+
if (!ops) {
|
|
1272
|
+
const omitted = omittedCommand(resource, method);
|
|
1273
|
+
if (omitted) failOmitted(omitted);
|
|
1274
|
+
const suggestion = didYouMean(resource, opsByResource().keys());
|
|
1275
|
+
fail(2, "Unknown command: " + resource + "." + (suggestion ? " Did you mean '" + BIN + " help " + suggestion + " --json'?" : ""),
|
|
1276
|
+
undefined, ["Run '" + BIN + " help --json' for the resources and their commands."]);
|
|
1277
|
+
}
|
|
1278
|
+
if (method === undefined) return ops!;
|
|
1279
|
+
const op = findOp(resource, method);
|
|
1280
|
+
if (!op) {
|
|
1281
|
+
const omitted = omittedCommand(resource, method);
|
|
1282
|
+
if (omitted) failOmitted(omitted);
|
|
1283
|
+
const suggestion = didYouMean(method, ops!.map((o) => o.command[1]));
|
|
1284
|
+
fail(2, "Unknown command: " + resource + " " + method + "." + (suggestion ? " Did you mean '" + BIN + " help " + resource + " " + suggestion + " --json'?" : ""),
|
|
1285
|
+
undefined, ["Run '" + BIN + " help " + resource + " --json' for its commands."]);
|
|
1286
|
+
}
|
|
1287
|
+
return [op!];
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async function cmdHelpJson(parsed: Parsed): Promise<void> {
|
|
1291
|
+
const [, resource, method, extra] = parsed.positionals;
|
|
1292
|
+
if (extra !== undefined) fail(2, "help --json takes at most a resource and a command.", undefined, ["Run '" + BIN + " help --json' for the index."]);
|
|
1293
|
+
const all = parsed.flags.get("all") === true;
|
|
1294
|
+
const pageFlag = parsed.flags.get("page");
|
|
1295
|
+
const page = pageFlag === undefined ? 1 : Number(pageFlag);
|
|
1296
|
+
if (!Number.isInteger(page) || page < 1) fail(2, "--page expects a whole number from 1.");
|
|
1297
|
+
if (resource === undefined) {
|
|
1298
|
+
if (pageFlag !== undefined) fail(2, "--page applies to help <resource> --json.");
|
|
1299
|
+
out(all ? helpAll() : helpIndex());
|
|
1300
|
+
await flushExit(0);
|
|
1301
|
+
}
|
|
1302
|
+
const ops = helpTarget(resource!, method);
|
|
1303
|
+
const summaries = commandSummaries();
|
|
1304
|
+
if (method !== undefined) {
|
|
1305
|
+
out({ ...helpHeader("command"), resource, ...helpEntry(ops[0]!, summaries[OPS.indexOf(ops[0]!)]!) });
|
|
1306
|
+
await flushExit(0);
|
|
1307
|
+
}
|
|
1308
|
+
if (all) {
|
|
1309
|
+
out({ ...helpHeader("resource"), resource, command_count: ops.length, commands: ops.map((op) => helpEntry(op, summaries[OPS.indexOf(op)]!)) });
|
|
1310
|
+
await flushExit(0);
|
|
1311
|
+
}
|
|
1312
|
+
const pages = Math.max(1, Math.ceil(ops.length / HELP_PAGE_SIZE));
|
|
1313
|
+
if (page > pages) fail(2, "--page " + page + " is past the last page (" + pages + ") of " + resource + ".", undefined, ["Run '" + BIN + " help " + resource + " --json' for the first page."]);
|
|
1314
|
+
const commands = ops.slice((page - 1) * HELP_PAGE_SIZE, page * HELP_PAGE_SIZE).map((op) => {
|
|
1315
|
+
const s = summaries[OPS.indexOf(op)]!;
|
|
1316
|
+
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) };
|
|
1317
|
+
});
|
|
1318
|
+
out({
|
|
1319
|
+
...helpHeader("resource"),
|
|
1320
|
+
resource,
|
|
1321
|
+
command_count: ops.length,
|
|
1322
|
+
page,
|
|
1323
|
+
pages,
|
|
1324
|
+
commands,
|
|
1325
|
+
...(page < pages ? { next_command: BIN + " help " + resource + " --json --page " + (page + 1) } : {}),
|
|
1326
|
+
discovery: { command: BIN + " help " + resource + " <command> --json", operation: BIN + " docs " + resource + " <command> --json", all: BIN + " help " + resource + " --json --all" },
|
|
1327
|
+
});
|
|
1328
|
+
await flushExit(0);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1197
1331
|
async function cmdAgentGuide(parsed: Parsed): Promise<void> {
|
|
1198
1332
|
if (parsed.help) {
|
|
1199
1333
|
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");
|
|
@@ -1543,7 +1677,7 @@ function completionFlagsFor(op: OpSpec): { flags: string[]; values: Record<strin
|
|
|
1543
1677
|
return { flags, values };
|
|
1544
1678
|
}
|
|
1545
1679
|
|
|
1546
|
-
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)];
|
|
1680
|
+
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)];
|
|
1547
1681
|
const BUILTIN_WORDS: Record<string, string[]> = {
|
|
1548
1682
|
config: ["list", "get", "set", "unset", "path"],
|
|
1549
1683
|
completion: ["bash", "zsh", "fish"],
|
|
@@ -1701,7 +1835,9 @@ function referenceFor(op: OpSpec, includeSchemas = false): string {
|
|
|
1701
1835
|
lines.push("", paintOut("bold", label + ":"));
|
|
1702
1836
|
for (const p of params) {
|
|
1703
1837
|
const name = p.kind === "path" ? "<" + p.name + ">" : "--" + p.flag;
|
|
1704
|
-
|
|
1838
|
+
// An object flag reads as its named type, which docs read explains.
|
|
1839
|
+
const named = objectTypeName(op, p);
|
|
1840
|
+
lines.push(" " + padPaint("cyan", name, 30) + (named ? INPUT_TYPES.args[op.tool]![p.name] : typeLabel(p)) + (p.required ? " " + paintOut("yellow", "(required)") : ""));
|
|
1705
1841
|
const values = p.type === "array" ? p.items?.enum : p.enum;
|
|
1706
1842
|
if (values && values.join("|").length > 24) lines.push(" one of: " + values.join(", "));
|
|
1707
1843
|
if (p.description) {
|
|
@@ -1720,11 +1856,34 @@ function referenceFor(op: OpSpec, includeSchemas = false): string {
|
|
|
1720
1856
|
lines.push("", paintOut("bold", "Wire arguments:"), JSON.stringify(op.exampleArguments, null, 2));
|
|
1721
1857
|
if (op.outputSchema) lines.push("", paintOut("bold", "Output schema:"), JSON.stringify(op.outputSchema, null, 2));
|
|
1722
1858
|
} else {
|
|
1859
|
+
const nested = op.params.find((p) => objectTypeName(op, p) || p.type === "object" || (p.type === "array" && p.items?.type === "object"));
|
|
1860
|
+
if (nested) {
|
|
1861
|
+
const named = objectTypeName(op, nested);
|
|
1862
|
+
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."
|
|
1863
|
+
+ (named ? " Named types: " + BIN + " docs read " + named + "." : ""));
|
|
1864
|
+
}
|
|
1723
1865
|
lines.push("", "Add --schema for the complete input/output schemas, or --json for the machine contract.");
|
|
1724
1866
|
}
|
|
1725
1867
|
return lines.join("\n");
|
|
1726
1868
|
}
|
|
1727
1869
|
|
|
1870
|
+
/** The named input object a flag takes (IssueFilter), when it has one. */
|
|
1871
|
+
function objectTypeName(op: OpSpec, p: ParamSpec): string | undefined {
|
|
1872
|
+
return namedTypesIn(INPUT_TYPES, INPUT_TYPES.args[op.tool]?.[p.name]).find((name) => INPUT_TYPES.types[name]!.fields);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
function docsTypeHint(name: string): string {
|
|
1876
|
+
return BIN + " docs read " + name;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/** docs --path: one nested argument, or a usage error naming the fields. */
|
|
1880
|
+
async function docsPathExit(root: { tool: string; inputSchema: Record<string, unknown>; label: string } | { type: string }, path: string): Promise<never> {
|
|
1881
|
+
const found = argumentPathText(INPUT_TYPES, root, path, docsTypeHint);
|
|
1882
|
+
if (!found.ok) fail(2, found.message + (found.available.length > 0 ? " Fields: " + found.available.join(", ") + "." : ""));
|
|
1883
|
+
process.stdout.write((found as { text: string }).text + "\n");
|
|
1884
|
+
return flushExit(0);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1728
1887
|
/** Opens a browser for a person; under an agent it prints the URL instead of opening anything. */
|
|
1729
1888
|
function openInBrowser(url: string, parsed?: Parsed): { opened: boolean; url: string } {
|
|
1730
1889
|
if (parsed && nonInteractive(parsed)) {
|
|
@@ -1744,11 +1903,12 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1744
1903
|
"",
|
|
1745
1904
|
" " + BIN + " docs overview",
|
|
1746
1905
|
" " + BIN + " docs <resource> <command> operation contract and example",
|
|
1906
|
+
" --path <a.b> one nested argument's type and fields",
|
|
1747
1907
|
" --schema include input/output JSON Schema",
|
|
1748
1908
|
" --json print the machine contract as JSON",
|
|
1749
1909
|
" " + BIN + " docs search <term> search reference and guides",
|
|
1750
1910
|
" --json / --format json print structured matches and availability",
|
|
1751
|
-
" " + BIN + " docs read <page> print a docs-site page
|
|
1911
|
+
" " + BIN + " docs read <page> print a named input type or a docs-site page",
|
|
1752
1912
|
" " + BIN + " docs --web open the docs site in a browser",
|
|
1753
1913
|
"",
|
|
1754
1914
|
"Guides come from the docs site's llms.txt (set with '" + BIN + " config set docs-url <url>').",
|
|
@@ -1815,6 +1975,13 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1815
1975
|
if (sub === "read") {
|
|
1816
1976
|
const page = parsed.positionals[2];
|
|
1817
1977
|
if (page === undefined) fail(2, "docs read expects a page path or URL");
|
|
1978
|
+
const typeName = findInputType(INPUT_TYPES, page!);
|
|
1979
|
+
if (typeName) {
|
|
1980
|
+
const path = parsed.flags.get("path");
|
|
1981
|
+
if (typeof path === "string" && path.trim()) await docsPathExit({ type: typeName }, path);
|
|
1982
|
+
process.stdout.write(inputTypeText(INPUT_TYPES, typeName, docsTypeHint) + "\n");
|
|
1983
|
+
await flushExit(0);
|
|
1984
|
+
}
|
|
1818
1985
|
let target = page!;
|
|
1819
1986
|
if (!/^https?:\/\//.test(target)) {
|
|
1820
1987
|
const index = await fetchDocs("llms.txt");
|
|
@@ -1853,6 +2020,8 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1853
2020
|
}, null, 2) + "\n");
|
|
1854
2021
|
await flushExit(0);
|
|
1855
2022
|
}
|
|
2023
|
+
const path = parsed.flags.get("path");
|
|
2024
|
+
if (typeof path === "string" && path.trim()) await docsPathExit({ tool: op!.tool, inputSchema: op!.inputSchema, label: op!.command.join(" ") }, path);
|
|
1856
2025
|
process.stdout.write(referenceFor(op!, parsed.flags.get("schema") === true) + "\n");
|
|
1857
2026
|
await flushExit(0);
|
|
1858
2027
|
}
|
|
@@ -2037,7 +2206,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
|
|
|
2037
2206
|
}
|
|
2038
2207
|
const width = termWidth();
|
|
2039
2208
|
const lines: string[] = [];
|
|
2040
|
-
lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.
|
|
2209
|
+
lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.24.0");
|
|
2041
2210
|
lines.push("");
|
|
2042
2211
|
lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
|
|
2043
2212
|
lines.push("");
|
|
@@ -2059,7 +2228,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
|
|
|
2059
2228
|
lines.push(...labeled(paintOut("yellow", "Coverage:") + " ", "this build includes " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations; api.json lists the rest", width, 14));
|
|
2060
2229
|
}
|
|
2061
2230
|
lines.push("");
|
|
2062
|
-
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)" +
|
|
2231
|
+
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)" +
|
|
2063
2232
|
(AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
|
|
2064
2233
|
lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
|
|
2065
2234
|
lines.push(...labeled("Credential env vars: ", [
|
|
@@ -2117,6 +2286,8 @@ function commandExtras(op: OpSpec): [string, string][] {
|
|
|
2117
2286
|
const collectionField = collectionProperty(op.outputSchema);
|
|
2118
2287
|
extras.push(["--fields <a,b.c>", "keep only these fields of the result" + (op.paginated ? " (per item)" : collectionField ? " (per item in " + collectionField + ")" : "")]);
|
|
2119
2288
|
if ((op.fileBundleProperty ?? bundleProperty(op.outputSchema)) !== null) extras.push(["--out <dir>", "write the response's files ({path, content}) into a directory"]);
|
|
2289
|
+
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" : "")]);
|
|
2290
|
+
if (!op.rawResponse && !op.sse) extras.push(["--format table", "print the result as a table for reading instead of JSON"]);
|
|
2120
2291
|
if (op.safety === "destructive") extras.push(["--force, -y", "destructive: required without a terminal, skips the prompt with one"]);
|
|
2121
2292
|
return extras;
|
|
2122
2293
|
}
|
|
@@ -2341,6 +2512,65 @@ let LAST_CLIENT_HAD_CREDENTIAL = false;
|
|
|
2341
2512
|
/** The last client's Basic credentials, for path arguments that default to the username. */
|
|
2342
2513
|
let LAST_CLIENT_BASIC: { basicAuth?: unknown; credentials?: unknown } = {};
|
|
2343
2514
|
|
|
2515
|
+
/** --dry-run: the client's fetch records the first request and sends
|
|
2516
|
+
* nothing. Set before makeClient; secrets are the credential values it
|
|
2517
|
+
* resolved, redacted wherever they appear in the preview. */
|
|
2518
|
+
let DRY_RUN: { fetch: typeof fetch; request?: { method: string; url: string; headers: Record<string, string>; body?: unknown }; secrets: string[] } | null = null;
|
|
2519
|
+
|
|
2520
|
+
class DryRunStop extends Error {}
|
|
2521
|
+
|
|
2522
|
+
function dryRunCapture(): NonNullable<typeof DRY_RUN> {
|
|
2523
|
+
const capture: NonNullable<typeof DRY_RUN> = {
|
|
2524
|
+
secrets: [],
|
|
2525
|
+
fetch: async (input, init) => {
|
|
2526
|
+
if (!capture.request) {
|
|
2527
|
+
const headers: Record<string, string> = {};
|
|
2528
|
+
new Headers(init?.headers).forEach((value, name) => { headers[name] = value; });
|
|
2529
|
+
capture.request = { method: init?.method ?? "GET", url: typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url, headers, body: init?.body ?? undefined };
|
|
2530
|
+
}
|
|
2531
|
+
throw new DryRunStop("dry run: not sent");
|
|
2532
|
+
},
|
|
2533
|
+
};
|
|
2534
|
+
return capture;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/** Every credential value a client was built with (not a Basic username,
|
|
2538
|
+
* which is an account identifier such as Twilio's AccountSid). */
|
|
2539
|
+
function credentialSecrets(options: Record<string, unknown>): string[] {
|
|
2540
|
+
const found: string[] = [];
|
|
2541
|
+
const walk = (value: unknown, key: string) => {
|
|
2542
|
+
if (typeof value === "string") { if (key !== "username") found.push(value); return; }
|
|
2543
|
+
if (value && typeof value === "object") for (const [k, v] of Object.entries(value as Record<string, unknown>)) walk(v, k);
|
|
2544
|
+
};
|
|
2545
|
+
for (const a of AUTH_SCALARS) walk(options[a.option], a.option);
|
|
2546
|
+
walk(options.basicAuth, "basicAuth");
|
|
2547
|
+
walk(options.credentials, "credentials");
|
|
2548
|
+
walk(options.bearerToken, "bearerToken");
|
|
2549
|
+
return found;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
/** Drive the command's call until its request reaches the dry-run fetch,
|
|
2553
|
+
* then print that request (JSON in agent mode, text for a person) and exit.
|
|
2554
|
+
* A failure before any request (a --validate violation) is reported as usual. */
|
|
2555
|
+
async function printDryRun(parsed: Parsed, callResult: unknown): Promise<never> {
|
|
2556
|
+
let failure: unknown;
|
|
2557
|
+
try {
|
|
2558
|
+
const pending = callResult as { then?: unknown; [Symbol.asyncIterator]?: () => AsyncIterator<unknown> };
|
|
2559
|
+
if (typeof pending.then === "function") await (callResult as Promise<unknown>);
|
|
2560
|
+
else if (typeof pending[Symbol.asyncIterator] === "function") await pending[Symbol.asyncIterator]!().next();
|
|
2561
|
+
} catch (e) {
|
|
2562
|
+
failure = e;
|
|
2563
|
+
}
|
|
2564
|
+
const request = DRY_RUN?.request;
|
|
2565
|
+
if (!request) {
|
|
2566
|
+
failApi(failure ?? new Error("--dry-run: the command made no request"), LAST_CLIENT_HAD_CREDENTIAL);
|
|
2567
|
+
}
|
|
2568
|
+
const preview = await requestPreview(request!, { sensitiveNames: CREDENTIAL_NAMES, secrets: DRY_RUN!.secrets });
|
|
2569
|
+
if (isAgentMode(parsed)) out(preview);
|
|
2570
|
+
else process.stdout.write(formatRequestPreview(preview));
|
|
2571
|
+
return await flushExit(0);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2344
2574
|
/** The configured Basic-auth username: the named scheme's, else basicAuth's. */
|
|
2345
2575
|
function credentialUsername(scheme: string): string | undefined {
|
|
2346
2576
|
const named = (LAST_CLIENT_BASIC.credentials as Record<string, unknown> | undefined)?.[scheme];
|
|
@@ -2539,6 +2769,12 @@ async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, cand
|
|
|
2539
2769
|
if (forIdentity) { options.fetch = identityFetch(baseUrl); options.maxRetries = 0; options.timeoutMs = 10_000; }
|
|
2540
2770
|
const extraHeaders = extraRequestHeaders();
|
|
2541
2771
|
if (Object.keys(extraHeaders).length) options.onRequest = (context) => { applyExtraHeaders(context.headers, extraHeaders); };
|
|
2772
|
+
// --dry-run swaps fetch on this client only: an OAuth refresh (and its
|
|
2773
|
+
// identity check) still uses the real transport, the command does not.
|
|
2774
|
+
if (DRY_RUN && !forIdentity) {
|
|
2775
|
+
DRY_RUN.secrets = credentialSecrets(options);
|
|
2776
|
+
return new TypeshipClient({ ...options, fetch: DRY_RUN.fetch, maxRetries: 0 });
|
|
2777
|
+
}
|
|
2542
2778
|
return new TypeshipClient(options);
|
|
2543
2779
|
}
|
|
2544
2780
|
|
|
@@ -2596,12 +2832,17 @@ async function main(): Promise<void> {
|
|
|
2596
2832
|
PROFILE = resolveProfile(configRoot(), { flag: profileFlag as string | undefined, environment: process.env["TYPESHIP_PROFILE"], allowMissing: ["login", "config", "auth", "init", "help"].includes(parsed.positionals[0] ?? "") || parsed.help });
|
|
2597
2833
|
if (!parsed.help && ["login", "init"].includes(parsed.positionals[0] ?? "")) expectedLoginIdentity(parsed.flags);
|
|
2598
2834
|
if (parsed.positionals[0] === "help") {
|
|
2599
|
-
// help --json:
|
|
2600
|
-
if (parsed.flags.get("json") === true || parsed.flags.get("format") === "json") {
|
|
2835
|
+
// help [<resource> [<command>]] --json: bounded discovery as data; --all is exhaustive.
|
|
2836
|
+
if (parsed.flags.get("json") === true || parsed.flags.get("format") === "json") { await cmdHelpJson(parsed); }
|
|
2601
2837
|
parsed.positionals.shift(); parsed.help = true;
|
|
2602
2838
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2839
|
+
const formatFlag = parsed.flags.get("format");
|
|
2840
|
+
if (formatFlag !== undefined && formatFlag !== "json" && formatFlag !== "table") {
|
|
2841
|
+
fail(2, "--format takes json (the default) or table.");
|
|
2842
|
+
}
|
|
2843
|
+
TABLE = formatFlag === "table";
|
|
2844
|
+
if (TABLE && !parsed.help && (BUILTIN_COMMANDS.includes(parsed.positionals[0] ?? "") || parsed.positionals.length === 0)) {
|
|
2845
|
+
fail(2, "--format table applies to API commands; " + (parsed.positionals[0] ? BIN + " " + parsed.positionals[0] : BIN) + " prints JSON.");
|
|
2605
2846
|
}
|
|
2606
2847
|
if (parsed.flags.has("version") || parsed.positionals[0] === "version") {
|
|
2607
2848
|
// "acme 1.0.0 (acme 1.0.0)" would say the name twice; when the API's
|
|
@@ -2704,7 +2945,7 @@ async function main(): Promise<void> {
|
|
|
2704
2945
|
// Mirrors opReservedFlags() in the generator: API parameters never use these
|
|
2705
2946
|
// names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
|
|
2706
2947
|
// check can be exact.
|
|
2707
|
-
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)]);
|
|
2948
|
+
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)]);
|
|
2708
2949
|
for (const spec of op.params) {
|
|
2709
2950
|
if (spec.kind === "path") continue;
|
|
2710
2951
|
const raw = parsed.flags.get(spec.flag);
|
|
@@ -2798,6 +3039,18 @@ async function main(): Promise<void> {
|
|
|
2798
3039
|
if (!op.paginated && parsed.flags.get("all") === true) {
|
|
2799
3040
|
fail(2, op.command.join(" ") + " does not paginate, so --all has nothing to walk. Run it without --all; it returns the whole response.");
|
|
2800
3041
|
}
|
|
3042
|
+
// --dry-run resolves everything a real call would (arguments, defaults,
|
|
3043
|
+
// credentials, the body encoding) and prints the request instead of
|
|
3044
|
+
// sending it. A destructive command needs no --force: nothing runs.
|
|
3045
|
+
if (parsed.flags.get("dry-run") === true) {
|
|
3046
|
+
if (op.paginated && parsed.flags.get("all") === true) fail(2, "--dry-run previews one request; run it without --all to see the first page's.");
|
|
3047
|
+
DRY_RUN = dryRunCapture();
|
|
3048
|
+
}
|
|
3049
|
+
// A table needs the whole result: streams (--all, events) and raw bodies stay as they are.
|
|
3050
|
+
if (TABLE) {
|
|
3051
|
+
const streams = parsed.flags.get("all") === true || op.sse || (op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value);
|
|
3052
|
+
if (streams || op.rawResponse) 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." : "."));
|
|
3053
|
+
}
|
|
2801
3054
|
const client = await makeClient(parsed.flags, op);
|
|
2802
3055
|
for (const spec of pathSpecs) {
|
|
2803
3056
|
if (!spec.credential || values[spec.name] !== undefined) continue;
|
|
@@ -2812,7 +3065,7 @@ async function main(): Promise<void> {
|
|
|
2812
3065
|
// Destructive commands need --force. A person gets asked; an agent gets
|
|
2813
3066
|
// an action_required envelope with the exact command to run, so nothing
|
|
2814
3067
|
// is deleted on a guess.
|
|
2815
|
-
if (op.safety === "destructive" && !assumeYes(parsed)) {
|
|
3068
|
+
if (op.safety === "destructive" && !assumeYes(parsed) && !DRY_RUN) {
|
|
2816
3069
|
// Credential flags are replaced by placeholders: the rerun is shown to
|
|
2817
3070
|
// agents and logged, so it must never repeat a key.
|
|
2818
3071
|
const secretFlags = new Set([...AUTH_SCALARS.map((a) => "--" + a.flag), "--header"]);
|
|
@@ -2842,9 +3095,10 @@ async function main(): Promise<void> {
|
|
|
2842
3095
|
// as NDJSON as they arrive, instead of failing on the event stream.
|
|
2843
3096
|
const streaming = op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value;
|
|
2844
3097
|
const callResult = target[streaming ? op.streamMethod!.method : op.method]!(...args);
|
|
3098
|
+
if (DRY_RUN) await printDryRun(parsed, callResult);
|
|
2845
3099
|
|
|
2846
3100
|
if (op.paginated && parsed.flags.get("all") === true) {
|
|
2847
|
-
const fieldsCheck = streamFieldsCheck();
|
|
3101
|
+
const fieldsCheck = streamFieldsCheck((listSchemaOf(op.outputSchema, "items") as { items?: unknown } | undefined)?.items);
|
|
2848
3102
|
try {
|
|
2849
3103
|
for await (const item of callResult as AsyncIterable<unknown>) {
|
|
2850
3104
|
process.stdout.write(JSON.stringify(fieldsCheck.item(item)) + "\n");
|
|
@@ -2875,7 +3129,7 @@ async function main(): Promise<void> {
|
|
|
2875
3129
|
if (result.ok) {
|
|
2876
3130
|
if (op.sse || streaming) {
|
|
2877
3131
|
// Server-sent events as NDJSON, one line per event, until the stream ends.
|
|
2878
|
-
const fieldsCheck = streamFieldsCheck();
|
|
3132
|
+
const fieldsCheck = streamFieldsCheck(undefined);
|
|
2879
3133
|
try {
|
|
2880
3134
|
for await (const event of result.data as AsyncIterable<{ event?: string; id?: string; data: string }>) {
|
|
2881
3135
|
process.stdout.write(JSON.stringify(fieldsCheck.item(event)) + "\n");
|
|
@@ -2917,17 +3171,17 @@ async function main(): Promise<void> {
|
|
|
2917
3171
|
// agent never has to reconstruct the cursor flag.
|
|
2918
3172
|
const page = result.data as { items: unknown[]; hasNextPage(): boolean; nextPageParams(): Record<string, unknown> | null; response: { requestId?: string } };
|
|
2919
3173
|
const next = page.nextPageParams();
|
|
2920
|
-
|
|
2921
|
-
items: project(page.items, true),
|
|
3174
|
+
printResult({
|
|
3175
|
+
items: project(page.items, true, listSchemaOf(op.outputSchema, "items")),
|
|
2922
3176
|
hasMore: next !== null,
|
|
2923
3177
|
...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
|
|
2924
3178
|
...(page.response.requestId ? { request_id: page.response.requestId } : {}),
|
|
2925
|
-
});
|
|
3179
|
+
}, op.command[0]);
|
|
2926
3180
|
} else if (FIELDS !== null && collectionField !== null && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) && Array.isArray((result.data as Record<string, unknown>)[collectionField])) {
|
|
2927
3181
|
// Batch-style collection envelopes ({data: [...]}) use item-relative
|
|
2928
3182
|
// fields, matching paginated results, while retaining envelope metadata.
|
|
2929
3183
|
const data = result.data as Record<string, unknown>;
|
|
2930
|
-
|
|
3184
|
+
printResult({ ...data, [collectionField]: project(data[collectionField], true, listSchemaOf(op.outputSchema, collectionField)) }, op.command[0], collectionField);
|
|
2931
3185
|
} else if (outDir !== undefined && bundleField !== null) {
|
|
2932
3186
|
// --out: a file-shaped response (an array of {path, content}) lands
|
|
2933
3187
|
// on disk; stdout gets the rest of the response plus a summary.
|
|
@@ -2935,9 +3189,9 @@ async function main(): Promise<void> {
|
|
|
2935
3189
|
const files = (data[bundleField] as { path: string; content: string }[] | undefined) ?? [];
|
|
2936
3190
|
const written = writeBundle(outDir, files);
|
|
2937
3191
|
const { [bundleField]: _omitted, ...rest } = data;
|
|
2938
|
-
|
|
3192
|
+
printResult({ ...(project(rest, false, op.outputSchema) as Record<string, unknown>), out: written }, op.command[0]);
|
|
2939
3193
|
} else {
|
|
2940
|
-
|
|
3194
|
+
printResult(result.data === undefined || result.data === null ? { ok: true } : project(result.data, Array.isArray(result.data), op.outputSchema), op.command[0], collectionField);
|
|
2941
3195
|
}
|
|
2942
3196
|
await flushExit(0);
|
|
2943
3197
|
}
|