@typeship-ax/cli 0.22.0 → 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 +13 -9
- package/README.md +14 -27
- package/api.json +8898 -9026
- package/api.md +370 -328
- package/dist/arguments.d.ts +54 -0
- package/dist/arguments.d.ts.map +1 -0
- package/dist/arguments.js +265 -0
- package/dist/cli-agent.d.ts +73 -9
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +331 -44
- package/dist/cli.js +802 -291
- package/dist/core/http.d.ts +162 -19
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +381 -48
- package/dist/core/pagination.d.ts +42 -6
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +111 -17
- package/dist/credential-storage.d.ts +10 -3
- package/dist/credential-storage.d.ts.map +1 -1
- package/dist/credential-storage.js +15 -6
- package/dist/dates.d.ts +1 -1
- package/dist/dates.js +1 -1
- package/dist/errors.d.ts +20 -84
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -108
- package/dist/fields.d.ts +36 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +187 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -25
- package/dist/named-credentials.d.ts +19 -0
- package/dist/named-credentials.d.ts.map +1 -1
- package/dist/named-credentials.js +81 -1
- package/dist/oauth-login.d.ts +8 -2
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +31 -19
- package/dist/oauth-request.d.ts +7 -1
- package/dist/oauth-request.d.ts.map +1 -1
- package/dist/oauth-request.js +26 -4
- package/dist/oauth-session.d.ts +13 -1
- package/dist/oauth-session.d.ts.map +1 -1
- package/dist/oauth-session.js +34 -18
- package/dist/ops.d.ts +58 -5
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +110 -41
- package/dist/polling-login.d.ts +8 -2
- package/dist/polling-login.d.ts.map +1 -1
- package/dist/polling-login.js +25 -11
- package/dist/resources/api-keys.d.ts +10 -7
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +10 -31
- package/dist/resources/deliveries.d.ts +89 -5
- package/dist/resources/deliveries.d.ts.map +1 -1
- package/dist/resources/deliveries.js +96 -19
- package/dist/resources/drafts.d.ts +16 -16
- package/dist/resources/drafts.d.ts.map +1 -1
- package/dist/resources/drafts.js +12 -65
- package/dist/resources/files.d.ts +4 -4
- package/dist/resources/files.d.ts.map +1 -1
- package/dist/resources/files.js +3 -12
- package/dist/resources/generations.d.ts +16 -16
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +23 -47
- package/dist/resources/organization.d.ts +4 -4
- package/dist/resources/organization.d.ts.map +1 -1
- package/dist/resources/organization.js +3 -10
- package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
- package/dist/resources/packages.d.ts.map +1 -0
- package/dist/resources/{generate.js → packages.js} +13 -29
- package/dist/resources/projects.d.ts +50 -50
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +60 -116
- package/dist/resources/releases.d.ts +22 -17
- package/dist/resources/releases.d.ts.map +1 -1
- package/dist/resources/releases.js +19 -40
- package/dist/resources/spec-revisions.d.ts +16 -7
- package/dist/resources/spec-revisions.d.ts.map +1 -1
- package/dist/resources/spec-revisions.js +7 -29
- package/dist/resources/specs.d.ts +7 -7
- package/dist/resources/specs.d.ts.map +1 -1
- package/dist/resources/specs.js +6 -34
- package/dist/resources/targets.d.ts +49 -49
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +59 -115
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +83 -81
- package/dist/search.d.ts +54 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +421 -0
- 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/dist/types.d.ts +499 -339
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +18 -18
- package/package.json +5 -2
- package/src/arguments.ts +254 -0
- package/src/cli-agent.ts +351 -46
- package/src/cli.ts +753 -266
- package/src/core/http.ts +457 -58
- package/src/core/pagination.ts +129 -18
- package/src/credential-storage.ts +16 -6
- package/src/dates.ts +1 -1
- package/src/errors.ts +46 -115
- package/src/fields.ts +167 -0
- package/src/index.ts +45 -28
- package/src/named-credentials.ts +66 -1
- package/src/oauth-login.ts +36 -21
- package/src/oauth-request.ts +32 -6
- package/src/oauth-session.ts +37 -19
- package/src/ops.ts +146 -45
- package/src/polling-login.ts +24 -11
- package/src/resources/api-keys.ts +34 -48
- package/src/resources/deliveries.ts +213 -32
- package/src/resources/drafts.ts +62 -109
- package/src/resources/files.ts +19 -20
- package/src/resources/generations.ts +61 -79
- package/src/resources/organization.ts +11 -16
- package/src/resources/{generate.ts → packages.ts} +43 -51
- package/src/resources/projects.ts +145 -200
- package/src/resources/releases.ts +50 -67
- package/src/resources/spec-revisions.ts +40 -49
- package/src/resources/specs.ts +39 -59
- package/src/resources/targets.ts +144 -194
- package/src/schemas.ts +83 -81
- package/src/search.ts +434 -0
- package/src/table.ts +167 -0
- package/src/type-docs.ts +205 -0
- package/src/types.ts +538 -357
- package/dist/console-login-check.d.ts +0 -21
- package/dist/console-login-check.d.ts.map +0 -1
- package/dist/console-login-check.js +0 -107
- package/dist/console-login-contract.d.ts +0 -45
- package/dist/console-login-contract.d.ts.map +0 -1
- package/dist/console-login-contract.js +0 -40
- package/dist/resources/generate.d.ts.map +0 -1
- package/dist/resources/publications.d.ts +0 -47
- package/dist/resources/publications.d.ts.map +0 -1
- package/dist/resources/publications.js +0 -70
- package/src/console-login-check.ts +0 -88
- package/src/console-login-contract.ts +0 -65
- package/src/resources/publications.ts +0 -140
package/dist/cli-agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The agent contract of a generated CLI. Generated by
|
|
2
|
+
* The agent contract of a generated CLI. Generated by Typeship — https://typeship.dev
|
|
3
3
|
*
|
|
4
4
|
* Everything a coding agent needs from a command-line tool and a human does
|
|
5
5
|
* not: a JSON error envelope with stable codes and next steps, agent-mode
|
|
@@ -27,6 +27,7 @@ export function exitCodeFor(code) {
|
|
|
27
27
|
case "UNKNOWN_COMMAND":
|
|
28
28
|
case "UNKNOWN_FLAG":
|
|
29
29
|
case "MISSING_ARGUMENT":
|
|
30
|
+
case "FIELDS_UNMATCHED":
|
|
30
31
|
case "TTY_REQUIRED":
|
|
31
32
|
case "CONFIRMATION_REQUIRED":
|
|
32
33
|
return 2;
|
|
@@ -38,9 +39,9 @@ export function exitCodeFor(code) {
|
|
|
38
39
|
export function classifyApiError(error, context) {
|
|
39
40
|
const e = (error ?? {});
|
|
40
41
|
// The message is the API's own words when it sent any, else the SDK's
|
|
41
|
-
// (
|
|
42
|
-
//
|
|
43
|
-
//
|
|
42
|
+
// ("HTTP 404"). The error class name rides in detail, not in front of the
|
|
43
|
+
// message: "NotFoundError: No such account (no such account)" said one
|
|
44
|
+
// thing three times.
|
|
44
45
|
const base = e.message ? e.message : String(error);
|
|
45
46
|
if (e.violations !== undefined) {
|
|
46
47
|
const nextStep = e.direction === "response"
|
|
@@ -50,12 +51,12 @@ export function classifyApiError(error, context) {
|
|
|
50
51
|
: "Correct the body fields named in detail.violations to match their constraints, then run the command again.";
|
|
51
52
|
return { code: "VALIDATION_FAILED", message: base, detail: { ...(e.name ? { error: e.name } : {}), violations: e.violations }, nextSteps: [nextStep] };
|
|
52
53
|
}
|
|
53
|
-
if (e.name === "TransportError" || (typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(base))) {
|
|
54
|
+
if (e.name === "TransportError" || (e.name !== "PaginationError" && typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(base))) {
|
|
54
55
|
return {
|
|
55
56
|
code: "NETWORK_ERROR",
|
|
56
57
|
message: base,
|
|
57
58
|
nextSteps: [
|
|
58
|
-
"Check the base URL (--base-url, the " + context.
|
|
59
|
+
"Check the base URL (--base-url, the " + context.envPrefix + "_BASE_URL variable, or '" + context.bin + " config base-url') and the network.",
|
|
59
60
|
"Retry once with backoff; do not loop.",
|
|
60
61
|
],
|
|
61
62
|
};
|
|
@@ -68,27 +69,57 @@ export function classifyApiError(error, context) {
|
|
|
68
69
|
? e.body.request_id ?? e.body.requestId
|
|
69
70
|
: undefined;
|
|
70
71
|
const requestId = e.response?.requestId ?? (typeof bodyRequestId === "string" ? bodyRequestId : undefined);
|
|
71
|
-
|
|
72
|
+
// An in-band GraphQL error keeps the vendor's code next to the normalized one.
|
|
73
|
+
const vendorCode = e.name === "GraphQLRequestError" ? graphqlVendorCode(error) : undefined;
|
|
74
|
+
const detail = { status, ...(e.name ? { error: e.name } : {}), ...(vendorCode ? { vendor_code: vendorCode } : {}), ...(requestId ? { request_id: requestId } : {}), ...(e.body !== undefined ? { body: e.body } : {}) };
|
|
72
75
|
const same = apiMessage !== undefined && (apiMessage.toLowerCase() === base.toLowerCase() || base.toLowerCase().includes(apiMessage.toLowerCase()) || apiMessage.toLowerCase().includes(base.toLowerCase()));
|
|
73
76
|
const message = apiMessage === undefined ? base : same ? apiMessage : apiMessage + " (" + base + ")";
|
|
74
77
|
const upgradeUrl = extractUrl(e.body, ["upgrade_url", "upgradeUrl", "signup_url", "claim_url"]);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
: { status: "action_required", code: "NO_AUTH", message, detail, nextSteps: ["No credential was sent. Set the auth env var, pass --token, or run '" + context.bin + " login'.", "'" + context.bin + " auth check' shows what the CLI would send."] };
|
|
78
|
+
// A rate limit can arrive as a 403 (GitHub); the SDK marks it either way.
|
|
79
|
+
if (status === 429 || e.rateLimit !== undefined) {
|
|
80
|
+
return { status: "action_required", code: "RATE_LIMITED", message, detail: { ...detail, ...(e.rateLimit?.retryAt ? { retry_at: isoSeconds(e.rateLimit.retryAt) } : {}) }, nextSteps: [rateLimitNextStep(e.rateLimit?.retryAt, "run the same command again")] };
|
|
79
81
|
}
|
|
82
|
+
// A failure the API reported inside a 2xx body: classify by its code.
|
|
83
|
+
const scopes = context.requiredScopes ?? [];
|
|
84
|
+
const scopeFailure = () => ({ code: "INSUFFICIENT_SCOPE", message: message + " This operation requires the OAuth scopes: " + scopes.join(", ") + ".", detail: { ...detail, required_scopes: scopes }, nextSteps: [
|
|
85
|
+
context.canLogin ? "Sign in again with those scopes: '" + context.bin + " login --scopes " + scopes.join(",") + "'." : "Use a token granted those scopes.",
|
|
86
|
+
"If the token already has them, the account may lack access to this resource.",
|
|
87
|
+
] });
|
|
88
|
+
if (e.name === "PayloadError") {
|
|
89
|
+
if (scopes.length && /^missing_scope$/i.test(String(e.code)))
|
|
90
|
+
return scopeFailure();
|
|
91
|
+
const reported = payloadFailureCode(e.code);
|
|
92
|
+
if (reported === "AUTH_INVALID")
|
|
93
|
+
return { code: "AUTH_INVALID", message, detail, nextSteps: ["The API rejected the credential (" + String(e.code) + "). Check it is current: '" + context.bin + " auth check'."] };
|
|
94
|
+
if (reported === "RATE_LIMITED")
|
|
95
|
+
return { status: "action_required", code: "RATE_LIMITED", message, detail, nextSteps: ["Wait, then run the same command again."] };
|
|
96
|
+
return { code: "CALL_FAILED", message, detail, nextSteps: ["The API reported a failure in a successful response; detail.body says why."] };
|
|
97
|
+
}
|
|
98
|
+
const unauthenticated = () => context.hadCredential
|
|
99
|
+
? { code: "AUTH_INVALID", message, detail, nextSteps: ["The credential was rejected. Check it is current: '" + context.bin + " auth check', then '" + context.bin + " login --help' to store a new one."] }
|
|
100
|
+
: { status: "action_required", code: "NO_AUTH", message, detail, nextSteps: ["No credential was sent. Set the auth env var, pass --token, or run '" + context.bin + " login'.", "'" + context.bin + " auth check' shows what the CLI would send."] };
|
|
101
|
+
const forbidden = () => scopes.length ? scopeFailure() : { code: "AUTH_INVALID", message, detail, nextSteps: ["The credential lacks access to this operation."] };
|
|
102
|
+
// An in-band GraphQL error (HTTP 200): classified by the vendor's code.
|
|
103
|
+
if (e.name === "GraphQLRequestError") {
|
|
104
|
+
switch (vendorCode === undefined ? undefined : graphqlErrorClass(vendorCode)) {
|
|
105
|
+
case "not_found": return { code: "NOT_FOUND", message, detail, nextSteps: ["Check the id in the arguments; list the resource first to find the right one."] };
|
|
106
|
+
case "unauthenticated": return unauthenticated();
|
|
107
|
+
case "forbidden": return forbidden();
|
|
108
|
+
case "bad_input": return { code: "INVALID_REQUEST", message, detail, nextSteps: ["The API rejected an argument or the selection; the errors in detail.body name it (message, path). Fix that and run the command again."] };
|
|
109
|
+
case "rate_limited": return { status: "action_required", code: "RATE_LIMITED", message, detail, nextSteps: [rateLimitNextStep(undefined, "run the same command again")] };
|
|
110
|
+
default: return { code: "CALL_FAILED", message, detail };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (status === 401)
|
|
114
|
+
return unauthenticated();
|
|
80
115
|
if (status === 403)
|
|
81
|
-
return
|
|
116
|
+
return forbidden();
|
|
82
117
|
if (status === 402) {
|
|
83
118
|
return { status: "action_required", code: "PLAN_LIMIT", message, detail, nextSteps: [upgradeUrl ? "Lift the limit at " + upgradeUrl + ", then run the same command again." : "The account's plan stops here; upgrade it, then run the same command again.", "Do not retry the same call as is."] };
|
|
84
119
|
}
|
|
85
120
|
if (status === 404)
|
|
86
121
|
return { code: "NOT_FOUND", message, detail, nextSteps: notFoundNextSteps(message) };
|
|
87
|
-
if (status ===
|
|
88
|
-
const retryAfter = extractRetryAfter(e);
|
|
89
|
-
return { status: "action_required", code: "RATE_LIMITED", message, detail, nextSteps: [retryAfter ? "Wait " + retryAfter + " seconds, then run the same command again." : "Back off and retry once; the SDK already retried with the server's Retry-After."] };
|
|
90
|
-
}
|
|
91
|
-
if (status === 422 && apiCode === "spec_error")
|
|
122
|
+
if (status === 422 && (apiCode === "spec_invalid" || apiCode === "spec_error"))
|
|
92
123
|
return { code: "SPEC_INVALID", message, detail, nextSteps: ["The API rejected the spec it was given; the message says why.", context.docsUrl ? "Look the message up: '" + context.bin + " docs search \"" + (apiMessage ?? "").slice(0, 60).replace(/"/g, "'") + "\"'." : "Fix the spec and run again."] };
|
|
93
124
|
if (status === 400 || status === 422 || status === 409 || status === 413)
|
|
94
125
|
return { code: "INVALID_REQUEST", message, detail, nextSteps: ["Read detail.body for the field the API named; run the command with --help for its flags."] };
|
|
@@ -96,6 +127,43 @@ export function classifyApiError(error, context) {
|
|
|
96
127
|
return { code: "SERVER_ERROR", message, detail, nextSteps: ["Retry once with backoff. If it persists, report detail.request_id."] };
|
|
97
128
|
return { code: "CALL_FAILED", message, detail };
|
|
98
129
|
}
|
|
130
|
+
/** Login, saved-session and credential-store failures, recognised by error
|
|
131
|
+
* name (so this module needs no login runtime) on the error or its cause.
|
|
132
|
+
* Undefined when the error is none of them. */
|
|
133
|
+
export function classifyAuthFailure(error, context) {
|
|
134
|
+
const login = "'" + context.bin + " login'";
|
|
135
|
+
const environment = context.envVars.length ? ["Or supply credentials through the environment: " + context.envVars.join(", ") + "."] : [];
|
|
136
|
+
for (let node = error, depth = 0; node && typeof node === "object" && depth < 4; node = node.cause, depth++) {
|
|
137
|
+
const message = typeof node.message === "string" && node.message ? node.message : "Login failed.";
|
|
138
|
+
switch (node.name) {
|
|
139
|
+
case "DeviceFlowUnavailableError":
|
|
140
|
+
return { status: "action_required", code: "LOGIN_FAILED", message, nextSteps: ["Run " + login + " without --device to sign in through the browser.", ...environment] };
|
|
141
|
+
case "LoginPollingError":
|
|
142
|
+
if (node.code === "request_failed" && typeof node.providerError === "string")
|
|
143
|
+
return { code: "LOGIN_FAILED", message, nextSteps: ["The provider rejected the login request. Check the client ID and that the application is registered for this sign-in flow, then run " + login + " again.", ...environment] };
|
|
144
|
+
if (node.code === "request_failed")
|
|
145
|
+
return { code: "LOGIN_FAILED", message, nextSteps: ["Check the network and the login service, then run " + login + " again.", ...environment] };
|
|
146
|
+
if (node.code === "invalid_response")
|
|
147
|
+
return { code: "LOGIN_FAILED", message, nextSteps: ["The login service's response did not match its contract. Retry " + login + " once; if it persists, report it to the API owner.", ...environment] };
|
|
148
|
+
return { code: "LOGIN_FAILED", message, nextSteps: ["Run " + login + " again and approve the new request.", ...environment] };
|
|
149
|
+
case "OAuthResponseError":
|
|
150
|
+
if (node.code === "request_failed" || node.code === "timed_out")
|
|
151
|
+
return { code: "NETWORK_ERROR", message: "The OAuth provider could not be reached (" + String(node.code).replace("_", " ") + ").", nextSteps: ["Check the network and the provider, then run " + login + " again."] };
|
|
152
|
+
return { code: "LOGIN_FAILED", message: "The OAuth provider returned an unusable response (" + String(node.code).replace(/_/g, " ") + ").", nextSteps: ["Run " + login + " again. If it keeps failing, report it to the API owner.", ...environment] };
|
|
153
|
+
case "OAuthLoginError":
|
|
154
|
+
return { code: "LOGIN_FAILED", message, nextSteps: ["Run " + login + " again. If it keeps failing, the OAuth application settings may need attention from the API owner.", ...environment] };
|
|
155
|
+
case "OAuthSessionError":
|
|
156
|
+
return { status: "action_required", code: "LOGIN_REQUIRED", message, nextSteps: ["Run " + login + " to sign in again.", ...environment] };
|
|
157
|
+
case "CredentialStorageError":
|
|
158
|
+
return { status: "action_required", code: "CREDENTIAL_STORE_UNAVAILABLE", message, nextSteps: [
|
|
159
|
+
"Unlock or enable the OS credential store (macOS Keychain, Linux Secret Service or Windows DPAPI) and retry.",
|
|
160
|
+
...environment,
|
|
161
|
+
"To use a plaintext file store instead, set " + context.storeVariable + "=file and run " + login + ".",
|
|
162
|
+
] };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
99
167
|
/** A file lookup needs path guidance, not the generic advice for a missing
|
|
100
168
|
* resource id. Prefer the API's own index when its message names one. */
|
|
101
169
|
function notFoundNextSteps(message) {
|
|
@@ -122,13 +190,50 @@ function extractUrl(body, keys) {
|
|
|
122
190
|
}
|
|
123
191
|
return undefined;
|
|
124
192
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
193
|
+
function isoSeconds(at) {
|
|
194
|
+
return new Date(Math.ceil(at.getTime() / 1000) * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
195
|
+
}
|
|
196
|
+
/** "Wait until <time>" when the API said when the limit resets. */
|
|
197
|
+
export function rateLimitNextStep(retryAt, then) {
|
|
198
|
+
return retryAt instanceof Date && !Number.isNaN(retryAt.getTime())
|
|
199
|
+
? "Rate limited: wait until " + isoSeconds(retryAt) + ", then " + then + "."
|
|
200
|
+
: "Rate limited: back off, then " + then + "; the SDK already honored any Retry-After within its ceiling.";
|
|
201
|
+
}
|
|
202
|
+
/** The vendor's own code on an in-band GraphQL error: the first error's
|
|
203
|
+
* extensions.code, or its type (GitHub). */
|
|
204
|
+
function graphqlVendorCode(error) {
|
|
205
|
+
const first = error?.errors?.[0];
|
|
206
|
+
const code = first?.extensions?.code ?? first?.type;
|
|
207
|
+
return typeof code === "string" && code !== "" ? code : undefined;
|
|
208
|
+
}
|
|
209
|
+
/** GraphQL error codes whose meaning is settled (Apollo's standard codes,
|
|
210
|
+
* GitHub's types, Linear's codes), by recovery class. Any other code is
|
|
211
|
+
* passed through as is, with no guessed next step. The MCP server's
|
|
212
|
+
* classification uses the same table. */
|
|
213
|
+
function graphqlErrorClass(code) {
|
|
214
|
+
const c = code.toUpperCase();
|
|
215
|
+
if (c === "NOT_FOUND")
|
|
216
|
+
return "not_found";
|
|
217
|
+
if (c === "UNAUTHENTICATED" || c === "AUTHENTICATION_ERROR")
|
|
218
|
+
return "unauthenticated";
|
|
219
|
+
if (c === "FORBIDDEN")
|
|
220
|
+
return "forbidden";
|
|
221
|
+
if (c === "BAD_USER_INPUT" || c === "GRAPHQL_VALIDATION_FAILED" || c === "GRAPHQL_PARSE_FAILED" || c === "INPUT_ERROR")
|
|
222
|
+
return "bad_input";
|
|
223
|
+
if (c === "RATE_LIMITED" || c === "RATELIMITED")
|
|
224
|
+
return "rate_limited";
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
/** Error codes APIs report inside a 2xx body (Slack's `error`), mapped to
|
|
228
|
+
* the stable codes when their meaning is unambiguous. */
|
|
229
|
+
export function payloadFailureCode(code) {
|
|
230
|
+
if (typeof code !== "string")
|
|
231
|
+
return undefined;
|
|
232
|
+
if (/^(not_authed|invalid_auth|token_revoked|token_expired|account_inactive|unauthorized|unauthenticated|forbidden|access_denied|missing_scope)$/i.test(code))
|
|
233
|
+
return "AUTH_INVALID";
|
|
234
|
+
if (/^(ratelimited|rate_limited|rate_limit_exceeded|too_many_requests)$/i.test(code))
|
|
235
|
+
return "RATE_LIMITED";
|
|
236
|
+
return undefined;
|
|
132
237
|
}
|
|
133
238
|
/**
|
|
134
239
|
* --mode agent beats <PREFIX>_MODE=agent beats "no terminal on either end".
|
|
@@ -166,6 +271,28 @@ export function detectHarness(env = process.env) {
|
|
|
166
271
|
return "devin";
|
|
167
272
|
return null;
|
|
168
273
|
}
|
|
274
|
+
const ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
275
|
+
/** Rewrite `${VAR}` header references into the client's syntax, or drop the headers that hold one. */
|
|
276
|
+
function entryForClient(client, entry) {
|
|
277
|
+
const style = client.headerEnv ?? "dollar";
|
|
278
|
+
if (!entry.headers || style === "dollar")
|
|
279
|
+
return { entry };
|
|
280
|
+
const headers = {};
|
|
281
|
+
const dropped = [];
|
|
282
|
+
for (const [name, value] of Object.entries(entry.headers)) {
|
|
283
|
+
if (style === "none" && value.search(ENV_REF) !== -1)
|
|
284
|
+
dropped.push(name);
|
|
285
|
+
else
|
|
286
|
+
headers[name] = style === "env-colon" ? value.replace(ENV_REF, "${env:$1}") : style === "brace-env" ? value.replace(ENV_REF, "{env:$1}") : value;
|
|
287
|
+
}
|
|
288
|
+
const next = { ...entry };
|
|
289
|
+
delete next.headers;
|
|
290
|
+
if (Object.keys(headers).length)
|
|
291
|
+
next.headers = headers;
|
|
292
|
+
return dropped.length
|
|
293
|
+
? { entry: next, note: client.label + " does not expand environment variables in headers, so the entry omits " + dropped.join(", ") + "; the client signs in with MCP authorization when the server supports it." }
|
|
294
|
+
: { entry: next };
|
|
295
|
+
}
|
|
169
296
|
function readOr(file, fallback) {
|
|
170
297
|
try {
|
|
171
298
|
return readFileSync(file, "utf8");
|
|
@@ -201,10 +328,18 @@ function tomlMerge(existing, name, entry) {
|
|
|
201
328
|
const lines = [header];
|
|
202
329
|
if (entry.url) {
|
|
203
330
|
lines.push("url = " + JSON.stringify(entry.url));
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
331
|
+
const envHeaders = [];
|
|
332
|
+
for (const [header, value] of Object.entries(entry.headers ?? {})) {
|
|
333
|
+
const envRef = /\$\{([A-Z0-9_]+)\}/.exec(value)?.[1];
|
|
334
|
+
if (!envRef)
|
|
335
|
+
continue;
|
|
336
|
+
if (header.toLowerCase() === "authorization" && /^Bearer /.test(value))
|
|
337
|
+
lines.push("bearer_token_env_var = " + JSON.stringify(envRef));
|
|
338
|
+
else
|
|
339
|
+
envHeaders.push(JSON.stringify(header) + " = " + JSON.stringify(envRef));
|
|
340
|
+
}
|
|
341
|
+
if (envHeaders.length)
|
|
342
|
+
lines.push("env_http_headers = { " + envHeaders.join(", ") + " }");
|
|
208
343
|
}
|
|
209
344
|
else {
|
|
210
345
|
lines.push("command = " + JSON.stringify(entry.command ?? "node"));
|
|
@@ -238,6 +373,7 @@ export const MCP_CLIENTS = [
|
|
|
238
373
|
label: "VS Code",
|
|
239
374
|
file: (cwd) => join(cwd, ".vscode", "mcp.json"),
|
|
240
375
|
detect: (cwd) => existsSync(join(cwd, ".vscode")) || existsSync(join(home(), ".vscode")),
|
|
376
|
+
headerEnv: "env-colon",
|
|
241
377
|
write: (existing, name, entry) => jsonMerge(existing, ["servers"], name, standardEntry(entry)),
|
|
242
378
|
},
|
|
243
379
|
{
|
|
@@ -245,6 +381,7 @@ export const MCP_CLIENTS = [
|
|
|
245
381
|
label: "Windsurf",
|
|
246
382
|
file: () => join(home(), ".codeium", "windsurf", "mcp_config.json"),
|
|
247
383
|
detect: () => existsSync(join(home(), ".codeium", "windsurf")),
|
|
384
|
+
headerEnv: "env-colon",
|
|
248
385
|
write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, entry.url ? { serverUrl: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : stdioEntry(entry)),
|
|
249
386
|
},
|
|
250
387
|
{
|
|
@@ -252,6 +389,7 @@ export const MCP_CLIENTS = [
|
|
|
252
389
|
label: "Gemini CLI",
|
|
253
390
|
file: () => join(home(), ".gemini", "settings.json"),
|
|
254
391
|
detect: () => existsSync(join(home(), ".gemini")),
|
|
392
|
+
headerEnv: "none",
|
|
255
393
|
write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, entry.url ? { httpUrl: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : stdioEntry(entry)),
|
|
256
394
|
},
|
|
257
395
|
{
|
|
@@ -259,6 +397,7 @@ export const MCP_CLIENTS = [
|
|
|
259
397
|
label: "OpenCode",
|
|
260
398
|
file: () => join(xdg(), "opencode", "opencode.json"),
|
|
261
399
|
detect: () => existsSync(join(xdg(), "opencode")),
|
|
400
|
+
headerEnv: "brace-env",
|
|
262
401
|
write: (existing, name, entry) => jsonMerge(existing, ["mcp"], name, entry.url ? { type: "remote", url: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : { type: "local", command: [entry.command ?? "node", ...(entry.args ?? [])] }),
|
|
263
402
|
},
|
|
264
403
|
{
|
|
@@ -266,6 +405,7 @@ export const MCP_CLIENTS = [
|
|
|
266
405
|
label: "Zed",
|
|
267
406
|
file: () => join(xdg(), "zed", "settings.json"),
|
|
268
407
|
detect: () => existsSync(join(xdg(), "zed")),
|
|
408
|
+
headerEnv: "none",
|
|
269
409
|
write: (existing, name, entry) => jsonMerge(existing, ["context_servers"], name, entry.url ? { source: "custom", url: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : { source: "custom", command: entry.command ?? "node", args: entry.args ?? [] }),
|
|
270
410
|
},
|
|
271
411
|
{
|
|
@@ -284,23 +424,32 @@ export const MCP_CLIENTS = [
|
|
|
284
424
|
label: "Cursor",
|
|
285
425
|
file: (cwd) => join(cwd, ".cursor", "mcp.json"),
|
|
286
426
|
detect: () => existsSync(join(home(), ".cursor")),
|
|
427
|
+
headerEnv: "env-colon",
|
|
287
428
|
write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, standardEntry(entry)),
|
|
288
429
|
},
|
|
289
430
|
];
|
|
431
|
+
// Every client's write receives entries in its own env-reference syntax,
|
|
432
|
+
// whether it is called through writeMcpConfig or directly.
|
|
433
|
+
for (const client of MCP_CLIENTS) {
|
|
434
|
+
const write = client.write;
|
|
435
|
+
client.write = (existing, name, entry) => write(existing, name, entryForClient(client, entry).entry);
|
|
436
|
+
}
|
|
290
437
|
export function findMcpClient(id) {
|
|
291
438
|
return MCP_CLIENTS.find((c) => c.id === id);
|
|
292
439
|
}
|
|
293
|
-
/** Merge the entry into one client's config file. Never writes a literal secret: callers pass
|
|
440
|
+
/** Merge the entry into one client's config file. Never writes a literal secret: callers pass `${VAR}` references, which each client receives in its own syntax. */
|
|
294
441
|
export function writeMcpConfig(client, cwd, name, entry) {
|
|
295
442
|
const file = client.file(cwd);
|
|
296
443
|
if (!entry.url && client.id === "claude-desktop" && !entry.command) {
|
|
297
444
|
return { client: client.id, file, written: false, note: "Claude Desktop reads only stdio servers from its config file; add a remote server as a connector in the app." };
|
|
298
445
|
}
|
|
299
446
|
const existing = readOr(file, "");
|
|
447
|
+
const adapted = entryForClient(client, entry);
|
|
300
448
|
const next = client.write(existing, name, entry);
|
|
301
449
|
mkdirSync(dirname(file), { recursive: true });
|
|
302
450
|
writeFileSync(file, next);
|
|
303
|
-
|
|
451
|
+
const note = [client.incompatible, adapted.note].filter(Boolean).join(" ");
|
|
452
|
+
return { client: client.id, file, written: true, ...(note ? { note } : {}) };
|
|
304
453
|
}
|
|
305
454
|
/** Does a client's config already mention this server? For doctor. */
|
|
306
455
|
export function mcpConfigured(client, cwd, name) {
|
|
@@ -346,7 +495,6 @@ export function agentInstructionsFile(cwd) {
|
|
|
346
495
|
return claude;
|
|
347
496
|
return agents;
|
|
348
497
|
}
|
|
349
|
-
/** One line per command, pipe-delimited: the compact index that goes into AGENTS.md. */
|
|
350
498
|
/** string, number, usd|eur, string[], usd|eur[], object, json — the type as a reader expects it. */
|
|
351
499
|
export function flagTypeLabel(f) {
|
|
352
500
|
const inline = (values) => values && values.join("|").length <= 24 ? values.join("|") : values ? "enum" : undefined;
|
|
@@ -354,6 +502,7 @@ export function flagTypeLabel(f) {
|
|
|
354
502
|
return (inline(f.items?.enum) ?? f.items?.type ?? "json") + "[]";
|
|
355
503
|
return inline(f.enum) ?? f.type;
|
|
356
504
|
}
|
|
505
|
+
/** One line per command, pipe-delimited: the compact index that goes into AGENTS.md. */
|
|
357
506
|
export function compactIndex(commands) {
|
|
358
507
|
return commands
|
|
359
508
|
.map((c) => {
|
|
@@ -362,27 +511,56 @@ export function compactIndex(commands) {
|
|
|
362
511
|
})
|
|
363
512
|
.join("\n");
|
|
364
513
|
}
|
|
514
|
+
/** Bytes of command index the guide and AGENTS.md carry before they switch to one line per resource. */
|
|
515
|
+
export const COMMAND_INDEX_BUDGET = 8_000;
|
|
516
|
+
const RESOURCE_INDEX_NAMES = 12;
|
|
517
|
+
/**
|
|
518
|
+
* One line per resource with its first command names, for an API whose
|
|
519
|
+
* per-command index would not fit COMMAND_INDEX_BUDGET. Resources past the
|
|
520
|
+
* budget are counted, not listed; help --json has them all.
|
|
521
|
+
*/
|
|
522
|
+
export function resourceIndex(bin, commands) {
|
|
523
|
+
const byResource = new Map();
|
|
524
|
+
for (const c of commands)
|
|
525
|
+
byResource.set(c.resource, [...(byResource.get(c.resource) ?? []), c.command]);
|
|
526
|
+
const lines = [];
|
|
527
|
+
let bytes = 0;
|
|
528
|
+
let listed = 0;
|
|
529
|
+
for (const [resource, names] of byResource) {
|
|
530
|
+
const more = names.length - RESOURCE_INDEX_NAMES;
|
|
531
|
+
const line = resource + ": " + names.slice(0, RESOURCE_INDEX_NAMES).join(", ") + (more > 0 ? ", … +" + more + " more" : "");
|
|
532
|
+
if (bytes + line.length + 1 > COMMAND_INDEX_BUDGET)
|
|
533
|
+
break;
|
|
534
|
+
lines.push(line);
|
|
535
|
+
bytes += line.length + 1;
|
|
536
|
+
listed++;
|
|
537
|
+
}
|
|
538
|
+
if (listed < byResource.size)
|
|
539
|
+
lines.push("… " + (byResource.size - listed) + " more resources: " + bin + " help --json");
|
|
540
|
+
return lines.join("\n");
|
|
541
|
+
}
|
|
365
542
|
/** The AGENTS.md block body. */
|
|
366
543
|
export function agentBlock(ctx, commands) {
|
|
367
544
|
const auth = ctx.authEnvVars.length ? ctx.authEnvVars.join(", ") : "(none)";
|
|
368
|
-
const
|
|
545
|
+
const docsIndex = ctx.docsIndexUrl ?? (ctx.docsUrl ? ctx.docsUrl.replace(/\/+$/, "") + "/llms.txt" : null);
|
|
546
|
+
const index = compactIndex(commands);
|
|
369
547
|
return [
|
|
370
548
|
"## " + ctx.bin + " CLI (" + ctx.apiTitle + ")",
|
|
371
549
|
"",
|
|
372
550
|
"API commands write JSON on stdout; discovery commands take --json. Errors are JSON on stderr ({status, issues[{code,message}], next_steps}), exit 0/1/2. Non-interactive under an agent: no prompts, no browsers.",
|
|
373
551
|
"",
|
|
374
|
-
...((ctx.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
552
|
+
...((ctx.omittedOperationCount ?? 0) > 0 ? ["- Coverage: this build includes " + (ctx.generatedOperationCount ?? commands.length) + " of " + ((ctx.generatedOperationCount ?? commands.length) + ctx.omittedOperationCount) + " operations; api.json lists the rest, and calling one returns `PLAN_LIMIT`."] : []),
|
|
553
|
+
ctx.authNotDeclared
|
|
554
|
+
? "- Auth: not declared by the API Spec. If the API needs a token, set " + auth + " or run `" + ctx.bin + " login`; other headers go in `--header \"Name: value\"` or " + ctx.envPrefix + "_HEADERS. Never write a key into a file in this repo."
|
|
555
|
+
: "- Auth: " + auth + " in the environment, or `" + ctx.bin + " login`. Never write a key into a file in this repo.",
|
|
556
|
+
"- Discover: `" + ctx.bin + " help --json` indexes resources and command names; `" + ctx.bin + " help <resource> --json` lists a resource's commands; `" + ctx.bin + " help <resource> <command> --json` gives one command's flags. `" + ctx.bin + " help --json --all` prints every command with every flag at once. Humans: `" + ctx.bin + " --help`, `" + ctx.bin + " <resource> <command> --help`.",
|
|
557
|
+
"- Docs: " + (docsIndex ? "`" + ctx.bin + " docs search <term> --json`; " + docsIndex : "`" + ctx.bin + " docs <resource> <command> --json` (a docs URL was not provided at generate time)") + ".",
|
|
378
558
|
"- Lists: `--all` streams every page as NDJSON. Destructive commands need `--force`.",
|
|
379
559
|
...(ctx.hasMcp ? ["- MCP: `" + ctx.bin + " mcp install --all` registers this API's MCP server with the agent clients on this machine" + (ctx.mcpUrl ? " (hosted: " + ctx.mcpUrl + ")" : "") + "."] : []),
|
|
380
560
|
"",
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
compactIndex(commands),
|
|
385
|
-
"```",
|
|
561
|
+
...(Buffer.byteLength(index) <= COMMAND_INDEX_BUDGET
|
|
562
|
+
? ["Commands (resource command | METHOD path | required flags | summary):", "", "```", index, "```"]
|
|
563
|
+
: ["Commands by resource (" + commands.length + " commands; `" + ctx.bin + " help <resource> --json` lists a resource's commands with summaries):", "", "```", resourceIndex(ctx.bin, commands), "```"]),
|
|
386
564
|
].join("\n");
|
|
387
565
|
}
|
|
388
566
|
/** What `agent-guide --format json` returns. */
|
|
@@ -401,12 +579,12 @@ export function agentGuide(ctx, commands) {
|
|
|
401
579
|
package: ctx.pkg,
|
|
402
580
|
api: ctx.apiTitle,
|
|
403
581
|
version: ctx.version,
|
|
404
|
-
generated_by: "
|
|
582
|
+
generated_by: "Typeship",
|
|
405
583
|
guide: agentBlock(ctx, commands),
|
|
406
584
|
first_command: first ? ctx.bin + " " + first.resource + " " + first.command : ctx.bin + " --help",
|
|
407
585
|
docs_index_url: docsIndexUrl,
|
|
408
586
|
docs_full_url: docsFullUrl,
|
|
409
|
-
...((ctx.
|
|
587
|
+
...((ctx.omittedOperationCount ?? 0) > 0 ? { coverage: { generated_operations: ctx.generatedOperationCount ?? commands.length, total_operations: (ctx.generatedOperationCount ?? commands.length) + ctx.omittedOperationCount } } : {}),
|
|
410
588
|
hosted_mcp_url: ctx.mcpUrl,
|
|
411
589
|
local_mcp: ctx.hasMcp ? ctx.bin + "-mcp (stdio) or '" + ctx.bin + " mcp install --all'" : null,
|
|
412
590
|
skills_install: ctx.skillsRepo ? "npx skills add " + ctx.skillsRepo : null,
|
|
@@ -419,12 +597,13 @@ export function agentGuide(ctx, commands) {
|
|
|
419
597
|
destructive: "Commands classified as destructive need --force (or --yes); without it they return CONFIRMATION_REQUIRED with the exact command to run.",
|
|
420
598
|
flags: "Positional path arguments first, then --flags. Array flags take a comma list, the flag repeated, or a JSON array; object flags take JSON. --data '<json>' merges under field flags (@<file> reads a file, - reads stdin).",
|
|
421
599
|
pagination: "Paginated commands return {items, hasMore, nextPage, nextCommand}: run nextCommand for the next page, or --all walks every page.",
|
|
600
|
+
discovery: "help --json is a bounded index of resources and command names. help <resource> --json pages through one resource's commands (next_command), help <resource> <command> --json is one command's flags, docs <resource> <command> --json its complete schemas, and docs search <term> --json finds commands by topic. help --json --all prints every command with every flag; on a large API it is large.",
|
|
422
601
|
},
|
|
423
602
|
builtins: ctx.builtins,
|
|
424
603
|
next_steps: [
|
|
425
604
|
...(ctx.authEnvVars.length ? ["Set " + ctx.authEnvVars[0] + " in the environment or run '" + ctx.bin + " login'."] : []),
|
|
426
605
|
"Run '" + ctx.bin + " auth check'.",
|
|
427
|
-
"Run '" + ctx.bin + " help --json' for the command index,
|
|
606
|
+
"Run '" + ctx.bin + " help --json' for the command index, then '" + ctx.bin + " help <resource> <command> --json' for the command you need.",
|
|
428
607
|
...(ctx.hasMcp ? ["Run '" + ctx.bin + " mcp install --all' if this session has an MCP-capable client."] : []),
|
|
429
608
|
],
|
|
430
609
|
};
|
|
@@ -544,3 +723,111 @@ export function summarizeDoctor(checks) {
|
|
|
544
723
|
next_steps: failing.map((c) => c.fix).filter((f) => Boolean(f)),
|
|
545
724
|
};
|
|
546
725
|
}
|
|
726
|
+
/** Every OAuth scope an operation's security requirements name, in order. */
|
|
727
|
+
export function requiredScopes(security) {
|
|
728
|
+
return [...new Set((security ?? []).flatMap((requirement) => Object.values(requirement).flat()))];
|
|
729
|
+
}
|
|
730
|
+
export const REDACTED = "<redacted>";
|
|
731
|
+
/** Header and query names that carry credentials, whatever the API calls
|
|
732
|
+
* them: Authorization, Cookie, X-Api-Key, api_key, access_token, a session
|
|
733
|
+
* or signature. */
|
|
734
|
+
const SENSITIVE_NAME = /^(authorization|proxy-authorization|cookie|cookie2|set-cookie)$|api[-_]?key|apikey|token|secret|password|passwd|session|signature|credential|^key$|^auth$/i;
|
|
735
|
+
function redactText(text, secrets) {
|
|
736
|
+
let out = text;
|
|
737
|
+
for (const secret of secrets)
|
|
738
|
+
if (secret.length >= 4)
|
|
739
|
+
out = out.split(secret).join(REDACTED);
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* The request a dry run captured, as --dry-run shows it. `sensitiveNames`
|
|
744
|
+
* are the header and query names the API's security schemes bind; `secrets`
|
|
745
|
+
* are the resolved credential values, redacted wherever they appear (a
|
|
746
|
+
* token pasted into a body field, a key in a URL).
|
|
747
|
+
*/
|
|
748
|
+
export async function requestPreview(input, options = {}) {
|
|
749
|
+
const secrets = [...new Set((options.secrets ?? []).filter((s) => typeof s === "string" && s.length >= 4))].sort((a, b) => b.length - a.length);
|
|
750
|
+
const named = new Set((options.sensitiveNames ?? []).map((n) => n.toLowerCase()));
|
|
751
|
+
const sensitive = (name) => named.has(name.toLowerCase()) || SENSITIVE_NAME.test(name);
|
|
752
|
+
const url = new URL(input.url);
|
|
753
|
+
const query = {};
|
|
754
|
+
for (const [name, raw] of url.searchParams) {
|
|
755
|
+
const value = sensitive(name) ? REDACTED : redactText(raw, secrets);
|
|
756
|
+
const previous = query[name];
|
|
757
|
+
query[name] = previous === undefined ? value : Array.isArray(previous) ? [...previous, value] : [previous, value];
|
|
758
|
+
}
|
|
759
|
+
const encodedSecrets = [...secrets, ...secrets.map(encodeURIComponent)];
|
|
760
|
+
const search = [...url.searchParams].map(([name, raw]) => encodeURIComponent(name) + "=" + (sensitive(name) ? REDACTED : redactText(encodeURIComponent(raw), encodedSecrets))).join("&");
|
|
761
|
+
const shownUrl = url.origin + redactText(url.pathname, encodedSecrets) + (search ? "?" + search : "");
|
|
762
|
+
const headers = {};
|
|
763
|
+
for (const [name, value] of Object.entries(input.headers)) {
|
|
764
|
+
headers[name] = sensitive(name) ? REDACTED : redactText(value, secrets);
|
|
765
|
+
}
|
|
766
|
+
const contentType = Object.entries(input.headers).find(([name]) => name.toLowerCase() === "content-type")?.[1] ?? "";
|
|
767
|
+
const body = await previewBody(input.body, contentType, secrets);
|
|
768
|
+
return {
|
|
769
|
+
dry_run: true,
|
|
770
|
+
sent: false,
|
|
771
|
+
message: "Dry run: nothing was sent to the API. This is the request the command would send, with credentials redacted.",
|
|
772
|
+
request: {
|
|
773
|
+
method: input.method,
|
|
774
|
+
url: shownUrl,
|
|
775
|
+
path: redactText(url.pathname, encodedSecrets),
|
|
776
|
+
query,
|
|
777
|
+
headers,
|
|
778
|
+
...(body !== undefined ? { body } : {}),
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
async function previewBody(body, contentType, secrets) {
|
|
783
|
+
if (body === undefined || body === null)
|
|
784
|
+
return undefined;
|
|
785
|
+
const redactValue = (value) => {
|
|
786
|
+
if (typeof value === "string")
|
|
787
|
+
return redactText(value, secrets);
|
|
788
|
+
if (Array.isArray(value))
|
|
789
|
+
return value.map(redactValue);
|
|
790
|
+
if (value && typeof value === "object")
|
|
791
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, SENSITIVE_NAME.test(k) && typeof v === "string" && secrets.some((s) => v.includes(s)) ? REDACTED : redactValue(v)]));
|
|
792
|
+
return value;
|
|
793
|
+
};
|
|
794
|
+
if (typeof body === "string") {
|
|
795
|
+
if (/json/i.test(contentType)) {
|
|
796
|
+
try {
|
|
797
|
+
return redactValue(JSON.parse(body));
|
|
798
|
+
}
|
|
799
|
+
catch { /* shown as text */ }
|
|
800
|
+
}
|
|
801
|
+
return redactText(body, secrets);
|
|
802
|
+
}
|
|
803
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) {
|
|
804
|
+
const parts = [];
|
|
805
|
+
for (const [name, value] of body.entries()) {
|
|
806
|
+
parts.push(typeof value === "string"
|
|
807
|
+
? { name, value: redactText(value, secrets) }
|
|
808
|
+
: { name, filename: value.name, ...(value.type ? { type: value.type } : {}), bytes: value.size });
|
|
809
|
+
}
|
|
810
|
+
return { multipart: parts };
|
|
811
|
+
}
|
|
812
|
+
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams)
|
|
813
|
+
return redactText(body.toString(), secrets);
|
|
814
|
+
if (typeof Blob !== "undefined" && body instanceof Blob)
|
|
815
|
+
return { binary: true, bytes: body.size, ...(body.type ? { type: body.type } : {}) };
|
|
816
|
+
if (body instanceof Uint8Array || body instanceof ArrayBuffer)
|
|
817
|
+
return { binary: true, bytes: body.byteLength };
|
|
818
|
+
return { stream: true };
|
|
819
|
+
}
|
|
820
|
+
/** --dry-run for a person: the request line, then query, headers and body. */
|
|
821
|
+
export function formatRequestPreview(preview) {
|
|
822
|
+
const { request } = preview;
|
|
823
|
+
const lines = ["Dry run: nothing was sent. Credentials are redacted.", "", request.method + " " + request.url];
|
|
824
|
+
const headers = Object.entries(request.headers);
|
|
825
|
+
if (headers.length)
|
|
826
|
+
lines.push("", "Headers:", ...headers.map(([name, value]) => " " + name + ": " + value));
|
|
827
|
+
if (request.body !== undefined) {
|
|
828
|
+
lines.push("", "Body:");
|
|
829
|
+
const text = typeof request.body === "string" ? request.body : JSON.stringify(request.body, null, 2);
|
|
830
|
+
lines.push(...text.split("\n").map((line) => " " + line));
|
|
831
|
+
}
|
|
832
|
+
return lines.join("\n") + "\n";
|
|
833
|
+
}
|