@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/src/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Typeship — command-line client. Generated by Typeship — https://typeship.dev
|
|
3
3
|
// Flags-only for API commands (CI-safe); `login` is the one interactive
|
|
4
4
|
// exception (hidden prompt on a TTY, --with-token/--token for scripts).
|
|
5
5
|
// Prints raw JSON to stdout; errors as JSON on stderr.
|
|
@@ -9,40 +9,56 @@ import { spawnSync } from "node:child_process";
|
|
|
9
9
|
import { oauthBrowserLogin, type OAuthLoginSession } from "./oauth-login.js";
|
|
10
10
|
import { oauthDeviceLogin, customBrowserApproval, loginEndpoint, type ApprovedCredential } from "./polling-login.js";
|
|
11
11
|
import { oauthStatusRequest } from "./oauth-request.js";
|
|
12
|
-
import {
|
|
13
|
-
import { type FileCredentialStore, assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, type CredentialDestination, oauthSessionToken, sessionBinding, type SessionConfiguration, type StoredCredentials as StoredCreds } from "./oauth-session.js";
|
|
12
|
+
import { type FileCredentialStore, sessionCredential, assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, type CredentialDestination, oauthSessionToken, sessionBinding, type SessionConfiguration, type StoredCredentials as StoredCreds } from "./oauth-session.js";
|
|
14
13
|
import { createCredentialStore } from "./credential-storage.js";
|
|
15
14
|
import { identityPolicyOf, identityFetch, identityResult, verifyApiIdentity, verifyClientIdentity, readApiIdentity, assertApiIdentity, type ApiIdentity, type IdentityConfiguration, type IdentityPolicy, type VerifiedIdentity } from "./api-identity.js";
|
|
16
|
-
import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability, type NamedCredentials, type CredentialSchemes } from "./named-credentials.js";
|
|
15
|
+
import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability, missingCredentials, oauthSessionSchemes, parseExtraHeaders, applyExtraHeaders, type NamedCredentials, type CredentialSchemes } from "./named-credentials.js";
|
|
17
16
|
import { resolveProfile, listProfiles, selectProfile, removeProfile, readProfileConfig, updateProfileConfig, type ProfileContext } from "./auth-profiles.js";
|
|
18
17
|
import { randomBytes } from "node:crypto";
|
|
19
18
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
20
19
|
import { homedir, hostname } from "node:os";
|
|
21
|
-
import { basename, dirname, join } from "node:path";
|
|
20
|
+
import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
22
21
|
import { fileURLToPath } from "node:url";
|
|
23
22
|
import { TypeshipClient, formatDebugEvent, type ClientOptions, type DebugEvent } from "./index.js";
|
|
24
|
-
import { asApiResult, validateAgainstSchema, ValidationError, type Violation } from "./core/http.js";
|
|
23
|
+
import { asApiResult, mediaTypeForPath, validateAgainstSchema, ValidationError, type Violation } from "./core/http.js";
|
|
25
24
|
import { SCHEMAS, DEFS } from "./schemas.js";
|
|
26
|
-
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";
|
|
27
26
|
import {
|
|
28
|
-
MCP_CLIENTS, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, collectionProperty, detectHarness, envelope,
|
|
29
|
-
exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig,
|
|
27
|
+
MCP_CLIENTS, requiredScopes, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, classifyAuthFailure, collectionProperty, detectHarness, envelope,
|
|
28
|
+
exitCodeFor, findMcpClient, formatRequestPreview, installSkills, mcpConfigured, pendingClaims, recordClaim, requestPreview, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig,
|
|
30
29
|
type AgentContext, type CommandSummary, type DoctorCheck, type EnvelopeInput, type IssueCode, type McpEntry, type McpWriteResult,
|
|
31
30
|
} from "./cli-agent.js";
|
|
32
31
|
import { relativeDate } from "./dates.js";
|
|
32
|
+
import { checkValue, type ArgumentIssue } from "./arguments.js";
|
|
33
33
|
import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
|
|
34
|
+
import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
|
|
35
|
+
import { renderTable } from "./table.js";
|
|
36
|
+
import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
|
|
37
|
+
import { argumentPathText, findInputType, inputTypeText, namedTypesIn } from "./type-docs.js";
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
const BIN = "typeship";
|
|
41
|
+
/** The MCP server's key in client configs: the API's name, even when this
|
|
42
|
+
* command was renamed away from a vendor's. It reads this command's login. */
|
|
43
|
+
const MCP_SERVER_KEY = "typeship";
|
|
37
44
|
const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
|
|
38
45
|
const NAMED_SCHEMES: CredentialSchemes = {"apiKey":{"kind":"bearer","options":["bearerToken"]}};
|
|
39
46
|
const AUTH_SCALARS: { option: string; flag: string; env: string }[] = [{"option":"bearerToken","flag":"token","env":"TYPESHIP_TOKEN"}];
|
|
47
|
+
/** Hosted MCP request headers as name → env reference, never a literal. */
|
|
48
|
+
const HOSTED_MCP_HEADERS: Record<string, string> = {"Authorization":"Bearer ${TYPESHIP_TOKEN}"};
|
|
49
|
+
const HOSTED_MCP_NOTE: string | null = null;
|
|
40
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[] = [];
|
|
53
|
+
/** The spec declares no security, so the token is offered, never required. */
|
|
54
|
+
const AUTH_UNDECLARED = false;
|
|
55
|
+
/** Repeated --header "Name: value" flags for this invocation. */
|
|
56
|
+
let HEADER_FLAGS: string[] = [];
|
|
41
57
|
/** Operations omitted from the generated package by its plan cap. */
|
|
42
58
|
const EXCLUDED_OPS = 0;
|
|
43
59
|
/** Generated CLI operations that are intentionally unavailable to MCP. */
|
|
44
60
|
const MCP_EXCLUDED_OPS = 0;
|
|
45
|
-
const VERSION = "0.
|
|
61
|
+
const VERSION = "0.24.0";
|
|
46
62
|
const API_VERSION = "1.0.0";
|
|
47
63
|
const SPEC_FORMAT = "openapi";
|
|
48
64
|
const IDENTITY_POLICY: IdentityPolicy = {};
|
|
@@ -51,10 +67,13 @@ const WHOAMI: { resource: string; method: string } | null = null;
|
|
|
51
67
|
const ENVIRONMENTS: Record<string, string> = {};
|
|
52
68
|
const HAS_MCP = false;
|
|
53
69
|
const PKG_NAME = "@typeship-ax/cli";
|
|
70
|
+
/** False when the package name was derived rather than chosen: the npm
|
|
71
|
+
* package of that name may be someone else's, so upgrade never installs it. */
|
|
72
|
+
const PKG_CONFIRMED = true;
|
|
54
73
|
const UPDATE_NOTICE = false;
|
|
55
74
|
const API_DESCRIPTION: string | null = "Resolve an OpenAPI or GraphQL Spec, diagnose it, and keep every\nselected CLI, MCP, and SDK Target current.\n\nEvery operation but one requires a bearer credential: an organization\nAPI key from the console, or an OAuth access token carrying the operation's\nread, generate, or write capability and the organization selected during\nconsent. OAuth grants cannot switch organizations after consent. A browser\nsession is not a credential for this API. The exception is POST /generate,\nwhich works anonymously with the free plan's limits.\n\nExamples use Parcel, a fictional delivery service. Replace its domains,\nrepository names, and resource identifiers with your own. The hosted\npetstore Spec is a runnable sample.\n";
|
|
56
|
-
const DOCS_URL_DEFAULT: string | null = "https://typeship.dev";
|
|
57
|
-
const DOCS_INDEX_URL_DEFAULT: string | null =
|
|
75
|
+
const DOCS_URL_DEFAULT: string | null = "https://typeship.dev/docs";
|
|
76
|
+
const DOCS_INDEX_URL_DEFAULT: string | null = "https://typeship.dev/llms.txt";
|
|
58
77
|
const RELAY: { mintUrl: string; project: string } | null = null;
|
|
59
78
|
const SUPPORT_URL: string | null = null;
|
|
60
79
|
const OAUTH_TOKEN_URL: string | null = null;
|
|
@@ -64,6 +83,9 @@ const OAUTH_DISCOVERY_URLS: string[] = [];
|
|
|
64
83
|
const OAUTH_DEVICE_URL: string | null = null;
|
|
65
84
|
const HAS_OAUTH_LOGIN = OAUTH_TOKEN_URL !== null || OAUTH_DISCOVERY_URLS.length > 0;
|
|
66
85
|
const OAUTH_LOGIN_METHOD: string = "device";
|
|
86
|
+
/** The loopback port browser login listens on unless the redirect URI names
|
|
87
|
+
* one: fixed per CLI, so it can be registered with the provider. */
|
|
88
|
+
const OAUTH_DEFAULT_REDIRECT_PORT = 49910;
|
|
67
89
|
const OAUTH_REDIRECT_URI: string | undefined = undefined;
|
|
68
90
|
const OAUTH_ORGANIZATION_PARAMETER: "organization" | "organization_id" | undefined = undefined;
|
|
69
91
|
const OAUTH_AUTHORIZATION_URL: string | undefined = undefined;
|
|
@@ -74,7 +96,7 @@ const MCP_URL: string | null = "https://typeship.dev/mcp";
|
|
|
74
96
|
const SKILLS_REPO: string | null = "typeship-ax/skills";
|
|
75
97
|
const CLI_AUTH_URL: string | null = "https://typeship.dev/api/auth/cli";
|
|
76
98
|
const ENV_PREFIX = "TYPESHIP";
|
|
77
|
-
const API_TITLE = "
|
|
99
|
+
const API_TITLE = "Typeship";
|
|
78
100
|
|
|
79
101
|
interface Parsed {
|
|
80
102
|
positionals: string[];
|
|
@@ -91,7 +113,7 @@ interface Parsed {
|
|
|
91
113
|
* API parameter with the same name (`accounts list --cursor <c>`) still
|
|
92
114
|
* takes its value. Boolean API parameters are recognized once the command
|
|
93
115
|
* is known. */
|
|
94
|
-
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"]);
|
|
95
117
|
const BUILTIN_BOOLEAN_FLAGS: Record<string, string[]> = {
|
|
96
118
|
login: ["with-token", "no-browser", "device"],
|
|
97
119
|
logout: ["local"],
|
|
@@ -99,7 +121,7 @@ const BUILTIN_BOOLEAN_FLAGS: Record<string, string[]> = {
|
|
|
99
121
|
mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
|
|
100
122
|
docs: ["web", "schema"],
|
|
101
123
|
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
|
|
102
|
-
auth: ["live"],
|
|
124
|
+
auth: ["live", "offline"],
|
|
103
125
|
doctor: [],
|
|
104
126
|
};
|
|
105
127
|
|
|
@@ -222,25 +244,64 @@ function out(value: unknown): void {
|
|
|
222
244
|
/** --fields a,b.c: the dotted paths to keep in API results (null = everything). Set in main(). */
|
|
223
245
|
let FIELDS: string[][] | null = null;
|
|
224
246
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return
|
|
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
|
+
|
|
256
|
+
/** Whether the command being run writes: an --fields mistake on a write
|
|
257
|
+
* must not tempt anyone into running it again. Set in main(). */
|
|
258
|
+
let FIELDS_AFTER_WRITE = false;
|
|
259
|
+
|
|
260
|
+
/** Keep only FIELDS of a result: arrays item by item, objects by dotted path;
|
|
261
|
+
* scalars untouched. A path that matches nothing is an error naming the keys
|
|
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 {
|
|
265
|
+
if (FIELDS === null) return value;
|
|
266
|
+
const unmatched = unmatchedFields(value, FIELDS, schema);
|
|
267
|
+
if (unmatched.length > 0) failUnmatchedFields(unmatched, perItem, value);
|
|
268
|
+
return projectFields(value, FIELDS);
|
|
269
|
+
}
|
|
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
|
+
|
|
276
|
+
/** --fields over a stream (--all, events): paths no item has matched yet.
|
|
277
|
+
* The stream is printed as it arrives, so the check fails at its end. */
|
|
278
|
+
function streamFieldsCheck(schema: unknown): { item(value: unknown): unknown; finish(): void } {
|
|
279
|
+
let pending: ReturnType<typeof unmatchedFields> | null = null;
|
|
280
|
+
return {
|
|
281
|
+
item(value: unknown): unknown {
|
|
282
|
+
if (FIELDS === null) return value;
|
|
283
|
+
const unmatched = unmatchedFields(value, FIELDS, schema);
|
|
284
|
+
pending = pending === null ? unmatched : pending.filter((p) => unmatched.some((u) => u.path === p.path));
|
|
285
|
+
return projectFields(value, FIELDS);
|
|
286
|
+
},
|
|
287
|
+
finish(): void {
|
|
288
|
+
if (pending !== null && pending.length > 0) failUnmatchedFields(pending, true, undefined);
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function failUnmatchedFields(unmatched: ReturnType<typeof unmatchedFields>, perItem: boolean, result: unknown): never {
|
|
294
|
+
return failWith({
|
|
295
|
+
code: "FIELDS_UNMATCHED",
|
|
296
|
+
message: unmatchedFieldsMessage(unmatched, perItem),
|
|
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 } : {}) },
|
|
304
|
+
});
|
|
244
305
|
}
|
|
245
306
|
|
|
246
307
|
/** Thrown after scheduling exit so sync callers stop; main() swallows it. */
|
|
@@ -323,7 +384,7 @@ let USAGE_HINT = BIN + " --help";
|
|
|
323
384
|
function fail(code: number, message: string, extra?: unknown, nextSteps?: string[]): never {
|
|
324
385
|
const usageCode: IssueCode = /^Unknown command/.test(message) ? "UNKNOWN_COMMAND"
|
|
325
386
|
: /^Unknown flag/.test(message) ? "UNKNOWN_FLAG"
|
|
326
|
-
: /^(Missing required|Expected \d+ argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
387
|
+
: /^(Missing required|Expected \d+ (or \d+ )?argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
327
388
|
: "INVALID_USAGE";
|
|
328
389
|
return failWith({
|
|
329
390
|
code: code === 2 ? usageCode : "CALL_FAILED",
|
|
@@ -334,8 +395,26 @@ function fail(code: number, message: string, extra?: unknown, nextSteps?: string
|
|
|
334
395
|
}
|
|
335
396
|
|
|
336
397
|
/** An SDK error result as an envelope: status-derived code, the API's body as detail, concrete next steps. */
|
|
398
|
+
/** OAuth scopes of the operation being run, so a 403 can name them. */
|
|
399
|
+
let CURRENT_SCOPES: string[] = [];
|
|
337
400
|
function failApi(error: unknown, hadCredential: boolean): never {
|
|
338
|
-
return failWith(classifyApiError(error, { bin: BIN, hadCredential, docsUrl: DOCS_URL_DEFAULT }));
|
|
401
|
+
return failWith(classifyAuthFailure(error, authFailureContext()) ?? classifyApiError(error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential, docsUrl: DOCS_URL_DEFAULT, requiredScopes: CURRENT_SCOPES, canLogin: HAS_OAUTH_LOGIN }));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The SDK raises NotModifiedError for a 304: a conditional request
|
|
405
|
+
* matched. For the CLI that is a result, not a failure. */
|
|
406
|
+
function isNotModified(error: unknown): error is { etag?: string } {
|
|
407
|
+
return (error as { name?: string } | null)?.name === "NotModifiedError";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function printNotModified(error: { etag?: string }): Promise<never> {
|
|
411
|
+
out({ ok: true, not_modified: true, ...(error.etag ? { etag: error.etag } : {}) });
|
|
412
|
+
return flushExit(0);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** What a login, saved-session or credential-store failure names as alternatives. */
|
|
416
|
+
function authFailureContext(): { bin: string; envVars: string[]; storeVariable: string } {
|
|
417
|
+
return { bin: BIN, storeVariable: ENV_PREFIX + "_CREDENTIAL_STORE", envVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? [ENV_PREFIX + "_CREDENTIALS"] : [])] };
|
|
339
418
|
}
|
|
340
419
|
|
|
341
420
|
// ---------------------------------------------------------------------------
|
|
@@ -353,7 +432,7 @@ function credsPath(): string {
|
|
|
353
432
|
return credentialStore().path;
|
|
354
433
|
}
|
|
355
434
|
|
|
356
|
-
function credentialStore(): FileCredentialStore { return createCredentialStore(configDir(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
435
|
+
function credentialStore(): FileCredentialStore { return createCredentialStore(configDir(), BIN, process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
357
436
|
function readCreds(): StoredCreds | null { return credentialStore().read(); }
|
|
358
437
|
function sessionConfiguration(baseUrl: string, config = readConfig()): SessionConfiguration {
|
|
359
438
|
return {
|
|
@@ -492,9 +571,10 @@ async function deviceLogin(clientId: string, parsed: Parsed): Promise<void> {
|
|
|
492
571
|
if (!apiBaseUrl) fail(2, "Set the API base URL before logging in.");
|
|
493
572
|
const loginConfiguration = sessionConfiguration(apiBaseUrl!);
|
|
494
573
|
await credentialStore().prepare();
|
|
574
|
+
if (!OAUTH_DEVICE_URL && !OAUTH_ISSUER && !OAUTH_DISCOVERY_URL) fail(2, "This API does not declare device authorization. Run '" + BIN + " login' without --device to sign in through the browser.");
|
|
495
575
|
const session = await withLoginCancellation((signal) => oauthDeviceLogin({
|
|
496
576
|
clientId, issuer: OAUTH_ISSUER, discoveryUrls: OAUTH_DISCOVERY_URLS,
|
|
497
|
-
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes:
|
|
577
|
+
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: loginScopes(parsed.flags),
|
|
498
578
|
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
499
579
|
}, { signal, authorize({ verificationUri, userCode, expiresIn }) {
|
|
500
580
|
process.stderr.write("Open " + paintErr("cyan", verificationUri) + " and enter code: " + paintErr("bold", userCode) + "\n");
|
|
@@ -509,11 +589,40 @@ async function deviceLogin(clientId: string, parsed: Parsed): Promise<void> {
|
|
|
509
589
|
await flushExit(0);
|
|
510
590
|
}
|
|
511
591
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
592
|
+
type LoginValue = string | { username: string; password: string };
|
|
593
|
+
/** What --with-token, the prompt and browser approval store. */
|
|
594
|
+
interface LoginTarget { label: string; basic: boolean; storedAs: string; save(value: LoginValue): StoredCreds }
|
|
595
|
+
|
|
596
|
+
/** The scheme named by --scheme, else the convenience credential that the
|
|
597
|
+
* most operations accept on its own (declared order breaks ties), else null. */
|
|
598
|
+
function loginTarget(flags: Map<string, string | boolean>): LoginTarget | null {
|
|
599
|
+
const requested = flags.get("scheme");
|
|
600
|
+
if (requested !== undefined) {
|
|
601
|
+
if (typeof requested !== "string" || !Object.hasOwn(NAMED_SCHEMES, requested)) fail(2, "--scheme expects one of this API's security schemes: " + (Object.keys(NAMED_SCHEMES).join(", ") || "none") + ".");
|
|
602
|
+
const name = requested as string;
|
|
603
|
+
return { label: name, basic: NAMED_SCHEMES[name]!.kind === "basic", storedAs: name, save(value) {
|
|
604
|
+
try { return { named: parseNamedCredentials({ [name]: value }, NAMED_SCHEMES) }; } catch (error) { fail(2, (error as Error).message); }
|
|
605
|
+
} };
|
|
606
|
+
}
|
|
607
|
+
const candidates = [...AUTH_SCALARS.map((a) => a.option), ...(BASIC ? ["basicAuth"] : [])];
|
|
608
|
+
if (!candidates.length) return null;
|
|
609
|
+
const uses = (option: string) => OPS.filter((op) => op.credentialOptions?.some((alternative) => alternative.length === 1 && alternative[0] === option)).length;
|
|
610
|
+
const option = candidates.reduce((best, candidate) => uses(candidate) > uses(best) ? candidate : best);
|
|
611
|
+
if (option === "basicAuth") return { label: "username and password", basic: true, storedAs: "basic", save: (value) => ({ basic: value as { username: string; password: string } }) };
|
|
612
|
+
const scalar = AUTH_SCALARS.find((a) => a.option === option)!;
|
|
613
|
+
return { label: scalar.flag.replace(/-/g, " "), basic: false, storedAs: scalar.flag, save: (value) => ({ scalars: { [scalar.option]: value as string } }) };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Basic credentials on stdin are one line: username:password. */
|
|
617
|
+
function basicFromText(text: string): { username: string; password: string } {
|
|
618
|
+
const separator = text.indexOf(":");
|
|
619
|
+
if (separator <= 0 || separator === text.length - 1) fail(2, "--with-token expects username:password on stdin for Basic auth.");
|
|
620
|
+
return { username: text.slice(0, separator), password: text.slice(separator + 1) };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function storePastedToken(value: LoginValue, target: LoginTarget, flags: Map<string, string | boolean>): Promise<void> {
|
|
624
|
+
await saveLoginCredentials(target.save(value), flags);
|
|
625
|
+
out({ ok: true, method: "paste", stored_as: target.storedAs, credentials: credsPath(), ...loginIdentityReport() });
|
|
517
626
|
await flushExit(0);
|
|
518
627
|
}
|
|
519
628
|
|
|
@@ -547,10 +656,9 @@ async function browserApprove(headless: boolean, flags: Map<string, string | boo
|
|
|
547
656
|
}
|
|
548
657
|
|
|
549
658
|
/** Store what the browser approval minted, marked as this CLI's own. */
|
|
550
|
-
async function storeMinted(minted: ApprovedCredential, flags: Map<string, string | boolean>): Promise<void> {
|
|
551
|
-
const first = AUTH_SCALARS[0]!;
|
|
659
|
+
async function storeMinted(minted: ApprovedCredential, target: LoginTarget, flags: Map<string, string | boolean>): Promise<void> {
|
|
552
660
|
await saveLoginCredentials({
|
|
553
|
-
|
|
661
|
+
...target.save(minted.api_key),
|
|
554
662
|
minted: { via: "browser", key_name: minted.key_name, revocationUrl: minted.revocationUrl, ...(minted.org_id ? { org_id: minted.org_id } : {}) },
|
|
555
663
|
}, flags, undefined, async () => {
|
|
556
664
|
try {
|
|
@@ -567,16 +675,37 @@ async function storeMinted(minted: ApprovedCredential, flags: Map<string, string
|
|
|
567
675
|
});
|
|
568
676
|
}
|
|
569
677
|
|
|
570
|
-
async function browserLogin(headless: boolean, flags: Map<string, string | boolean>): Promise<void> {
|
|
678
|
+
async function browserLogin(headless: boolean, target: LoginTarget, flags: Map<string, string | boolean>): Promise<void> {
|
|
571
679
|
await credentialStore().prepare();
|
|
572
680
|
const minted = await browserApprove(headless, flags);
|
|
573
|
-
await storeMinted(minted, flags);
|
|
574
|
-
out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
|
|
681
|
+
await storeMinted(minted, target, flags);
|
|
682
|
+
out({ ok: true, method: "browser", stored_as: target.storedAs, key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
|
|
575
683
|
await flushExit(0);
|
|
576
684
|
}
|
|
577
685
|
|
|
686
|
+
/** The callback URL browser login uses: the configured redirect URI, else
|
|
687
|
+
* http://127.0.0.1:<port>/callback. --redirect-port or
|
|
688
|
+
* TYPESHIP_OAUTH_REDIRECT_PORT picks another port. */
|
|
689
|
+
function oauthRedirectUri(flags: Map<string, string | boolean>): string {
|
|
690
|
+
const requested = typeof flags.get("redirect-port") === "string" ? flags.get("redirect-port") as string : process.env["TYPESHIP_OAUTH_REDIRECT_PORT"];
|
|
691
|
+
const redirect = new URL(OAUTH_REDIRECT_URI ?? "http://127.0.0.1:" + OAUTH_DEFAULT_REDIRECT_PORT + "/callback");
|
|
692
|
+
if (requested !== undefined) {
|
|
693
|
+
if (!/^[1-9][0-9]{0,4}$/.test(requested) || Number(requested) > 65535) fail(2, "--redirect-port expects a port number from 1 to 65535.");
|
|
694
|
+
redirect.port = requested;
|
|
695
|
+
}
|
|
696
|
+
return redirect.href;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** --scopes a,b narrows (or widens) what this login asks for. */
|
|
700
|
+
function loginScopes(flags: Map<string, string | boolean>): string[] {
|
|
701
|
+
const requested = flags.get("scopes");
|
|
702
|
+
if (requested === undefined) return OAUTH_SCOPES;
|
|
703
|
+
if (typeof requested !== "string" || !requested.trim()) fail(2, "--scopes expects a comma- or space-separated list of scopes.");
|
|
704
|
+
return [...new Set((requested as string).split(/[\s,]+/).filter(Boolean))];
|
|
705
|
+
}
|
|
706
|
+
|
|
578
707
|
async function acquireOAuthBrowserSession(parsed: Parsed, clientId: string): Promise<void> {
|
|
579
|
-
if (!OAUTH_ISSUER) fail(2, "Browser OAuth
|
|
708
|
+
if (!OAUTH_ISSUER && !OAUTH_AUTHORIZATION_URL) fail(2, "Browser OAuth needs an authorization URL: the API Spec's authorizationCode flow, or auth.oauth_server.issuer configured by the API owner.");
|
|
580
709
|
const apiBaseUrl = resolveBaseUrl(parsed.flags);
|
|
581
710
|
if (!apiBaseUrl) fail(2, "Set the API base URL before logging in.");
|
|
582
711
|
const loginConfiguration = sessionConfiguration(apiBaseUrl!);
|
|
@@ -588,7 +717,7 @@ async function acquireOAuthBrowserSession(parsed: Parsed, clientId: string): Pro
|
|
|
588
717
|
} }, parsed.flags, loginConfiguration);
|
|
589
718
|
}
|
|
590
719
|
|
|
591
|
-
/**
|
|
720
|
+
/** The native browser exchange. */
|
|
592
721
|
async function startOAuthBrowserSession(parsed: Parsed, clientId: string, timeoutMs?: number): Promise<OAuthLoginSession> {
|
|
593
722
|
const controller = new AbortController();
|
|
594
723
|
const cancel = () => controller.abort();
|
|
@@ -596,9 +725,9 @@ async function startOAuthBrowserSession(parsed: Parsed, clientId: string, timeou
|
|
|
596
725
|
process.once("SIGTERM", cancel);
|
|
597
726
|
try {
|
|
598
727
|
return await oauthBrowserLogin({
|
|
599
|
-
issuer: OAUTH_ISSUER
|
|
728
|
+
issuer: OAUTH_ISSUER, clientId, discoveryUrl: OAUTH_DISCOVERY_URL,
|
|
600
729
|
authorizationUrl: OAUTH_AUTHORIZATION_URL, tokenUrl: OAUTH_TOKEN_URL ?? undefined,
|
|
601
|
-
redirectUri:
|
|
730
|
+
redirectUri: oauthRedirectUri(parsed.flags), scopes: loginScopes(parsed.flags),
|
|
602
731
|
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
603
732
|
organization: requestedLoginOrganization(parsed.flags),
|
|
604
733
|
}, { signal: controller.signal, timeoutMs, authorize(url) {
|
|
@@ -609,56 +738,23 @@ async function startOAuthBrowserSession(parsed: Parsed, clientId: string, timeou
|
|
|
609
738
|
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
|
610
739
|
}
|
|
611
740
|
|
|
612
|
-
async function cmdConsoleLoginCheck(parsed: Parsed): Promise<void> {
|
|
613
|
-
const file = parsed.flags.get("console-check");
|
|
614
|
-
if (typeof file !== "string" || !file || parsed.flags.get("device") === true || parsed.flags.get("with-token") === true || explicitNonInteractive(parsed)) fail(2, "--console-check requires a downloaded JSON file and browser login. Use --no-browser to print the sign-in link.");
|
|
615
|
-
const baseUrl = resolveBaseUrl(parsed.flags);
|
|
616
|
-
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") as string : undefined) ?? process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
|
|
617
|
-
const op = WHOAMI && OPS.find((value) => value.resource === WHOAMI.resource && value.method === WHOAMI.method);
|
|
618
|
-
if (!baseUrl || !OAUTH_ISSUER || !clientId || OAUTH_LOGIN_METHOD !== "browser" || !op || op.auth !== "required" || !op.security?.length) fail(2, "Configure browser OAuth and a required-authentication identity read, then regenerate this CLI.");
|
|
619
|
-
const envOptions: Record<string, unknown> = {}, flagOptions: Record<string, unknown> = {};
|
|
620
|
-
for (const scalar of AUTH_SCALARS) {
|
|
621
|
-
if (process.env[scalar.env] !== undefined) envOptions[scalar.option] = process.env[scalar.env];
|
|
622
|
-
if (typeof parsed.flags.get(scalar.flag) === "string") flagOptions[scalar.option] = parsed.flags.get(scalar.flag);
|
|
623
|
-
}
|
|
624
|
-
if (BASIC && process.env[BASIC.envUser] && process.env[BASIC.envPass]) envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
|
|
625
|
-
if (BASIC && typeof parsed.flags.get("username") === "string" && typeof parsed.flags.get("password") === "string") flagOptions.basicAuth = { username: parsed.flags.get("username"), password: parsed.flags.get("password") };
|
|
626
|
-
const environment = (typeof parsed.flags.get("environment") === "string" ? parsed.flags.get("environment") as string : Object.entries(ENVIRONMENTS).find(([, url]) => new URL(url).href === new URL(baseUrl!).href)?.[0]) ?? null;
|
|
627
|
-
const result = await checkConsoleBrowserLogin({
|
|
628
|
-
file: file as string, configuration: {
|
|
629
|
-
baseUrl: baseUrl!, environment, operation: op!.resource + "." + op!.method, requirements: op!.security!,
|
|
630
|
-
request: { method: op!.graphql ? "POST" : op!.httpMethod, path: op!.graphql ? "" : op!.path, graphqlField: op!.graphql?.field ?? null, graphqlQuery: op!.graphql ? op!.graphql.docPrefix + op!.graphql.defaultSelection + " }" : null },
|
|
631
|
-
issuer: OAUTH_ISSUER!, clientId: clientId!, discoveryUrl: OAUTH_DISCOVERY_URL ?? null,
|
|
632
|
-
authorizationUrl: OAUTH_AUTHORIZATION_URL ?? null, tokenUrl: OAUTH_TOKEN_URL,
|
|
633
|
-
redirectUri: OAUTH_REDIRECT_URI ?? "http://127.0.0.1/callback", scopes: OAUTH_SCOPES,
|
|
634
|
-
audience: OAUTH_TOKEN_PARAMS.audience ?? null, resource: OAUTH_TOKEN_PARAMS.resource ?? null,
|
|
635
|
-
}, schemes: NAMED_SCHEMES,
|
|
636
|
-
credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ options: envOptions, named: environmentCredentials() }, { options: flagOptions, named: flagCredentials(parsed.flags) }]),
|
|
637
|
-
login: (timeoutMs) => startOAuthBrowserSession(parsed, clientId!, timeoutMs),
|
|
638
|
-
async verify(credentials, expectations) {
|
|
639
|
-
const kind = (value: "user" | "account" | "organization") => value === "user" ? "subject" : value;
|
|
640
|
-
const policy = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), entry.pointer])) as IdentityPolicy;
|
|
641
|
-
const expected = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), String(entry.expected)])) as ApiIdentity;
|
|
642
|
-
await verifyClientIdentity((options) => new TypeshipClient(options as unknown as ClientOptions), { baseUrl: baseUrl!, credentials }, op!, policy, expected);
|
|
643
|
-
},
|
|
644
|
-
progress: (message) => process.stderr.write(message + "\n"),
|
|
645
|
-
});
|
|
646
|
-
out(result);
|
|
647
|
-
await flushExit(0);
|
|
648
|
-
}
|
|
649
741
|
|
|
650
742
|
async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
651
743
|
if (parsed.help) {
|
|
744
|
+
const target = loginTarget(new Map());
|
|
652
745
|
const lines = [
|
|
653
746
|
BIN + " login — store credentials at " + credsPath(),
|
|
654
747
|
"",
|
|
655
748
|
...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
|
|
656
|
-
...(CLI_AUTH_URL ? [" " + BIN + " login approve in the browser: a key is minted for you (add --no-browser to print the link instead of opening it)"] : []),
|
|
657
|
-
" " + BIN + " login --with-token read the
|
|
658
|
-
...(
|
|
659
|
-
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --
|
|
749
|
+
...(CLI_AUTH_URL && target && !target.basic ? [" " + BIN + " login approve in the browser: a key is minted for you (add --no-browser to print the link instead of opening it)"] : []),
|
|
750
|
+
...(target ? [" " + BIN + " login --with-token read the " + target.label + " from stdin" + (target.basic ? " as one username:password line" : "") + " (CI)"] : []),
|
|
751
|
+
...(Object.keys(NAMED_SCHEMES).length > 1 || (!target && Object.keys(NAMED_SCHEMES).length) ? [" " + BIN + " login --scheme <name> choose the scheme --with-token, the prompt" + (CLI_AUTH_URL ? " and browser approval" : "") + " store" + (target ? " (default: " + target.storedAs + ")" : "") + "; a Basic scheme reads username:password"] : []),
|
|
752
|
+
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --client-id <id> OAuth " + (OAUTH_LOGIN_METHOD === "browser" ? "browser (authorization code + PKCE)" : "device") + " login" + (OAUTH_CLIENT_ID ? " (a default id is built in)" : "; register an application with the provider for its client ID, or set " + ENV_PREFIX + "_CLIENT_ID")] : []),
|
|
753
|
+
...(HAS_OAUTH_LOGIN && OAUTH_LOGIN_METHOD === "browser" ? [" " + BIN + " login --no-browser print the sign-in URL instead of opening a browser", " " + BIN + " login --redirect-port <n> listen on another loopback port (default callback " + oauthRedirectUri(new Map()) + "; register it with the provider)"] : []),
|
|
754
|
+
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --scopes <a,b> request these scopes instead of " + (OAUTH_SCOPES.length ? OAUTH_SCOPES.join(" ") : "the provider's defaults")] : []),
|
|
755
|
+
...(HAS_OAUTH_LOGIN && (OAUTH_DEVICE_URL || OAUTH_ISSUER || OAUTH_DISCOVERY_URL) ? [" " + BIN + " login --device use device authorization when your provider supports it"] : []),
|
|
660
756
|
...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
|
|
661
|
-
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
757
|
+
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN || !target ? [] : [" " + BIN + " login interactive prompt" + (target.basic ? " for username and hidden password" : "") + " (TTY only)"]),
|
|
662
758
|
"",
|
|
663
759
|
"Precedence per scheme: flags > env vars > stored credentials. Named inputs beat convenience flags within the same source.",
|
|
664
760
|
" " + BIN + " login --credentials @<JSON-file> store named credentials (use - for stdin)",
|
|
@@ -674,9 +770,8 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
674
770
|
}
|
|
675
771
|
if (parsed.flags.has("login-organization")) {
|
|
676
772
|
expectedLoginIdentity(parsed.flags);
|
|
677
|
-
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("
|
|
773
|
+
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("with-token") || explicitNonInteractive(parsed)) fail(2, "--login-organization is available only for interactive OAuth browser login, including --no-browser.");
|
|
678
774
|
}
|
|
679
|
-
if (parsed.flags.has("console-check")) { await cmdConsoleLoginCheck(parsed); return; }
|
|
680
775
|
const named = flagCredentials(parsed.flags);
|
|
681
776
|
const scalarValues: Record<string, string> = {};
|
|
682
777
|
for (const a of AUTH_SCALARS) {
|
|
@@ -698,9 +793,11 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
698
793
|
}
|
|
699
794
|
|
|
700
795
|
if (parsed.flags.get("with-token") === true) {
|
|
796
|
+
const target = loginTarget(parsed.flags);
|
|
797
|
+
if (!target) fail(2, "This API declares no credential the CLI can store. See '" + BIN + " login --help'.");
|
|
701
798
|
const token = (await readStdin()).trim();
|
|
702
799
|
if (!token) fail(2, "--with-token expects the credential on stdin.");
|
|
703
|
-
await storePastedToken(token, parsed.flags);
|
|
800
|
+
await storePastedToken(target!.basic ? basicFromText(token) : token, target!, parsed.flags);
|
|
704
801
|
}
|
|
705
802
|
|
|
706
803
|
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") as string : undefined)
|
|
@@ -717,8 +814,9 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
717
814
|
// Browser approval: the API mints a key for this CLI once a person
|
|
718
815
|
// approves in the browser. Works under an agent too (it prints the URL and
|
|
719
816
|
// polls); only the explicit non-interactive switch turns it off.
|
|
720
|
-
|
|
721
|
-
|
|
817
|
+
const target = loginTarget(parsed.flags);
|
|
818
|
+
if (CLI_AUTH_URL && target && !target.basic && !explicitNonInteractive(parsed)) {
|
|
819
|
+
await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, target, parsed.flags);
|
|
722
820
|
}
|
|
723
821
|
|
|
724
822
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
@@ -728,17 +826,24 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
728
826
|
message: "login needs a terminal to prompt, and there is none.",
|
|
729
827
|
nextSteps: [
|
|
730
828
|
...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
|
|
731
|
-
"
|
|
829
|
+
...(BASIC ? ["Pass Basic credentials: '" + BIN + " login --username <u> --password <p>', or set " + BASIC.envUser + " and " + BASIC.envPass + " in the environment."] : []),
|
|
830
|
+
...(target ? ["Pipe it: echo \"" + (target.basic ? "$USERNAME:$PASSWORD" : "$TOKEN") + "\" | " + BIN + " login --with-token" + (parsed.flags.has("scheme") ? " --scheme " + target.storedAs : "")] : []),
|
|
732
831
|
...(HAS_OAUTH_LOGIN ? ["OAuth login (or --device when supported by your provider): '" + BIN + " login --client-id <id>' (opens or prints the provider sign-in URL)."] : []),
|
|
733
832
|
...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
|
|
734
833
|
],
|
|
735
834
|
});
|
|
736
835
|
}
|
|
737
|
-
|
|
738
|
-
if (
|
|
739
|
-
|
|
836
|
+
if (!target) fail(2, "This API declares no credential the CLI can prompt for. See '" + BIN + " login --help'.");
|
|
837
|
+
if (target!.basic) {
|
|
838
|
+
process.stderr.write("Username: ");
|
|
839
|
+
const username = (await readLine()).trim();
|
|
840
|
+
const password = await promptHidden("Password (input hidden): ");
|
|
841
|
+
if (!username || !password) fail(2, "Enter both a username and a password.");
|
|
842
|
+
await storePastedToken({ username, password }, target!, parsed.flags);
|
|
843
|
+
}
|
|
844
|
+
const token = (await promptHidden("Paste " + target!.label + " (input hidden): ")).trim();
|
|
740
845
|
if (!token) fail(2, "Nothing entered.");
|
|
741
|
-
await storePastedToken(token, parsed.flags);
|
|
846
|
+
await storePastedToken(token, target!, parsed.flags);
|
|
742
847
|
}
|
|
743
848
|
|
|
744
849
|
async function cmdLogout(parsed: Parsed): Promise<void> {
|
|
@@ -753,8 +858,8 @@ async function cmdLogout(parsed: Parsed): Promise<void> {
|
|
|
753
858
|
// way out, so logging out ends the credential and not just the file. A
|
|
754
859
|
// pasted or CI key is someone else's to revoke, and is left alone.
|
|
755
860
|
let revoked: boolean | null = null;
|
|
756
|
-
|
|
757
|
-
const ownKey =
|
|
861
|
+
// The approval stored exactly one credential, as a scalar or a named scheme.
|
|
862
|
+
const ownKey = stored?.minted?.via === "browser" ? [...Object.values(stored.scalars ?? {}), ...Object.values(stored.named ?? {})].find((value): value is string => typeof value === "string") : undefined;
|
|
758
863
|
if (ownKey) {
|
|
759
864
|
try {
|
|
760
865
|
const response = await oauthStatusRequest(loginEndpoint(stored!.minted!.revocationUrl!), { method: "POST", headers: { Authorization: "Bearer " + ownKey } }, 15_000);
|
|
@@ -915,12 +1020,12 @@ function mcpEntryFor(url: string | undefined, readOnly = false): { entry: McpEnt
|
|
|
915
1020
|
const warnings: string[] = [];
|
|
916
1021
|
const hosted = url ?? MCP_URL ?? undefined;
|
|
917
1022
|
if (hosted) {
|
|
918
|
-
const envVar = AUTH_SCALARS[0]?.env;
|
|
919
1023
|
// The hosted endpoint serves its read-only twin at <url>/readonly; a
|
|
920
1024
|
// remote server of someone else's may not, so say so.
|
|
921
1025
|
const target = readOnly ? hosted.replace(/\/+$/, "") + "/readonly" : hosted;
|
|
922
|
-
if (readOnly && url !== undefined && !MCP_URL) warnings.push("--read-only appended /readonly to the URL
|
|
923
|
-
|
|
1026
|
+
if (readOnly && url !== undefined && !MCP_URL) warnings.push("--read-only appended /readonly to the URL; check that this server serves a read-only endpoint there.");
|
|
1027
|
+
if (HOSTED_MCP_NOTE) warnings.push(HOSTED_MCP_NOTE);
|
|
1028
|
+
return { entry: { url: target, ...(Object.keys(HOSTED_MCP_HEADERS).length ? { headers: { ...HOSTED_MCP_HEADERS } } : {}) }, warnings };
|
|
924
1029
|
}
|
|
925
1030
|
if (!HAS_MCP) {
|
|
926
1031
|
fail(2, "This package was generated without the MCP server target. Regenerate with it, or pass --url for a remote endpoint.");
|
|
@@ -992,7 +1097,7 @@ async function cmdMcp(parsed: Parsed): Promise<void> {
|
|
|
992
1097
|
|
|
993
1098
|
if (wanted.size === 0) {
|
|
994
1099
|
out({
|
|
995
|
-
server:
|
|
1100
|
+
server: MCP_SERVER_KEY,
|
|
996
1101
|
entry,
|
|
997
1102
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
998
1103
|
detected: MCP_CLIENTS.filter((c) => c.detect(cwd)).map((c) => c.id),
|
|
@@ -1003,11 +1108,11 @@ async function cmdMcp(parsed: Parsed): Promise<void> {
|
|
|
1003
1108
|
const results: McpWriteResult[] = [];
|
|
1004
1109
|
for (const id of wanted) {
|
|
1005
1110
|
const client = findMcpClient(id)!;
|
|
1006
|
-
results.push(writeMcpConfig(client, cwd,
|
|
1111
|
+
results.push(writeMcpConfig(client, cwd, MCP_SERVER_KEY, entry));
|
|
1007
1112
|
}
|
|
1008
1113
|
out({
|
|
1009
1114
|
ok: true,
|
|
1010
|
-
server:
|
|
1115
|
+
server: MCP_SERVER_KEY,
|
|
1011
1116
|
entry,
|
|
1012
1117
|
written: results.filter((r) => r.written).map((r) => r.file),
|
|
1013
1118
|
clients: results,
|
|
@@ -1029,10 +1134,11 @@ function agentContext(): AgentContext {
|
|
|
1029
1134
|
version: VERSION,
|
|
1030
1135
|
envPrefix: ENV_PREFIX,
|
|
1031
1136
|
authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? ["TYPESHIP_CREDENTIALS"] : [])],
|
|
1137
|
+
...(AUTH_UNDECLARED ? { authNotDeclared: true } : {}),
|
|
1032
1138
|
docsUrl: docsSiteUrl(),
|
|
1033
1139
|
docsIndexUrl: docsIndexUrl(),
|
|
1034
1140
|
generatedOperationCount: OPS.length,
|
|
1035
|
-
|
|
1141
|
+
omittedOperationCount: OMITTED_OPS.length,
|
|
1036
1142
|
mcpUrl: MCP_URL,
|
|
1037
1143
|
skillsRepo: SKILLS_REPO,
|
|
1038
1144
|
hasMcp: HAS_MCP,
|
|
@@ -1055,63 +1161,173 @@ function commandSummaries(): CommandSummary[] {
|
|
|
1055
1161
|
}
|
|
1056
1162
|
|
|
1057
1163
|
/**
|
|
1058
|
-
* help --json:
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
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.
|
|
1061
1170
|
*/
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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);
|
|
1068
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> {
|
|
1069
1206
|
return {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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"),
|
|
1076
1226
|
usage: BIN + " <resource> <command> [args] [--flags]",
|
|
1077
|
-
resources: [...byResource.entries()].map(([resource,
|
|
1227
|
+
resources: [...byResource.entries()].map(([resource, ops]) => ({
|
|
1078
1228
|
resource,
|
|
1079
|
-
commands:
|
|
1080
|
-
const op = OPS.find((o) => o.command[0] === resource && o.command[1] === c.command)!;
|
|
1081
|
-
return {
|
|
1082
|
-
command: c.command,
|
|
1083
|
-
method: c.method,
|
|
1084
|
-
path: c.path,
|
|
1085
|
-
...(c.summary ? { summary: c.summary } : {}),
|
|
1086
|
-
paginated: c.paginated,
|
|
1087
|
-
safety: op.safety,
|
|
1088
|
-
destructive: c.destructive,
|
|
1089
|
-
auth: c.auth,
|
|
1090
|
-
positional: op.params.filter((p) => p.kind === "path").map((p) => p.name),
|
|
1091
|
-
flags: c.flags,
|
|
1092
|
-
details_command: BIN + " docs " + resource + " " + c.command + " --json",
|
|
1093
|
-
};
|
|
1094
|
-
}),
|
|
1229
|
+
commands: ops.map((op) => helpEntry(op, summaries[OPS.indexOf(op)]!)),
|
|
1095
1230
|
})),
|
|
1096
|
-
...(
|
|
1097
|
-
|
|
1098
|
-
generated_operations: OPS.length,
|
|
1099
|
-
total_operations: OPS.length + EXCLUDED_OPS,
|
|
1100
|
-
omitted_operations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
|
|
1101
|
-
reason: "plan_limit",
|
|
1102
|
-
},
|
|
1103
|
-
} : {}),
|
|
1104
|
-
discovery: {
|
|
1105
|
-
search: BIN + " docs search <term> --json",
|
|
1106
|
-
operation: BIN + " docs <resource> <command> --json",
|
|
1107
|
-
note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
|
|
1108
|
-
},
|
|
1231
|
+
...helpCoverage(),
|
|
1232
|
+
discovery: helpDiscovery(),
|
|
1109
1233
|
builtins: BUILTIN_COMMANDS,
|
|
1110
|
-
global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--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>")],
|
|
1111
1235
|
auth_env_vars: agentContext().authEnvVars,
|
|
1112
1236
|
};
|
|
1113
1237
|
}
|
|
1114
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
|
+
|
|
1115
1331
|
async function cmdAgentGuide(parsed: Parsed): Promise<void> {
|
|
1116
1332
|
if (parsed.help) {
|
|
1117
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");
|
|
@@ -1139,24 +1355,26 @@ async function cmdAuth(parsed: Parsed): Promise<void> {
|
|
|
1139
1355
|
}
|
|
1140
1356
|
if (parsed.help || sub !== "check") {
|
|
1141
1357
|
process.stdout.write([
|
|
1142
|
-
BIN + " auth check [--
|
|
1358
|
+
BIN + " auth check [--offline] — report the credential the CLI would use and verify it with the API's identity read, as JSON: {status: ok|action_required, authenticated, verification: verified|unverified|rejected, source, ...}",
|
|
1143
1359
|
" " + BIN + " auth profiles list profiles without unlocking credentials",
|
|
1144
1360
|
" " + BIN + " auth use <name> select the default profile",
|
|
1145
1361
|
" " + BIN + " auth remove <name> remove a profile after logout",
|
|
1146
1362
|
" --profile <name> overrides TYPESHIP_PROFILE, then the saved selection, then default.",
|
|
1147
|
-
" --
|
|
1363
|
+
" --offline skip the identity read; the credential is then reported as unverified" + (WHOAMI ? "" : " (this API has no identity read, so credentials are always unverified)"),
|
|
1148
1364
|
"",
|
|
1149
1365
|
"Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
|
|
1150
1366
|
].join("\n") + "\n");
|
|
1151
1367
|
await flushExit(parsed.help ? 0 : 2);
|
|
1152
1368
|
}
|
|
1153
1369
|
const source = credentialSource(parsed.flags) ?? "none";
|
|
1154
|
-
const
|
|
1370
|
+
const present = source !== "none";
|
|
1155
1371
|
const savedIdentity = source === "login" ? readCreds() : null;
|
|
1156
1372
|
if (savedIdentity) assertStoredIdentity(savedIdentity, identityConfiguration());
|
|
1373
|
+
// A credential counts as authenticated only once the API accepted it.
|
|
1157
1374
|
const report: Record<string, unknown> = {
|
|
1158
|
-
status:
|
|
1159
|
-
authenticated,
|
|
1375
|
+
status: present ? "ok" : "action_required",
|
|
1376
|
+
authenticated: false,
|
|
1377
|
+
verification: present ? "unverified" : "none",
|
|
1160
1378
|
source,
|
|
1161
1379
|
credentials_path: existsSync(credsPath()) ? credsPath() : null,
|
|
1162
1380
|
credential_storage: credentialStore().backend,
|
|
@@ -1164,12 +1382,13 @@ async function cmdAuth(parsed: Parsed): Promise<void> {
|
|
|
1164
1382
|
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
1165
1383
|
auth_env_vars: agentContext().authEnvVars,
|
|
1166
1384
|
base_url: resolveBaseUrl(parsed.flags) ?? null,
|
|
1167
|
-
next_steps:
|
|
1385
|
+
next_steps: present ? [WHOAMI ? "Run '" + BIN + " auth check' without --offline to verify the credential." : "This API has no identity read, so the credential was not checked. Run a read command to confirm the API accepts it."] : [
|
|
1168
1386
|
...AUTH_SCALARS.map((a) => "Set " + a.env + " in the environment, or run '" + BIN + " login --" + a.flag + " <value>'."),
|
|
1169
|
-
|
|
1387
|
+
...(AUTH_UNDECLARED ? ["The API Spec does not declare authentication. Add another header with --header \"Name: value\" or TYPESHIP_HEADERS."] : []),
|
|
1388
|
+
...(WHOAMI ? ["Then run '" + BIN + " auth check'."] : []),
|
|
1170
1389
|
],
|
|
1171
1390
|
};
|
|
1172
|
-
if (
|
|
1391
|
+
if (present && parsed.flags.get("offline") !== true && WHOAMI) {
|
|
1173
1392
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1174
1393
|
if (op) {
|
|
1175
1394
|
const client = await makeClient(parsed.flags, op);
|
|
@@ -1178,9 +1397,12 @@ async function cmdAuth(parsed: Parsed): Promise<void> {
|
|
|
1178
1397
|
if (result.ok) {
|
|
1179
1398
|
if (identityConfiguration() && savedIdentity?.identity) assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
|
|
1180
1399
|
report.identity = result.data;
|
|
1400
|
+
// An identity read that also answers anonymous callers proves nothing.
|
|
1401
|
+
if (op.auth !== "none") { report.authenticated = true; report.verification = "verified"; report.next_steps = []; }
|
|
1181
1402
|
}
|
|
1182
1403
|
else {
|
|
1183
|
-
const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1404
|
+
const why = classifyApiError(result.error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1405
|
+
report.verification = "rejected";
|
|
1184
1406
|
report.status = "action_required";
|
|
1185
1407
|
report.live = { ok: false, code: why.code, message: why.message };
|
|
1186
1408
|
report.next_steps = why.nextSteps ?? [];
|
|
@@ -1209,7 +1431,13 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
|
|
|
1209
1431
|
if (baseUrl) {
|
|
1210
1432
|
try {
|
|
1211
1433
|
const response = await fetch(baseUrl, { method: "GET", signal: AbortSignal.timeout(8_000) });
|
|
1212
|
-
|
|
1434
|
+
void response.body?.cancel().catch(() => {});
|
|
1435
|
+
const reachable = response.status >= 200 && response.status < 300;
|
|
1436
|
+
checks.push({ name: "base_url", ok: reachable, detail: baseUrl + " → HTTP " + response.status, ...(reachable ? {} : { fix: response.status === 404 || response.status === 405
|
|
1437
|
+
? "The API does not answer at this base URL. Check --base-url / " + ENV_PREFIX + "_BASE_URL, or run '" + BIN + " upgrade' if this CLI is older than the API."
|
|
1438
|
+
: response.status === 401 || response.status === 403
|
|
1439
|
+
? "The API answered without a credential with HTTP " + response.status + "; the identity check below tests the credential."
|
|
1440
|
+
: "The API answered HTTP " + response.status + ". Check the base URL and the API's status." }) });
|
|
1213
1441
|
} catch (e) {
|
|
1214
1442
|
checks.push({ name: "base_url", ok: false, detail: baseUrl + ": " + (e as Error).message, fix: "Check the network, or set --base-url / " + ENV_PREFIX + "_BASE_URL." });
|
|
1215
1443
|
}
|
|
@@ -1223,7 +1451,13 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
|
|
|
1223
1451
|
const client = await makeClient(parsed.flags, op);
|
|
1224
1452
|
const target = (client as unknown as Record<string, Record<string, () => Promise<{ ok: boolean; error?: unknown }>>>)[op.resource]!;
|
|
1225
1453
|
const result = await asApiResult(target[op.method]!());
|
|
1226
|
-
|
|
1454
|
+
const status = (result.error as { status?: unknown } | undefined)?.status;
|
|
1455
|
+
checks.push(result.ok ? { name: "identity", ok: true, detail: op.command.join(" ") + " ok" } : { name: "identity", ok: false, detail: classifyApiError(result.error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential: true, docsUrl: DOCS_URL_DEFAULT }).message,
|
|
1456
|
+
// Only 401 and 403 are about the credential. A 404 or 405 means the
|
|
1457
|
+
// API no longer has this endpoint where this CLI version expects it.
|
|
1458
|
+
fix: status === 401 || status === 403 ? "The credential was rejected; run '" + BIN + " login' with a current one."
|
|
1459
|
+
: status === 404 || status === 405 ? "The API does not know " + wireOf(op) + ", which this CLI version calls. Run '" + BIN + " upgrade', and check the base URL."
|
|
1460
|
+
: "The identity read failed; the detail says why." });
|
|
1227
1461
|
} catch (e) {
|
|
1228
1462
|
checks.push({ name: "identity", ok: false, detail: (e as Error).message });
|
|
1229
1463
|
}
|
|
@@ -1236,7 +1470,7 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
|
|
|
1236
1470
|
}
|
|
1237
1471
|
if (HAS_MCP || MCP_URL) {
|
|
1238
1472
|
const cwd = process.cwd();
|
|
1239
|
-
const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd,
|
|
1473
|
+
const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd, MCP_SERVER_KEY)).map((c) => c.id);
|
|
1240
1474
|
const detected = MCP_CLIENTS.filter((c) => c.detect(cwd) && !c.incompatible).map((c) => c.id);
|
|
1241
1475
|
checks.push({ name: "mcp_clients", ok: detected.length === 0 || configured.length > 0, detail: "detected: " + (detected.join(", ") || "none") + "; configured: " + (configured.join(", ") || "none"), ...(detected.length > 0 && configured.length === 0 ? { fix: "Run '" + BIN + " mcp install --all'." } : {}) });
|
|
1242
1476
|
}
|
|
@@ -1276,13 +1510,18 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1276
1510
|
const first = AUTH_SCALARS[0];
|
|
1277
1511
|
const named = flagCredentials(parsed.flags), envNamed = environmentCredentials();
|
|
1278
1512
|
const stored: StoredCreds = Object.keys(named).length || Object.keys(envNamed).length || first && process.env[first.env] ? {} : readCreds() ?? {};
|
|
1279
|
-
|
|
1280
|
-
|
|
1513
|
+
// -k stores the credential login would store; --<flag> stores that scalar.
|
|
1514
|
+
const target = loginTarget(parsed.flags);
|
|
1515
|
+
const keyValue = typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") as string : undefined;
|
|
1516
|
+
const flagValue = first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) as string : undefined;
|
|
1281
1517
|
if (Object.keys(named).length) {
|
|
1282
1518
|
await saveLoginCredentials({ named }, parsed.flags);
|
|
1283
1519
|
report.credential = { status: "stored", path: credsPath() };
|
|
1284
|
-
} else if (
|
|
1285
|
-
await saveLoginCredentials(
|
|
1520
|
+
} else if (keyValue && target && !target.basic) {
|
|
1521
|
+
await saveLoginCredentials(target.save(keyValue), parsed.flags);
|
|
1522
|
+
report.credential = { status: "stored", path: credsPath() };
|
|
1523
|
+
} else if ((keyValue ?? flagValue) && first) {
|
|
1524
|
+
await saveLoginCredentials({ scalars: { [first.option]: (keyValue ?? flagValue)! } }, parsed.flags);
|
|
1286
1525
|
report.credential = { status: "stored", path: credsPath() };
|
|
1287
1526
|
} else if (first && process.env[first.env]) {
|
|
1288
1527
|
report.credential = { status: "env", variable: first.env };
|
|
@@ -1293,13 +1532,13 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1293
1532
|
} else if (first && HAS_OAUTH_LOGIN && OAUTH_LOGIN_METHOD === "browser" && (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID) && !explicitNonInteractive(parsed)) {
|
|
1294
1533
|
await acquireOAuthBrowserSession(parsed, (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID)!);
|
|
1295
1534
|
report.credential = { status: "stored", method: "oauth_browser", path: credsPath() };
|
|
1296
|
-
} else if (
|
|
1535
|
+
} else if (target && !target.basic && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
|
|
1297
1536
|
// Nothing anywhere: approve a credential in the browser, as `login`
|
|
1298
1537
|
// would, then carry on. Under an agent the URL is printed for the person
|
|
1299
1538
|
// and polled; only the explicit non-interactive switch skips this.
|
|
1300
1539
|
await credentialStore().prepare();
|
|
1301
1540
|
const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
1302
|
-
await storeMinted(minted, parsed.flags);
|
|
1541
|
+
await storeMinted(minted, target, parsed.flags);
|
|
1303
1542
|
report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
|
|
1304
1543
|
} else {
|
|
1305
1544
|
report.credential = { status: "none" };
|
|
@@ -1317,7 +1556,7 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1317
1556
|
} else {
|
|
1318
1557
|
const { entry, warnings } = mcpEntryFor(undefined);
|
|
1319
1558
|
const clients = MCP_CLIENTS.filter((c) => !c.incompatible && c.detect(cwd) && !(c.id === "claude-desktop" && entry.url));
|
|
1320
|
-
const results = clients.map((c) => writeMcpConfig(c, cwd,
|
|
1559
|
+
const results = clients.map((c) => writeMcpConfig(c, cwd, MCP_SERVER_KEY, entry));
|
|
1321
1560
|
report.mcp = { status: results.length > 0 ? "written" : "no-clients", entry, clients: results, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
1322
1561
|
if (results.length === 0) nextSteps.push("No MCP client was found on this machine; run '" + BIN + " mcp' to print the entry.");
|
|
1323
1562
|
}
|
|
@@ -1331,7 +1570,7 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1331
1570
|
report.agents_md = { status: result.updated ? "updated" : "written", file: result.file };
|
|
1332
1571
|
}
|
|
1333
1572
|
|
|
1334
|
-
nextSteps.push("Run '" + BIN + " auth check
|
|
1573
|
+
nextSteps.push("Run '" + BIN + " auth check'" + (WHOAMI ? "" : " (or any read command)") + " to confirm the connection.");
|
|
1335
1574
|
nextSteps.push("Run '" + BIN + " agent-guide' for the conventions, or '" + BIN + " --help' for commands.");
|
|
1336
1575
|
out({ ...report, next_steps: nextSteps });
|
|
1337
1576
|
await flushExit(0);
|
|
@@ -1357,6 +1596,7 @@ function registryBase(): string {
|
|
|
1357
1596
|
}
|
|
1358
1597
|
|
|
1359
1598
|
async function latestVersion(timeoutMs: number): Promise<string | null> {
|
|
1599
|
+
if (!PKG_CONFIRMED) return null;
|
|
1360
1600
|
try {
|
|
1361
1601
|
const response = await fetch(registryBase() + "/" + PKG_NAME, {
|
|
1362
1602
|
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
@@ -1383,6 +1623,9 @@ async function cmdUpgrade(parsed: Parsed): Promise<void> {
|
|
|
1383
1623
|
process.stdout.write(lines.join("\n") + "\n");
|
|
1384
1624
|
await flushExit(0);
|
|
1385
1625
|
}
|
|
1626
|
+
if (!PKG_CONFIRMED) {
|
|
1627
|
+
fail(1, "This build's package name (" + PKG_NAME + ") was not confirmed when it was generated, so upgrade does not look it up on npm, where that name may belong to another package. This package updates by regeneration from its API spec; get the latest from the API provider.");
|
|
1628
|
+
}
|
|
1386
1629
|
const latest = await latestVersion(5000);
|
|
1387
1630
|
if (latest === null) {
|
|
1388
1631
|
fail(1, PKG_NAME + " is not on the registry (" + registryBase() + "). This package updates by regeneration from its API spec; get the latest from the API provider.");
|
|
@@ -1434,7 +1677,7 @@ function completionFlagsFor(op: OpSpec): { flags: string[]; values: Record<strin
|
|
|
1434
1677
|
return { flags, values };
|
|
1435
1678
|
}
|
|
1436
1679
|
|
|
1437
|
-
const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--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)];
|
|
1438
1681
|
const BUILTIN_WORDS: Record<string, string[]> = {
|
|
1439
1682
|
config: ["list", "get", "set", "unset", "path"],
|
|
1440
1683
|
completion: ["bash", "zsh", "fish"],
|
|
@@ -1574,34 +1817,6 @@ async function fetchDocs(pathOrFile: string): Promise<string | null> {
|
|
|
1574
1817
|
}
|
|
1575
1818
|
}
|
|
1576
1819
|
|
|
1577
|
-
function searchTerms(query: string): string[] {
|
|
1578
|
-
return [...new Set(query.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 2))];
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
/** Token search across names, prose, paths, and arguments. A phrase such as
|
|
1582
|
-
* "create project" should find the projects create command, even though that exact
|
|
1583
|
-
* substring never occurs in the generated command. */
|
|
1584
|
-
function referenceSearchScore(op: OpSpec, query: string): number {
|
|
1585
|
-
const terms = searchTerms(query);
|
|
1586
|
-
if (terms.length === 0) return 0;
|
|
1587
|
-
const names = [...op.command, op.tool].join("_").toLowerCase().split(/[^a-z0-9]+/);
|
|
1588
|
-
const summary = (op.summary ?? "").toLowerCase();
|
|
1589
|
-
const description = (op.description ?? "").toLowerCase();
|
|
1590
|
-
const path = op.path.toLowerCase();
|
|
1591
|
-
const params = op.params.flatMap((p) => [p.name.toLowerCase(), p.flag.toLowerCase()]);
|
|
1592
|
-
let score = 0;
|
|
1593
|
-
for (const term of terms) {
|
|
1594
|
-
if (names.includes(term)) score += 10;
|
|
1595
|
-
if (summary.split(/[^a-z0-9]+/).includes(term)) score += 5;
|
|
1596
|
-
else if (summary.includes(term)) score += 3;
|
|
1597
|
-
if (path.includes(term)) score += 3;
|
|
1598
|
-
if (params.includes(term)) score += 3;
|
|
1599
|
-
else if (params.some((param) => param.includes(term))) score += 1;
|
|
1600
|
-
if (description.includes(term)) score += 1;
|
|
1601
|
-
}
|
|
1602
|
-
return score;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
1820
|
function referenceFor(op: OpSpec, includeSchemas = false): string {
|
|
1606
1821
|
const lines: string[] = [];
|
|
1607
1822
|
lines.push(paintOut("bold", usageLine(op)));
|
|
@@ -1620,7 +1835,9 @@ function referenceFor(op: OpSpec, includeSchemas = false): string {
|
|
|
1620
1835
|
lines.push("", paintOut("bold", label + ":"));
|
|
1621
1836
|
for (const p of params) {
|
|
1622
1837
|
const name = p.kind === "path" ? "<" + p.name + ">" : "--" + p.flag;
|
|
1623
|
-
|
|
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)") : ""));
|
|
1624
1841
|
const values = p.type === "array" ? p.items?.enum : p.enum;
|
|
1625
1842
|
if (values && values.join("|").length > 24) lines.push(" one of: " + values.join(", "));
|
|
1626
1843
|
if (p.description) {
|
|
@@ -1639,11 +1856,34 @@ function referenceFor(op: OpSpec, includeSchemas = false): string {
|
|
|
1639
1856
|
lines.push("", paintOut("bold", "Wire arguments:"), JSON.stringify(op.exampleArguments, null, 2));
|
|
1640
1857
|
if (op.outputSchema) lines.push("", paintOut("bold", "Output schema:"), JSON.stringify(op.outputSchema, null, 2));
|
|
1641
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
|
+
}
|
|
1642
1865
|
lines.push("", "Add --schema for the complete input/output schemas, or --json for the machine contract.");
|
|
1643
1866
|
}
|
|
1644
1867
|
return lines.join("\n");
|
|
1645
1868
|
}
|
|
1646
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
|
+
|
|
1647
1887
|
/** Opens a browser for a person; under an agent it prints the URL instead of opening anything. */
|
|
1648
1888
|
function openInBrowser(url: string, parsed?: Parsed): { opened: boolean; url: string } {
|
|
1649
1889
|
if (parsed && nonInteractive(parsed)) {
|
|
@@ -1663,11 +1903,12 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1663
1903
|
"",
|
|
1664
1904
|
" " + BIN + " docs overview",
|
|
1665
1905
|
" " + BIN + " docs <resource> <command> operation contract and example",
|
|
1906
|
+
" --path <a.b> one nested argument's type and fields",
|
|
1666
1907
|
" --schema include input/output JSON Schema",
|
|
1667
1908
|
" --json print the machine contract as JSON",
|
|
1668
1909
|
" " + BIN + " docs search <term> search reference and guides",
|
|
1669
1910
|
" --json / --format json print structured matches and availability",
|
|
1670
|
-
" " + BIN + " docs read <page> print a docs-site page
|
|
1911
|
+
" " + BIN + " docs read <page> print a named input type or a docs-site page",
|
|
1671
1912
|
" " + BIN + " docs --web open the docs site in a browser",
|
|
1672
1913
|
"",
|
|
1673
1914
|
"Guides come from the docs site's llms.txt (set with '" + BIN + " config set docs-url <url>').",
|
|
@@ -1690,23 +1931,22 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1690
1931
|
const term = parsed.positionals.slice(2).join(" ");
|
|
1691
1932
|
if (!term) fail(2, "docs search expects a term");
|
|
1692
1933
|
const jsonOutput = parsed.flags.get("json") === true || parsed.flags.get("format") === "json";
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
.sort((a, b) => b.score - a.score || a.op.command.join(" ").localeCompare(b.op.command.join(" ")))
|
|
1696
|
-
.map((match) => match.op);
|
|
1934
|
+
// The MCP server's search_docs ranking, so both surfaces agree.
|
|
1935
|
+
const refMatches = rankOperations(OPS, term).map((match) => match.op);
|
|
1697
1936
|
const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
|
|
1698
1937
|
if (jsonOutput) {
|
|
1699
1938
|
out({
|
|
1700
1939
|
schema_version: "1",
|
|
1701
1940
|
query: term,
|
|
1702
|
-
reference: refMatches.slice(0,
|
|
1941
|
+
reference: refMatches.slice(0, SEARCH_PAGE_SIZE).map((op) => ({
|
|
1703
1942
|
command: op.command.join(" "),
|
|
1704
1943
|
method: op.httpMethod,
|
|
1705
1944
|
path: op.path,
|
|
1706
1945
|
...(op.summary ? { summary: op.summary } : {}),
|
|
1946
|
+
...(op.deprecated ? { deprecated: true } : {}),
|
|
1707
1947
|
details_command: BIN + " docs " + op.command.join(" ") + " --json",
|
|
1708
1948
|
})),
|
|
1709
|
-
guides: proseMatches.slice(0,
|
|
1949
|
+
guides: proseMatches.slice(0, SEARCH_PAGE_SIZE).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
|
|
1710
1950
|
totals: { reference: refMatches.length, guides: proseMatches.length },
|
|
1711
1951
|
guides_status: docsStatus,
|
|
1712
1952
|
...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
|
|
@@ -1717,11 +1957,11 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1717
1957
|
const lines: string[] = [];
|
|
1718
1958
|
if (refMatches.length > 0) {
|
|
1719
1959
|
lines.push(paintOut("bold", "Reference:"));
|
|
1720
|
-
for (const op of refMatches.slice(0,
|
|
1960
|
+
for (const op of refMatches.slice(0, SEARCH_PAGE_SIZE)) lines.push(" " + padPaint("cyan", op.command.join(" "), 34) + (op.deprecated ? "(deprecated) " : "") + (op.summary ?? wireOf(op)));
|
|
1721
1961
|
}
|
|
1722
1962
|
if (proseMatches.length > 0) {
|
|
1723
1963
|
lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
|
|
1724
|
-
for (const match of proseMatches.slice(0,
|
|
1964
|
+
for (const match of proseMatches.slice(0, SEARCH_PAGE_SIZE)) lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
|
|
1725
1965
|
} else if (docsStatus === "not_configured") {
|
|
1726
1966
|
lines.push(...(lines.length > 0 ? [""] : []), "(no docs site configured for guide search: '" + BIN + " config set docs-url <url>')");
|
|
1727
1967
|
} else if (docsStatus === "unavailable") {
|
|
@@ -1735,6 +1975,13 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1735
1975
|
if (sub === "read") {
|
|
1736
1976
|
const page = parsed.positionals[2];
|
|
1737
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
|
+
}
|
|
1738
1985
|
let target = page!;
|
|
1739
1986
|
if (!/^https?:\/\//.test(target)) {
|
|
1740
1987
|
const index = await fetchDocs("llms.txt");
|
|
@@ -1773,12 +2020,14 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1773
2020
|
}, null, 2) + "\n");
|
|
1774
2021
|
await flushExit(0);
|
|
1775
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);
|
|
1776
2025
|
process.stdout.write(referenceFor(op!, parsed.flags.get("schema") === true) + "\n");
|
|
1777
2026
|
await flushExit(0);
|
|
1778
2027
|
}
|
|
1779
2028
|
|
|
1780
2029
|
const lines: string[] = [];
|
|
1781
|
-
lines.push(paintOut("bold", "
|
|
2030
|
+
lines.push(paintOut("bold", "Typeship") + " (v" + API_VERSION + ")");
|
|
1782
2031
|
if (API_DESCRIPTION) lines.push("", API_DESCRIPTION.trim());
|
|
1783
2032
|
lines.push("", paintOut("bold", "Reference:") + " " + BIN + " docs <resource> <command>");
|
|
1784
2033
|
const byResource = new Map<string, number>();
|
|
@@ -1834,7 +2083,7 @@ function shellQuote(value: string): string {
|
|
|
1834
2083
|
}
|
|
1835
2084
|
|
|
1836
2085
|
function usageLine(op: OpSpec): string {
|
|
1837
|
-
const paths = op.params.filter((p) => p.kind === "path").map((p) => "<" + p.name + ">").join(" ");
|
|
2086
|
+
const paths = op.params.filter((p) => p.kind === "path").map((p) => p.credential ? "[<" + p.name + ">]" : "<" + p.name + ">").join(" ");
|
|
1838
2087
|
return BIN + " " + op.command[0] + " " + op.command[1] + (paths ? " " + paths : "");
|
|
1839
2088
|
}
|
|
1840
2089
|
|
|
@@ -1882,7 +2131,7 @@ function helpSentence(description: string | undefined): string {
|
|
|
1882
2131
|
/** Type column text for a param: string, number, string[], a|b|c, enum, object, json, path. */
|
|
1883
2132
|
function typeLabel(p: ParamSpec): string {
|
|
1884
2133
|
if (p.nullable) return typeLabel({ ...p, nullable: false }) + "|null";
|
|
1885
|
-
if (p.type === "file") return "path (uploaded)";
|
|
2134
|
+
if (p.type === "file") return p.multiple ? "paths (uploaded, repeatable)" : "path (uploaded)";
|
|
1886
2135
|
if (p.format && p.type === "string") return p.format;
|
|
1887
2136
|
const inlineEnum = (values: string[] | undefined) => values && values.join("|").length <= 24 ? values.join("|") : undefined;
|
|
1888
2137
|
if (p.type === "array") {
|
|
@@ -1957,7 +2206,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
|
|
|
1957
2206
|
}
|
|
1958
2207
|
const width = termWidth();
|
|
1959
2208
|
const lines: string[] = [];
|
|
1960
|
-
lines.push(paintOut("bold", BIN) + ": " + "
|
|
2209
|
+
lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.24.0");
|
|
1961
2210
|
lines.push("");
|
|
1962
2211
|
lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
|
|
1963
2212
|
lines.push("");
|
|
@@ -1976,18 +2225,18 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
|
|
|
1976
2225
|
}
|
|
1977
2226
|
if (EXCLUDED_OPS > 0) {
|
|
1978
2227
|
lines.push("");
|
|
1979
|
-
lines.push(...labeled(paintOut("yellow", "
|
|
1980
|
-
lines.push(...labeled("Omitted: ", OMITTED_OPS.map((op) => op.command.join(" ") + " (" + op.httpMethod + " " + op.path + ")").join(", "), width, 14));
|
|
1981
|
-
lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
|
|
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));
|
|
1982
2229
|
}
|
|
1983
2230
|
lines.push("");
|
|
1984
|
-
const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check parameters and JSON bodies)" +
|
|
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)" +
|
|
1985
2232
|
(AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
|
|
1986
2233
|
lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
|
|
1987
2234
|
lines.push(...labeled("Credential env vars: ", [
|
|
1988
2235
|
"TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
|
|
1989
2236
|
...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
|
|
1990
2237
|
].join(", ") || "none", width, 21));
|
|
2238
|
+
if (AUTH_UNDECLARED) lines.push(...labeled("Auth: ", "not declared by the API Spec; " + AUTH_SCALARS[0]!.env + " is sent as Authorization: Bearer when set", width, 6));
|
|
2239
|
+
lines.push(...labeled("Extra headers: ", "--header \"Name: value\" (repeatable) or TYPESHIP_HEADERS", width, 15));
|
|
1991
2240
|
lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
|
|
1992
2241
|
lines.push(...labeled("Sign-in: ", BIN + " login | logout | whoami | auth check (stored at " + credsPath() + ")", width, 9));
|
|
1993
2242
|
lines.push(...labeled("Setup: ", BIN + " init (connect this machine)" + " | " + BIN + " config (defaults)" + (HAS_MCP || MCP_URL ? " | " + BIN + " mcp install --all (agent clients)" : "") + " | " + BIN + " doctor | " + BIN + " upgrade | " + BIN + " completion <shell>", width, 7));
|
|
@@ -2030,12 +2279,15 @@ function commandExtras(op: OpSpec): [string, string][] {
|
|
|
2030
2279
|
const extras: [string, string][] = [];
|
|
2031
2280
|
if (op.auth !== "none" && Object.keys(NAMED_SCHEMES).length) extras.push(["--credentials @<file>|-", "named credentials as JSON; use - for stdin"]);
|
|
2032
2281
|
if (op.hasBody && op.bodyKind === "binary") extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
|
|
2282
|
+
if (op.rawResponse) extras.push(["--output <file>", "write the " + (op.rawResponse === "binary" ? "binary " : "") + "response body to a file (- for stdout) and print what was written"]);
|
|
2033
2283
|
else if (op.hasBody) extras.push(["--data '<json>'", "raw JSON body" + (op.bodyStyle === "fields" ? " (merged under field flags)" : "") + "; @<file> reads a file, - reads stdin"]);
|
|
2034
2284
|
if (op.select) extras.push(["--select '<selection>'", "GraphQL selection set replacing the default, e.g. '{ id name }'"]);
|
|
2035
2285
|
if (op.paginated) extras.push(["--all", "stream every item from every page (NDJSON)"]);
|
|
2036
2286
|
const collectionField = collectionProperty(op.outputSchema);
|
|
2037
2287
|
extras.push(["--fields <a,b.c>", "keep only these fields of the result" + (op.paginated ? " (per item)" : collectionField ? " (per item in " + collectionField + ")" : "")]);
|
|
2038
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"]);
|
|
2039
2291
|
if (op.safety === "destructive") extras.push(["--force, -y", "destructive: required without a terminal, skips the prompt with one"]);
|
|
2040
2292
|
return extras;
|
|
2041
2293
|
}
|
|
@@ -2044,6 +2296,7 @@ function commandExtras(op: OpSpec): [string, string][] {
|
|
|
2044
2296
|
function exampleLine(op: OpSpec): string {
|
|
2045
2297
|
const parts = [BIN, op.command[0], op.command[1]];
|
|
2046
2298
|
for (const p of op.params) {
|
|
2299
|
+
if (p.credential) continue; // defaulted from the configured credential
|
|
2047
2300
|
const hasExample = p.type !== "file" && Object.hasOwn(op.exampleArguments, p.name);
|
|
2048
2301
|
if (!p.required && !hasExample) continue;
|
|
2049
2302
|
const generated = p.type === "file" ? undefined : op.exampleArguments[p.name];
|
|
@@ -2074,6 +2327,7 @@ function exampleLine(op: OpSpec): string {
|
|
|
2074
2327
|
|
|
2075
2328
|
/** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
|
|
2076
2329
|
function authNote(op: OpSpec): string {
|
|
2330
|
+
if (AUTH_UNDECLARED) return op.auth === "none" ? " (no auth needed)" : " (auth not declared)";
|
|
2077
2331
|
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
|
|
2078
2332
|
return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
|
|
2079
2333
|
}
|
|
@@ -2083,6 +2337,8 @@ function printOp(op: OpSpec): void {
|
|
|
2083
2337
|
lines.push(paintOut("bold", usageLine(op)));
|
|
2084
2338
|
if (op.summary) lines.push(helpSentence(op.summary));
|
|
2085
2339
|
lines.push(wireOf(op) + authNote(op));
|
|
2340
|
+
const scopes = requiredScopes(op.security);
|
|
2341
|
+
if (scopes.length) lines.push("Requires OAuth scopes: " + scopes.join(", ") + (HAS_OAUTH_LOGIN ? " (login --scopes " + scopes.join(",") + ")" : ""));
|
|
2086
2342
|
lines.push("");
|
|
2087
2343
|
const rows = op.params.filter((p) => p.kind !== "path");
|
|
2088
2344
|
const extras = commandExtras(op);
|
|
@@ -2135,7 +2391,7 @@ function readStdinBytes(): Promise<Uint8Array<ArrayBuffer>> {
|
|
|
2135
2391
|
/** A local file as an upload part; the SDK's multipart encoder takes Blobs. */
|
|
2136
2392
|
function fileFromPath(flag: string, path: string): File {
|
|
2137
2393
|
try {
|
|
2138
|
-
return new File([readFileSync(path)], basename(path));
|
|
2394
|
+
return new File([readFileSync(path)], basename(path), { type: mediaTypeForPath(path) });
|
|
2139
2395
|
} catch (e) {
|
|
2140
2396
|
return fail(2, "--" + flag + ": cannot read " + path + " (" + (e as Error).message + ")");
|
|
2141
2397
|
}
|
|
@@ -2182,6 +2438,7 @@ function coerce(spec: ParamSpec, raw: string | boolean, repeated?: string[]): un
|
|
|
2182
2438
|
if (spec.nullable && raw === "null" && repeated === undefined) return null;
|
|
2183
2439
|
if (spec.type === "file") {
|
|
2184
2440
|
if (raw === true) fail(2, "--" + spec.flag + " expects a file path");
|
|
2441
|
+
if (spec.multiple) return (repeated ?? [String(raw)]).map((path) => fileFromPath(spec.flag, path));
|
|
2185
2442
|
return fileFromPath(spec.flag, String(raw));
|
|
2186
2443
|
}
|
|
2187
2444
|
if (spec.type === "boolean") {
|
|
@@ -2252,18 +2509,83 @@ function validateParameters(op: OpSpec, values: Record<string, unknown>, flags:
|
|
|
2252
2509
|
|
|
2253
2510
|
/** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
|
|
2254
2511
|
let LAST_CLIENT_HAD_CREDENTIAL = false;
|
|
2512
|
+
/** The last client's Basic credentials, for path arguments that default to the username. */
|
|
2513
|
+
let LAST_CLIENT_BASIC: { basicAuth?: unknown; credentials?: unknown } = {};
|
|
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
|
+
|
|
2574
|
+
/** The configured Basic-auth username: the named scheme's, else basicAuth's. */
|
|
2575
|
+
function credentialUsername(scheme: string): string | undefined {
|
|
2576
|
+
const named = (LAST_CLIENT_BASIC.credentials as Record<string, unknown> | undefined)?.[scheme];
|
|
2577
|
+
const username = named && typeof named === "object" ? (named as { username?: unknown }).username
|
|
2578
|
+
: (LAST_CLIENT_BASIC.basicAuth as { username?: unknown } | undefined)?.username;
|
|
2579
|
+
return typeof username === "string" && username !== "" ? username : undefined;
|
|
2580
|
+
}
|
|
2255
2581
|
|
|
2256
2582
|
/** Check the same complete alternatives the request runtime can select, after
|
|
2257
2583
|
* per-scheme flag, environment, profile, and OAuth resolution. */
|
|
2258
2584
|
function requireOperationCredentials(op: OpSpec, options: ClientOptions & Record<string, unknown>): void {
|
|
2259
2585
|
if (op.auth !== "required") return;
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
const
|
|
2263
|
-
const available = namedCredentialAvailability(NAMED_SCHEMES, new Set(Object.keys(options).filter((key) => supplied(options[key]))), named);
|
|
2264
|
-
if (op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => available.has(option)))) return;
|
|
2265
|
-
const alternatives = (op.credentialOptions ?? []).filter((alternative) => alternative.length > 0 && alternative.every((option) => option.startsWith("credentials.")));
|
|
2266
|
-
const missing = alternatives.map((alternative) => alternative.filter((option) => !available.has(option)).map((option) => option.slice("credentials.".length)));
|
|
2586
|
+
const gap = missingCredentials(NAMED_SCHEMES, op.credentialOptions, options);
|
|
2587
|
+
if (!gap) return;
|
|
2588
|
+
const { alternatives, missing } = gap;
|
|
2267
2589
|
const names = new Set(missing.flat());
|
|
2268
2590
|
const relevant = AUTH_SCALARS.filter((scalar) => [...names].some((name) => NAMED_SCHEMES[name]?.options.includes(scalar.option)));
|
|
2269
2591
|
const needsBasic = [...names].some((name) => NAMED_SCHEMES[name]?.options.includes("basicAuth"));
|
|
@@ -2276,7 +2598,8 @@ function requireOperationCredentials(op: OpSpec, options: ClientOptions & Record
|
|
|
2276
2598
|
nextSteps: alternatives.length ? [
|
|
2277
2599
|
...relevant.map((a) => "Set " + a.env + " in the environment, pass --" + a.flag + " <value>, or run '" + BIN + " login'."),
|
|
2278
2600
|
...(needsBasic && BASIC ? ["Set " + BASIC.envUser + " and " + BASIC.envPass + ", or pass --username and --password."] : []),
|
|
2279
|
-
|
|
2601
|
+
...(HAS_OAUTH_LOGIN && [...names].some((name) => OAUTH_SESSION_SCHEMES.includes(name)) ? ["Sign in with OAuth: '" + BIN + " login'."] : []),
|
|
2602
|
+
"Supply all schemes in one alternative through " + "TYPESHIP_CREDENTIALS" + " or --credentials @<JSON-file>: " + alternatives.map((alternative) => alternative.join(" + ")).join(" OR ") + ".",
|
|
2280
2603
|
] : ["Check the operation's security schemes in the API Spec and regenerate with a supported, compatible alternative."],
|
|
2281
2604
|
});
|
|
2282
2605
|
}
|
|
@@ -2307,6 +2630,23 @@ function environmentCredentials(): NamedCredentials {
|
|
|
2307
2630
|
return value === undefined ? {} : parseNamedCredentials(value, NAMED_SCHEMES);
|
|
2308
2631
|
}
|
|
2309
2632
|
|
|
2633
|
+
/** --timeout <seconds> or TYPESHIP_TIMEOUT: the per-attempt deadline
|
|
2634
|
+
* (default 60 seconds) for slow operations. */
|
|
2635
|
+
function requestTimeoutMs(flags: Map<string, string | boolean>): number | undefined {
|
|
2636
|
+
const raw = typeof flags.get("timeout") === "string" ? flags.get("timeout") as string : process.env["TYPESHIP_TIMEOUT"];
|
|
2637
|
+
if (raw === undefined) return undefined;
|
|
2638
|
+
const seconds = Number(raw);
|
|
2639
|
+
if (!/^\d+(\.\d+)?$/.test(raw.trim()) || !Number.isFinite(seconds) || seconds <= 0 || seconds > 3600) fail(2, "--timeout expects seconds between 0 and 3600, such as --timeout 120.");
|
|
2640
|
+
return Math.round(seconds * 1000);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/** --header flags and TYPESHIP_HEADERS: sent on every API request, after
|
|
2644
|
+
* (and in place of) any generated header of the same name. */
|
|
2645
|
+
function extraRequestHeaders(): Record<string, string> {
|
|
2646
|
+
try { return parseExtraHeaders(process.env["TYPESHIP_HEADERS"], HEADER_FLAGS, "TYPESHIP_HEADERS"); }
|
|
2647
|
+
catch (error) { fail(2, (error as Error).message); }
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2310
2650
|
/** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
|
|
2311
2651
|
function credentialSource(flags: Map<string, string | boolean>): string | null {
|
|
2312
2652
|
if (Object.keys(flagCredentials(flags)).length) return "flags";
|
|
@@ -2330,7 +2670,29 @@ function resolveBaseUrl(flags: Map<string, string | boolean>, config = readConfi
|
|
|
2330
2670
|
?? DEFAULT_BASE_URL ?? undefined;
|
|
2331
2671
|
}
|
|
2332
2672
|
|
|
2673
|
+
/** OAuth schemes the login session authenticates by name; empty when the
|
|
2674
|
+
* session is the convenience bearer token (see oauthSessionSchemes). */
|
|
2675
|
+
const OAUTH_SESSION_SCHEMES = oauthSessionSchemes(NAMED_SCHEMES);
|
|
2676
|
+
|
|
2677
|
+
/** Send a login session to the OAuth scheme, never to a separate http bearer
|
|
2678
|
+
* scheme. Explicit credentials for the same scheme keep precedence. */
|
|
2679
|
+
function applyOAuthSession(options: ClientOptions & Record<string, unknown>, token: string | (() => Promise<string>), replace = false): void {
|
|
2680
|
+
if (!OAUTH_SESSION_SCHEMES.length) {
|
|
2681
|
+
if (replace || options.bearerToken === undefined) options.bearerToken = token;
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const credentials = (options.credentials ??= {}) as Record<string, unknown>;
|
|
2685
|
+
for (const name of OAUTH_SESSION_SCHEMES) if (replace || !Object.hasOwn(credentials, name)) credentials[name] = token;
|
|
2686
|
+
}
|
|
2687
|
+
function withOAuthSession(options: ClientOptions & Record<string, unknown>, accessToken: string): ClientOptions & Record<string, unknown> {
|
|
2688
|
+
const next: ClientOptions & Record<string, unknown> = { ...options, credentials: { ...(options.credentials as Record<string, unknown>) } as ClientOptions["credentials"] };
|
|
2689
|
+
applyOAuthSession(next, accessToken, true);
|
|
2690
|
+
if (!OAUTH_SESSION_SCHEMES.length) next.credentials = { ...(next.credentials as Record<string, unknown>), ...resolveNamedCredentials(NAMED_SCHEMES, [{ options: { bearerToken: accessToken } }]) } as ClientOptions["credentials"];
|
|
2691
|
+
return next;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2333
2694
|
async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, candidate?: StoredCreds, forIdentity = false): Promise<TypeshipClient> {
|
|
2695
|
+
CURRENT_SCOPES = requiredScopes(op.security);
|
|
2334
2696
|
const flagNamed = flagCredentials(flags), envNamed = environmentCredentials();
|
|
2335
2697
|
const explicitOptions = new Set(AUTH_SCALARS.filter((a) => typeof flags.get(a.flag) === "string" || process.env[a.env] !== undefined).map((a) => a.option));
|
|
2336
2698
|
if (BASIC && (typeof flags.get("username") === "string" || process.env[BASIC.envUser] !== undefined) && (typeof flags.get("password") === "string" || process.env[BASIC.envPass] !== undefined)) explicitOptions.add("basicAuth");
|
|
@@ -2369,25 +2731,29 @@ async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, cand
|
|
|
2369
2731
|
options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
|
|
2370
2732
|
{ named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
|
|
2371
2733
|
{ named: envNamed, options: envOptions }, { named: flagNamed, options: flagOptions },
|
|
2372
|
-
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2734
|
+
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth && !OAUTH_SESSION_SCHEMES.length ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2373
2735
|
]);
|
|
2374
|
-
if (stored?.oauth
|
|
2736
|
+
if (stored?.oauth) {
|
|
2375
2737
|
const sessionId = stored.oauth.sessionId;
|
|
2376
|
-
|
|
2738
|
+
const token = forIdentity ? stored.oauth.accessToken : sessionCredential((rejected) => oauthSessionToken(credentialStore(), {
|
|
2377
2739
|
...sessionConfiguration(baseUrl, config),
|
|
2378
|
-
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken: string) => verifyClientIdentity((values) => new TypeshipClient(values as unknown as ClientOptions),
|
|
2379
|
-
}, sessionId, OAUTH_TOKEN_PARAMS);
|
|
2740
|
+
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken: string) => verifyClientIdentity((values) => new TypeshipClient(values as unknown as ClientOptions), withOAuthSession(options, accessToken), WHOAMI!, IDENTITY_POLICY) } : {}),
|
|
2741
|
+
}, sessionId, OAUTH_TOKEN_PARAMS, rejected));
|
|
2742
|
+
applyOAuthSession(options, token, forIdentity);
|
|
2380
2743
|
}
|
|
2381
2744
|
if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
|
|
2382
2745
|
options.debug = (event: DebugEvent) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
|
|
2383
2746
|
}
|
|
2384
2747
|
if (flags.get("validate") === true) options.validate = true;
|
|
2748
|
+
const timeout = requestTimeoutMs(flags);
|
|
2749
|
+
if (timeout !== undefined) options.timeoutMs = timeout;
|
|
2385
2750
|
for (const g of GLOBALS) {
|
|
2386
2751
|
const flagValue = flags.get(g.flag);
|
|
2387
2752
|
const value = typeof flagValue === "string" ? flagValue : process.env["TYPESHIP_" + g.envSuffix];
|
|
2388
2753
|
if (value !== undefined) options[g.option] = value;
|
|
2389
2754
|
}
|
|
2390
2755
|
LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
|
|
2756
|
+
LAST_CLIENT_BASIC = { basicAuth: options.basicAuth, credentials: options.credentials };
|
|
2391
2757
|
requireOperationCredentials(op, options);
|
|
2392
2758
|
// Identify the package and version. Optional harness and caller details
|
|
2393
2759
|
// let the API distinguish agent traffic from other non-interactive use.
|
|
@@ -2401,6 +2767,14 @@ async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, cand
|
|
|
2401
2767
|
"User-Agent": PKG_NAME + "-cli/" + VERSION + (details ? " (" + details + ")" : ""),
|
|
2402
2768
|
};
|
|
2403
2769
|
if (forIdentity) { options.fetch = identityFetch(baseUrl); options.maxRetries = 0; options.timeoutMs = 10_000; }
|
|
2770
|
+
const extraHeaders = extraRequestHeaders();
|
|
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
|
+
}
|
|
2404
2778
|
return new TypeshipClient(options);
|
|
2405
2779
|
}
|
|
2406
2780
|
|
|
@@ -2438,9 +2812,9 @@ function failOmitted(op: OmittedOpSpec): never {
|
|
|
2438
2812
|
return failWith({
|
|
2439
2813
|
status: "action_required",
|
|
2440
2814
|
code: "PLAN_LIMIT",
|
|
2441
|
-
message: "The command '" + BIN + " " + op.command.join(" ") + "'
|
|
2442
|
-
detail: { operation: op.tool, method: op.httpMethod, path: op.path, generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
|
|
2443
|
-
nextSteps: ["
|
|
2815
|
+
message: "The command '" + BIN + " " + op.command.join(" ") + "' is in the API but not in this package, which was generated with " + OPS.length + " of its " + (OPS.length + EXCLUDED_OPS) + " operations.",
|
|
2816
|
+
detail: { operation: op.tool, ...(op.graphql ? { graphql: op.graphql.kind + " " + op.graphql.field } : { method: op.httpMethod, path: op.path }), generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
|
|
2817
|
+
nextSteps: ["The package's publisher can regenerate it with every operation.", "Do not invent or retry an omitted command against this package."],
|
|
2444
2818
|
});
|
|
2445
2819
|
}
|
|
2446
2820
|
|
|
@@ -2449,24 +2823,32 @@ const BUILTIN_COMMANDS = ["login","logout","whoami","config","mcp","docs","upgra
|
|
|
2449
2823
|
async function main(): Promise<void> {
|
|
2450
2824
|
const argv = process.argv.slice(2);
|
|
2451
2825
|
const parsed = parseArgv(argv);
|
|
2826
|
+
const headerFlag = parsed.flags.get("header");
|
|
2827
|
+
HEADER_FLAGS = parsed.repeated.get("header") ?? (typeof headerFlag === "string" ? [headerFlag] : []);
|
|
2828
|
+
if (headerFlag === true) fail(2, "--header expects \"Name: value\".");
|
|
2452
2829
|
if (!parsed.help) validateCredentialsInput(parsed.flags);
|
|
2453
2830
|
const profileFlag = parsed.flags.get("profile");
|
|
2454
2831
|
if (profileFlag !== undefined && typeof profileFlag !== "string") fail(2, "--profile requires a profile name.");
|
|
2455
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 });
|
|
2456
2833
|
if (!parsed.help && ["login", "init"].includes(parsed.positionals[0] ?? "")) expectedLoginIdentity(parsed.flags);
|
|
2457
2834
|
if (parsed.positionals[0] === "help") {
|
|
2458
|
-
// help --json:
|
|
2459
|
-
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); }
|
|
2460
2837
|
parsed.positionals.shift(); parsed.help = true;
|
|
2461
2838
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
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.");
|
|
2464
2846
|
}
|
|
2465
2847
|
if (parsed.flags.has("version") || parsed.positionals[0] === "version") {
|
|
2466
2848
|
// "acme 1.0.0 (acme 1.0.0)" would say the name twice; when the API's
|
|
2467
2849
|
// title is the bin, name the API version as such.
|
|
2468
2850
|
const apiLabel = API_TITLE.toLowerCase().replace(/[^a-z0-9]/g, "") === BIN.toLowerCase().replace(/[^a-z0-9]/g, "") ? "API " + API_VERSION : API_TITLE + " " + API_VERSION;
|
|
2469
|
-
process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by
|
|
2851
|
+
process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by Typeship)\n");
|
|
2470
2852
|
await flushExit(0);
|
|
2471
2853
|
}
|
|
2472
2854
|
const [resourceCmd, methodCmd] = parsed.positionals;
|
|
@@ -2531,12 +2913,18 @@ async function main(): Promise<void> {
|
|
|
2531
2913
|
|
|
2532
2914
|
const pathSpecs = op.params.filter((p) => p.kind === "path");
|
|
2533
2915
|
const pathValues = parsed.positionals.slice(2);
|
|
2534
|
-
|
|
2535
|
-
|
|
2916
|
+
// A path argument that is the Basic-auth username (Twilio's AccountSid)
|
|
2917
|
+
// may be left out; it defaults to the configured credential below.
|
|
2918
|
+
const pathGiven = pathValues.length === pathSpecs.length ? pathSpecs
|
|
2919
|
+
: pathValues.length === pathSpecs.filter((p) => !p.credential).length ? pathSpecs.filter((p) => !p.credential)
|
|
2920
|
+
: null;
|
|
2921
|
+
if (!pathGiven) {
|
|
2922
|
+
const optional = pathSpecs.filter((p) => p.credential).length;
|
|
2923
|
+
fail(2, "Expected " + (optional ? (pathSpecs.length - optional) + " or " : "") + pathSpecs.length + " argument(s): " + usageLine(op));
|
|
2536
2924
|
}
|
|
2537
2925
|
|
|
2538
2926
|
const values: Record<string, unknown> = {};
|
|
2539
|
-
|
|
2927
|
+
pathGiven!.forEach((spec, i) => { values[spec.name] = pathValues[i]; });
|
|
2540
2928
|
|
|
2541
2929
|
let dataBody: unknown;
|
|
2542
2930
|
const dataRaw = parsed.flags.get("data");
|
|
@@ -2557,7 +2945,7 @@ async function main(): Promise<void> {
|
|
|
2557
2945
|
// Mirrors opReservedFlags() in the generator: API parameters never use these
|
|
2558
2946
|
// names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
|
|
2559
2947
|
// check can be exact.
|
|
2560
|
-
const RESERVED_FLAGS = new Set(["data", "credentials", "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)]);
|
|
2561
2949
|
for (const spec of op.params) {
|
|
2562
2950
|
if (spec.kind === "path") continue;
|
|
2563
2951
|
const raw = parsed.flags.get(spec.flag);
|
|
@@ -2566,7 +2954,7 @@ async function main(): Promise<void> {
|
|
|
2566
2954
|
// every value (each coerced to the element type); loosely typed (json)
|
|
2567
2955
|
// params become an array of the parsed values.
|
|
2568
2956
|
const all = parsed.repeated.get(spec.flag);
|
|
2569
|
-
values[spec.name] = spec.type === "array"
|
|
2957
|
+
values[spec.name] = spec.type === "array" || (spec.type === "file" && spec.multiple)
|
|
2570
2958
|
? coerce(spec, raw, all)
|
|
2571
2959
|
: all !== undefined && spec.type === "json"
|
|
2572
2960
|
? all.map((v) => coerce(spec, v))
|
|
@@ -2575,12 +2963,39 @@ async function main(): Promise<void> {
|
|
|
2575
2963
|
for (const key of parsed.flags.keys()) {
|
|
2576
2964
|
if (RESERVED_FLAGS.has(key)) continue;
|
|
2577
2965
|
if (key === "file" && op.bodyKind === "binary") continue;
|
|
2966
|
+
if (key === "output" && op.rawResponse) continue;
|
|
2578
2967
|
if (!op.params.some((p) => p.flag === key)) {
|
|
2579
2968
|
const suggestion = didYouMean(key, [...op.params.filter((p) => p.kind !== "path").map((p) => p.flag), ...RESERVED_FLAGS]);
|
|
2580
2969
|
fail(2, "Unknown flag --" + key + "." + (suggestion ? " Did you mean --" + suggestion + "?" : ""));
|
|
2581
2970
|
}
|
|
2582
2971
|
}
|
|
2583
2972
|
|
|
2973
|
+
// Object and array values (a GraphQL input, a JSON flag, --data fields)
|
|
2974
|
+
// get the checks the MCP server applies: nested types, enums, required
|
|
2975
|
+
// properties, patterns and a closed object's unknown keys, all at once.
|
|
2976
|
+
const argumentSchemas = (op.inputSchema.properties ?? {}) as Record<string, Record<string, unknown>>;
|
|
2977
|
+
const nestedIssues: ArgumentIssue[] = [];
|
|
2978
|
+
const checkNested = (name: string, value: unknown): unknown =>
|
|
2979
|
+
value !== null && typeof value === "object" && !(value instanceof Blob) && argumentSchemas[name]
|
|
2980
|
+
? checkValue(value, argumentSchemas[name]!, name, nestedIssues) : value;
|
|
2981
|
+
for (const spec of op.params) {
|
|
2982
|
+
if (spec.kind !== "path" && spec.type !== "file" && values[spec.name] !== undefined) values[spec.name] = checkNested(spec.name, values[spec.name]);
|
|
2983
|
+
}
|
|
2984
|
+
if (op.bodyStyle === "fields" && dataBody !== null && typeof dataBody === "object" && !Array.isArray(dataBody) && !(dataBody instanceof Blob)) {
|
|
2985
|
+
const body = dataBody as Record<string, unknown>;
|
|
2986
|
+
for (const name of Object.keys(body)) {
|
|
2987
|
+
if (op.params.some((p) => p.kind === "body" && p.name === name && p.type !== "file")) body[name] = checkNested(name, body[name]);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
if (nestedIssues.length > 0) {
|
|
2991
|
+
failWith({
|
|
2992
|
+
code: nestedIssues.every((issue) => issue.code === "MISSING_ARGUMENT") ? "MISSING_ARGUMENT" : "INVALID_USAGE",
|
|
2993
|
+
message: nestedIssues.length + (nestedIssues.length === 1 ? " problem" : " problems") + " in the arguments; nothing was sent: " + nestedIssues.map((issue) => issue.message).join("; "),
|
|
2994
|
+
detail: { issues: nestedIssues },
|
|
2995
|
+
nextSteps: ["Fix the values listed in detail.issues and run again.", "Run '" + USAGE_HINT + "' for each argument's type."],
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2584
2999
|
const missing = missingRequired(op, values).filter((name) =>
|
|
2585
3000
|
!(op.bodyStyle === "fields" && dataBody !== undefined && typeof dataBody === "object" && dataBody !== null && name in (dataBody as object)),
|
|
2586
3001
|
);
|
|
@@ -2600,6 +3015,7 @@ async function main(): Promise<void> {
|
|
|
2600
3015
|
if (typeof fieldsRaw === "string") {
|
|
2601
3016
|
FIELDS = fieldsRaw.split(",").map((f) => f.trim()).filter((f) => f !== "").map((f) => f.split("."));
|
|
2602
3017
|
if (FIELDS.length === 0) fail(2, "--fields expects at least one field path");
|
|
3018
|
+
FIELDS_AFTER_WRITE = op.safety !== "read";
|
|
2603
3019
|
}
|
|
2604
3020
|
|
|
2605
3021
|
// --out <dir> materializes a file-shaped response (see cli-agent.ts bundleProperty).
|
|
@@ -2611,13 +3027,54 @@ async function main(): Promise<void> {
|
|
|
2611
3027
|
}
|
|
2612
3028
|
|
|
2613
3029
|
validateParameters(op, values, parsed.flags);
|
|
3030
|
+
// Raw bytes on a terminal are unreadable and can garble it: ask for a
|
|
3031
|
+
// destination before the request runs (it may be billed, like speech).
|
|
3032
|
+
const outputFlag = op.rawResponse ? parsed.flags.get("output") : undefined;
|
|
3033
|
+
if (outputFlag === true) fail(2, "--output expects a file path, or - for stdout.");
|
|
3034
|
+
if (op.rawResponse === "binary" && outputFlag === undefined && process.stdout.isTTY) {
|
|
3035
|
+
fail(2, op.command.join(" ") + " returns binary data. Pass --output <file>, or redirect stdout to a file.");
|
|
3036
|
+
}
|
|
3037
|
+
// --all on a list the generator could not page would print one page and
|
|
3038
|
+
// exit 0, which reads as "that is everything".
|
|
3039
|
+
if (!op.paginated && parsed.flags.get("all") === true) {
|
|
3040
|
+
fail(2, op.command.join(" ") + " does not paginate, so --all has nothing to walk. Run it without --all; it returns the whole response.");
|
|
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
|
+
}
|
|
2614
3054
|
const client = await makeClient(parsed.flags, op);
|
|
3055
|
+
for (const spec of pathSpecs) {
|
|
3056
|
+
if (!spec.credential || values[spec.name] !== undefined) continue;
|
|
3057
|
+
const username = credentialUsername(spec.credential.scheme);
|
|
3058
|
+
if (username === undefined) {
|
|
3059
|
+
fail(2, "Missing required: <" + spec.name + ">. It defaults to the Basic-auth username (" + spec.credential.env + "), and none is configured.", undefined,
|
|
3060
|
+
["Pass " + spec.name + " as an argument: " + usageLine(op) + ".", "Or set " + spec.credential.env + (BASIC ? " and " + BASIC.envPass : "") + ", or run '" + BIN + " login'."]);
|
|
3061
|
+
}
|
|
3062
|
+
values[spec.name] = username;
|
|
3063
|
+
}
|
|
2615
3064
|
|
|
2616
3065
|
// Destructive commands need --force. A person gets asked; an agent gets
|
|
2617
3066
|
// an action_required envelope with the exact command to run, so nothing
|
|
2618
3067
|
// is deleted on a guess.
|
|
2619
|
-
if (op.safety === "destructive" && !assumeYes(parsed)) {
|
|
2620
|
-
|
|
3068
|
+
if (op.safety === "destructive" && !assumeYes(parsed) && !DRY_RUN) {
|
|
3069
|
+
// Credential flags are replaced by placeholders: the rerun is shown to
|
|
3070
|
+
// agents and logged, so it must never repeat a key.
|
|
3071
|
+
const secretFlags = new Set([...AUTH_SCALARS.map((a) => "--" + a.flag), "--header"]);
|
|
3072
|
+
const rerun = BIN + " " + process.argv.slice(2).map((a, i, all) => {
|
|
3073
|
+
const eq = a.indexOf("=");
|
|
3074
|
+
if (a.startsWith("--") && eq > 0 && secretFlags.has(a.slice(0, eq))) return a.slice(0, eq) + "=<" + a.slice(2, eq) + ">";
|
|
3075
|
+
if (i > 0 && secretFlags.has(all[i - 1]!)) return "<" + all[i - 1]!.slice(2) + ">";
|
|
3076
|
+
return /\s/.test(a) ? JSON.stringify(a) : a;
|
|
3077
|
+
}).join(" ") + " --force";
|
|
2621
3078
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
2622
3079
|
failWith({
|
|
2623
3080
|
status: "action_required",
|
|
@@ -2634,20 +3091,30 @@ async function main(): Promise<void> {
|
|
|
2634
3091
|
const selectValue = typeof parsed.flags.get("select") === "string" ? (parsed.flags.get("select") as string) : undefined;
|
|
2635
3092
|
const args = buildArgs(op, values, dataBody, selectValue);
|
|
2636
3093
|
const target = (client as unknown as Record<string, Record<string, (...a: unknown[]) => unknown>>)[op.resource]!;
|
|
2637
|
-
|
|
3094
|
+
// --stream true on an operation with a streaming twin prints its events
|
|
3095
|
+
// as NDJSON as they arrive, instead of failing on the event stream.
|
|
3096
|
+
const streaming = op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value;
|
|
3097
|
+
const callResult = target[streaming ? op.streamMethod!.method : op.method]!(...args);
|
|
3098
|
+
if (DRY_RUN) await printDryRun(parsed, callResult);
|
|
2638
3099
|
|
|
2639
3100
|
if (op.paginated && parsed.flags.get("all") === true) {
|
|
3101
|
+
const fieldsCheck = streamFieldsCheck((listSchemaOf(op.outputSchema, "items") as { items?: unknown } | undefined)?.items);
|
|
2640
3102
|
try {
|
|
2641
3103
|
for await (const item of callResult as AsyncIterable<unknown>) {
|
|
2642
|
-
process.stdout.write(JSON.stringify(
|
|
3104
|
+
process.stdout.write(JSON.stringify(fieldsCheck.item(item)) + "\n");
|
|
2643
3105
|
}
|
|
3106
|
+
fieldsCheck.finish();
|
|
2644
3107
|
await flushExit(0);
|
|
2645
3108
|
} catch (e) {
|
|
3109
|
+
if (isNotModified(e)) await printNotModified(e);
|
|
2646
3110
|
failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
|
|
2647
3111
|
}
|
|
2648
3112
|
}
|
|
2649
3113
|
|
|
2650
3114
|
let result = await asApiResult(callResult as Promise<unknown>);
|
|
3115
|
+
// 304 Not Modified: the conditional request matched, so there is no
|
|
3116
|
+
// body. Say so, with the ETag to send next time.
|
|
3117
|
+
if (!result.ok && isNotModified(result.error)) await printNotModified(result.error);
|
|
2651
3118
|
if (result.ok && op.httpMethod === "POST" && op.path === "/projects/{project_id}/generate") {
|
|
2652
3119
|
const batch = result.data as { data: Array<{ id: string }> };
|
|
2653
3120
|
const generations = (client as unknown as { generations: { wait(id: string): Promise<unknown> } }).generations;
|
|
@@ -2660,15 +3127,33 @@ async function main(): Promise<void> {
|
|
|
2660
3127
|
result = { ...result, data: { ...batch, data: completed } };
|
|
2661
3128
|
}
|
|
2662
3129
|
if (result.ok) {
|
|
2663
|
-
if (op.sse) {
|
|
3130
|
+
if (op.sse || streaming) {
|
|
2664
3131
|
// Server-sent events as NDJSON, one line per event, until the stream ends.
|
|
3132
|
+
const fieldsCheck = streamFieldsCheck(undefined);
|
|
2665
3133
|
try {
|
|
2666
3134
|
for await (const event of result.data as AsyncIterable<{ event?: string; id?: string; data: string }>) {
|
|
2667
|
-
process.stdout.write(JSON.stringify(
|
|
3135
|
+
process.stdout.write(JSON.stringify(fieldsCheck.item(event)) + "\n");
|
|
2668
3136
|
}
|
|
2669
3137
|
} catch (e) {
|
|
2670
3138
|
failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
|
|
2671
3139
|
}
|
|
3140
|
+
fieldsCheck.finish();
|
|
3141
|
+
await flushExit(0);
|
|
3142
|
+
}
|
|
3143
|
+
// A non-JSON body (audio, a file, CSV): raw bytes to stdout, or to the
|
|
3144
|
+
// --output file with a JSON note of what was written. Never "{}".
|
|
3145
|
+
if (op.rawResponse || result.data instanceof Blob) {
|
|
3146
|
+
const data = result.data;
|
|
3147
|
+
const bytes = data instanceof Blob
|
|
3148
|
+
? new Uint8Array(await data.arrayBuffer())
|
|
3149
|
+
: new TextEncoder().encode(typeof data === "string" ? data : JSON.stringify(data ?? ""));
|
|
3150
|
+
if (typeof outputFlag === "string" && outputFlag !== "-") {
|
|
3151
|
+
try { writeFileSync(outputFlag, bytes); } catch (e) { fail(1, "--output: cannot write " + outputFlag + " (" + (e as Error).message + ")"); }
|
|
3152
|
+
const mediaType = data instanceof Blob && data.type ? data.type : op.rawResponse === "text" ? "text/plain" : "application/octet-stream";
|
|
3153
|
+
out({ saved_to: resolvePath(outputFlag), bytes: bytes.length, media_type: mediaType });
|
|
3154
|
+
} else {
|
|
3155
|
+
process.stdout.write(bytes);
|
|
3156
|
+
}
|
|
2672
3157
|
await flushExit(0);
|
|
2673
3158
|
}
|
|
2674
3159
|
// A claim-shaped response (claim.url): say where to claim it, and leave
|
|
@@ -2686,17 +3171,17 @@ async function main(): Promise<void> {
|
|
|
2686
3171
|
// agent never has to reconstruct the cursor flag.
|
|
2687
3172
|
const page = result.data as { items: unknown[]; hasNextPage(): boolean; nextPageParams(): Record<string, unknown> | null; response: { requestId?: string } };
|
|
2688
3173
|
const next = page.nextPageParams();
|
|
2689
|
-
|
|
2690
|
-
items: project(page.items),
|
|
3174
|
+
printResult({
|
|
3175
|
+
items: project(page.items, true, listSchemaOf(op.outputSchema, "items")),
|
|
2691
3176
|
hasMore: next !== null,
|
|
2692
3177
|
...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
|
|
2693
3178
|
...(page.response.requestId ? { request_id: page.response.requestId } : {}),
|
|
2694
|
-
});
|
|
3179
|
+
}, op.command[0]);
|
|
2695
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])) {
|
|
2696
3181
|
// Batch-style collection envelopes ({data: [...]}) use item-relative
|
|
2697
3182
|
// fields, matching paginated results, while retaining envelope metadata.
|
|
2698
3183
|
const data = result.data as Record<string, unknown>;
|
|
2699
|
-
|
|
3184
|
+
printResult({ ...data, [collectionField]: project(data[collectionField], true, listSchemaOf(op.outputSchema, collectionField)) }, op.command[0], collectionField);
|
|
2700
3185
|
} else if (outDir !== undefined && bundleField !== null) {
|
|
2701
3186
|
// --out: a file-shaped response (an array of {path, content}) lands
|
|
2702
3187
|
// on disk; stdout gets the rest of the response plus a summary.
|
|
@@ -2704,9 +3189,9 @@ async function main(): Promise<void> {
|
|
|
2704
3189
|
const files = (data[bundleField] as { path: string; content: string }[] | undefined) ?? [];
|
|
2705
3190
|
const written = writeBundle(outDir, files);
|
|
2706
3191
|
const { [bundleField]: _omitted, ...rest } = data;
|
|
2707
|
-
|
|
3192
|
+
printResult({ ...(project(rest, false, op.outputSchema) as Record<string, unknown>), out: written }, op.command[0]);
|
|
2708
3193
|
} else {
|
|
2709
|
-
|
|
3194
|
+
printResult(result.data === undefined || result.data === null ? { ok: true } : project(result.data, Array.isArray(result.data), op.outputSchema), op.command[0], collectionField);
|
|
2710
3195
|
}
|
|
2711
3196
|
await flushExit(0);
|
|
2712
3197
|
}
|
|
@@ -2724,6 +3209,8 @@ function errorMessage(error: unknown): string {
|
|
|
2724
3209
|
main().catch((e) => {
|
|
2725
3210
|
if (e instanceof ExitPending) return;
|
|
2726
3211
|
try {
|
|
3212
|
+
const auth = classifyAuthFailure(e, authFailureContext());
|
|
3213
|
+
if (auth) failWith(auth);
|
|
2727
3214
|
fail(1, errorMessage(e));
|
|
2728
3215
|
} catch {
|
|
2729
3216
|
// exit already scheduled
|