@typeship-ax/cli 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -9
- package/README.md +14 -27
- package/api.json +8898 -9026
- package/api.md +370 -328
- package/dist/arguments.d.ts +54 -0
- package/dist/arguments.d.ts.map +1 -0
- package/dist/arguments.js +265 -0
- package/dist/cli-agent.d.ts +73 -9
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +331 -44
- package/dist/cli.js +802 -291
- package/dist/core/http.d.ts +162 -19
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +381 -48
- package/dist/core/pagination.d.ts +42 -6
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +111 -17
- package/dist/credential-storage.d.ts +10 -3
- package/dist/credential-storage.d.ts.map +1 -1
- package/dist/credential-storage.js +15 -6
- package/dist/dates.d.ts +1 -1
- package/dist/dates.js +1 -1
- package/dist/errors.d.ts +20 -84
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -108
- package/dist/fields.d.ts +36 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +187 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -25
- package/dist/named-credentials.d.ts +19 -0
- package/dist/named-credentials.d.ts.map +1 -1
- package/dist/named-credentials.js +81 -1
- package/dist/oauth-login.d.ts +8 -2
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +31 -19
- package/dist/oauth-request.d.ts +7 -1
- package/dist/oauth-request.d.ts.map +1 -1
- package/dist/oauth-request.js +26 -4
- package/dist/oauth-session.d.ts +13 -1
- package/dist/oauth-session.d.ts.map +1 -1
- package/dist/oauth-session.js +34 -18
- package/dist/ops.d.ts +58 -5
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +110 -41
- package/dist/polling-login.d.ts +8 -2
- package/dist/polling-login.d.ts.map +1 -1
- package/dist/polling-login.js +25 -11
- package/dist/resources/api-keys.d.ts +10 -7
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +10 -31
- package/dist/resources/deliveries.d.ts +89 -5
- package/dist/resources/deliveries.d.ts.map +1 -1
- package/dist/resources/deliveries.js +96 -19
- package/dist/resources/drafts.d.ts +16 -16
- package/dist/resources/drafts.d.ts.map +1 -1
- package/dist/resources/drafts.js +12 -65
- package/dist/resources/files.d.ts +4 -4
- package/dist/resources/files.d.ts.map +1 -1
- package/dist/resources/files.js +3 -12
- package/dist/resources/generations.d.ts +16 -16
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +23 -47
- package/dist/resources/organization.d.ts +4 -4
- package/dist/resources/organization.d.ts.map +1 -1
- package/dist/resources/organization.js +3 -10
- package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
- package/dist/resources/packages.d.ts.map +1 -0
- package/dist/resources/{generate.js → packages.js} +13 -29
- package/dist/resources/projects.d.ts +50 -50
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +60 -116
- package/dist/resources/releases.d.ts +22 -17
- package/dist/resources/releases.d.ts.map +1 -1
- package/dist/resources/releases.js +19 -40
- package/dist/resources/spec-revisions.d.ts +16 -7
- package/dist/resources/spec-revisions.d.ts.map +1 -1
- package/dist/resources/spec-revisions.js +7 -29
- package/dist/resources/specs.d.ts +7 -7
- package/dist/resources/specs.d.ts.map +1 -1
- package/dist/resources/specs.js +6 -34
- package/dist/resources/targets.d.ts +49 -49
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +59 -115
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +83 -81
- package/dist/search.d.ts +54 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +421 -0
- package/dist/table.d.ts +28 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +167 -0
- package/dist/type-docs.d.ts +61 -0
- package/dist/type-docs.d.ts.map +1 -0
- package/dist/type-docs.js +174 -0
- package/dist/types.d.ts +499 -339
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +18 -18
- package/package.json +5 -2
- package/src/arguments.ts +254 -0
- package/src/cli-agent.ts +351 -46
- package/src/cli.ts +753 -266
- package/src/core/http.ts +457 -58
- package/src/core/pagination.ts +129 -18
- package/src/credential-storage.ts +16 -6
- package/src/dates.ts +1 -1
- package/src/errors.ts +46 -115
- package/src/fields.ts +167 -0
- package/src/index.ts +45 -28
- package/src/named-credentials.ts +66 -1
- package/src/oauth-login.ts +36 -21
- package/src/oauth-request.ts +32 -6
- package/src/oauth-session.ts +37 -19
- package/src/ops.ts +146 -45
- package/src/polling-login.ts +24 -11
- package/src/resources/api-keys.ts +34 -48
- package/src/resources/deliveries.ts +213 -32
- package/src/resources/drafts.ts +62 -109
- package/src/resources/files.ts +19 -20
- package/src/resources/generations.ts +61 -79
- package/src/resources/organization.ts +11 -16
- package/src/resources/{generate.ts → packages.ts} +43 -51
- package/src/resources/projects.ts +145 -200
- package/src/resources/releases.ts +50 -67
- package/src/resources/spec-revisions.ts +40 -49
- package/src/resources/specs.ts +39 -59
- package/src/resources/targets.ts +144 -194
- package/src/schemas.ts +83 -81
- package/src/search.ts +434 -0
- package/src/table.ts +167 -0
- package/src/type-docs.ts +205 -0
- package/src/types.ts +538 -357
- package/dist/console-login-check.d.ts +0 -21
- package/dist/console-login-check.d.ts.map +0 -1
- package/dist/console-login-check.js +0 -107
- package/dist/console-login-contract.d.ts +0 -45
- package/dist/console-login-contract.d.ts.map +0 -1
- package/dist/console-login-contract.js +0 -40
- package/dist/resources/generate.d.ts.map +0 -1
- package/dist/resources/publications.d.ts +0 -47
- package/dist/resources/publications.d.ts.map +0 -1
- package/dist/resources/publications.js +0 -70
- package/src/console-login-check.ts +0 -88
- package/src/console-login-contract.ts +0 -65
- package/src/resources/publications.ts +0 -140
package/dist/cli.js
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.
|
|
@@ -8,34 +8,50 @@ import { spawnSync } from "node:child_process";
|
|
|
8
8
|
import { oauthBrowserLogin } from "./oauth-login.js";
|
|
9
9
|
import { oauthDeviceLogin, customBrowserApproval, loginEndpoint } from "./polling-login.js";
|
|
10
10
|
import { oauthStatusRequest } from "./oauth-request.js";
|
|
11
|
-
import {
|
|
12
|
-
import { assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, oauthSessionToken, sessionBinding } from "./oauth-session.js";
|
|
11
|
+
import { sessionCredential, assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, oauthSessionToken, sessionBinding } from "./oauth-session.js";
|
|
13
12
|
import { createCredentialStore } from "./credential-storage.js";
|
|
14
13
|
import { identityFetch, identityResult, verifyApiIdentity, verifyClientIdentity, readApiIdentity, assertApiIdentity } from "./api-identity.js";
|
|
15
|
-
import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability } from "./named-credentials.js";
|
|
14
|
+
import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability, missingCredentials, oauthSessionSchemes, parseExtraHeaders, applyExtraHeaders } from "./named-credentials.js";
|
|
16
15
|
import { resolveProfile, listProfiles, selectProfile, removeProfile, readProfileConfig, updateProfileConfig } from "./auth-profiles.js";
|
|
17
16
|
import { randomBytes } from "node:crypto";
|
|
18
17
|
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
19
18
|
import { homedir, hostname } from "node:os";
|
|
20
|
-
import { basename, join } from "node:path";
|
|
19
|
+
import { basename, join, resolve as resolvePath } from "node:path";
|
|
21
20
|
import { fileURLToPath } from "node:url";
|
|
22
21
|
import { TypeshipClient, formatDebugEvent } from "./index.js";
|
|
23
|
-
import { asApiResult, validateAgainstSchema, ValidationError } from "./core/http.js";
|
|
22
|
+
import { asApiResult, mediaTypeForPath, validateAgainstSchema, ValidationError } from "./core/http.js";
|
|
24
23
|
import { SCHEMAS, DEFS } from "./schemas.js";
|
|
25
|
-
import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
|
|
26
|
-
import { MCP_CLIENTS, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
|
|
24
|
+
import { GLOBALS, INPUT_TYPES, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
|
|
25
|
+
import { MCP_CLIENTS, requiredScopes, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, classifyAuthFailure, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, formatRequestPreview, installSkills, mcpConfigured, pendingClaims, recordClaim, requestPreview, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
|
|
27
26
|
import { relativeDate } from "./dates.js";
|
|
27
|
+
import { checkValue } from "./arguments.js";
|
|
28
28
|
import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
|
|
29
|
+
import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
|
|
30
|
+
import { renderTable } from "./table.js";
|
|
31
|
+
import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
|
|
32
|
+
import { argumentPathText, findInputType, inputTypeText, namedTypesIn } from "./type-docs.js";
|
|
29
33
|
const BIN = "typeship";
|
|
34
|
+
/** The MCP server's key in client configs: the API's name, even when this
|
|
35
|
+
* command was renamed away from a vendor's. It reads this command's login. */
|
|
36
|
+
const MCP_SERVER_KEY = "typeship";
|
|
30
37
|
const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
|
|
31
38
|
const NAMED_SCHEMES = { "apiKey": { "kind": "bearer", "options": ["bearerToken"] } };
|
|
32
39
|
const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESHIP_TOKEN" }];
|
|
40
|
+
/** Hosted MCP request headers as name → env reference, never a literal. */
|
|
41
|
+
const HOSTED_MCP_HEADERS = { "Authorization": "Bearer ${TYPESHIP_TOKEN}" };
|
|
42
|
+
const HOSTED_MCP_NOTE = null;
|
|
33
43
|
const BASIC = null;
|
|
44
|
+
/** Header and query names the API's key schemes use: --dry-run redacts them. */
|
|
45
|
+
const CREDENTIAL_NAMES = [];
|
|
46
|
+
/** The spec declares no security, so the token is offered, never required. */
|
|
47
|
+
const AUTH_UNDECLARED = false;
|
|
48
|
+
/** Repeated --header "Name: value" flags for this invocation. */
|
|
49
|
+
let HEADER_FLAGS = [];
|
|
34
50
|
/** Operations omitted from the generated package by its plan cap. */
|
|
35
51
|
const EXCLUDED_OPS = 0;
|
|
36
52
|
/** Generated CLI operations that are intentionally unavailable to MCP. */
|
|
37
53
|
const MCP_EXCLUDED_OPS = 0;
|
|
38
|
-
const VERSION = "0.
|
|
54
|
+
const VERSION = "0.24.0";
|
|
39
55
|
const API_VERSION = "1.0.0";
|
|
40
56
|
const SPEC_FORMAT = "openapi";
|
|
41
57
|
const IDENTITY_POLICY = {};
|
|
@@ -44,10 +60,13 @@ const WHOAMI = null;
|
|
|
44
60
|
const ENVIRONMENTS = {};
|
|
45
61
|
const HAS_MCP = false;
|
|
46
62
|
const PKG_NAME = "@typeship-ax/cli";
|
|
63
|
+
/** False when the package name was derived rather than chosen: the npm
|
|
64
|
+
* package of that name may be someone else's, so upgrade never installs it. */
|
|
65
|
+
const PKG_CONFIRMED = true;
|
|
47
66
|
const UPDATE_NOTICE = false;
|
|
48
67
|
const API_DESCRIPTION = "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";
|
|
49
|
-
const DOCS_URL_DEFAULT = "https://typeship.dev";
|
|
50
|
-
const DOCS_INDEX_URL_DEFAULT =
|
|
68
|
+
const DOCS_URL_DEFAULT = "https://typeship.dev/docs";
|
|
69
|
+
const DOCS_INDEX_URL_DEFAULT = "https://typeship.dev/llms.txt";
|
|
51
70
|
const RELAY = null;
|
|
52
71
|
const SUPPORT_URL = null;
|
|
53
72
|
const OAUTH_TOKEN_URL = null;
|
|
@@ -57,6 +76,9 @@ const OAUTH_DISCOVERY_URLS = [];
|
|
|
57
76
|
const OAUTH_DEVICE_URL = null;
|
|
58
77
|
const HAS_OAUTH_LOGIN = OAUTH_TOKEN_URL !== null || OAUTH_DISCOVERY_URLS.length > 0;
|
|
59
78
|
const OAUTH_LOGIN_METHOD = "device";
|
|
79
|
+
/** The loopback port browser login listens on unless the redirect URI names
|
|
80
|
+
* one: fixed per CLI, so it can be registered with the provider. */
|
|
81
|
+
const OAUTH_DEFAULT_REDIRECT_PORT = 49910;
|
|
60
82
|
const OAUTH_REDIRECT_URI = undefined;
|
|
61
83
|
const OAUTH_ORGANIZATION_PARAMETER = undefined;
|
|
62
84
|
const OAUTH_AUTHORIZATION_URL = undefined;
|
|
@@ -67,14 +89,14 @@ const MCP_URL = "https://typeship.dev/mcp";
|
|
|
67
89
|
const SKILLS_REPO = "typeship-ax/skills";
|
|
68
90
|
const CLI_AUTH_URL = "https://typeship.dev/api/auth/cli";
|
|
69
91
|
const ENV_PREFIX = "TYPESHIP";
|
|
70
|
-
const API_TITLE = "
|
|
92
|
+
const API_TITLE = "Typeship";
|
|
71
93
|
/** Flags that never take a value, so they don't swallow the next positional
|
|
72
94
|
* (`--non-interactive accounts list`). Built-in commands' own switches
|
|
73
95
|
* (`mcp --cursor`, `upgrade --check`) count only under that command, so an
|
|
74
96
|
* API parameter with the same name (`accounts list --cursor <c>`) still
|
|
75
97
|
* takes its value. Boolean API parameters are recognized once the command
|
|
76
98
|
* is known. */
|
|
77
|
-
const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json"]);
|
|
99
|
+
const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json", "dry-run"]);
|
|
78
100
|
const BUILTIN_BOOLEAN_FLAGS = {
|
|
79
101
|
login: ["with-token", "no-browser", "device"],
|
|
80
102
|
logout: ["local"],
|
|
@@ -82,7 +104,7 @@ const BUILTIN_BOOLEAN_FLAGS = {
|
|
|
82
104
|
mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
|
|
83
105
|
docs: ["web", "schema"],
|
|
84
106
|
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
|
|
85
|
-
auth: ["live"],
|
|
107
|
+
auth: ["live", "offline"],
|
|
86
108
|
doctor: [],
|
|
87
109
|
};
|
|
88
110
|
function isBooleanFlag(name, positionals) {
|
|
@@ -217,35 +239,65 @@ function out(value) {
|
|
|
217
239
|
}
|
|
218
240
|
/** --fields a,b.c: the dotted paths to keep in API results (null = everything). Set in main(). */
|
|
219
241
|
let FIELDS = null;
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return value;
|
|
228
|
-
const groups = new Map();
|
|
229
|
-
for (const [key, ...rest] of paths) {
|
|
230
|
-
if (key === undefined)
|
|
231
|
-
continue;
|
|
232
|
-
const group = groups.get(key);
|
|
233
|
-
if (group)
|
|
234
|
-
group.push(rest);
|
|
235
|
-
else
|
|
236
|
-
groups.set(key, [rest]);
|
|
237
|
-
}
|
|
238
|
-
const out = {};
|
|
239
|
-
for (const [key, rests] of groups) {
|
|
240
|
-
const child = value[key];
|
|
241
|
-
if (child === undefined)
|
|
242
|
-
continue;
|
|
243
|
-
if (rests.some((rest) => rest.length === 0))
|
|
244
|
-
out[key] = child;
|
|
245
|
-
else if (child !== null && typeof child === "object")
|
|
246
|
-
out[key] = project(child, rests);
|
|
242
|
+
/** --format table: print API results as text for a person instead of JSON. Opt-in only; set in main(). */
|
|
243
|
+
let TABLE = false;
|
|
244
|
+
/** An API result on stdout: JSON, or the --format table view of the same value. */
|
|
245
|
+
function printResult(value, resource, collectionField = null) {
|
|
246
|
+
if (!TABLE) {
|
|
247
|
+
out(value);
|
|
248
|
+
return;
|
|
247
249
|
}
|
|
248
|
-
|
|
250
|
+
process.stdout.write(renderTable(value, { width: process.stdout.columns || 120, heading: (text) => paintOut("bold", text), collectionField, resource }));
|
|
251
|
+
}
|
|
252
|
+
/** Whether the command being run writes: an --fields mistake on a write
|
|
253
|
+
* must not tempt anyone into running it again. Set in main(). */
|
|
254
|
+
let FIELDS_AFTER_WRITE = false;
|
|
255
|
+
/** Keep only FIELDS of a result: arrays item by item, objects by dotted path;
|
|
256
|
+
* scalars untouched. A path that matches nothing is an error naming the keys
|
|
257
|
+
* that exist, never a silent {}; one the response schema declares (an
|
|
258
|
+
* optional key no item has) is simply absent. */
|
|
259
|
+
function project(value, perItem, schema) {
|
|
260
|
+
if (FIELDS === null)
|
|
261
|
+
return value;
|
|
262
|
+
const unmatched = unmatchedFields(value, FIELDS, schema);
|
|
263
|
+
if (unmatched.length > 0)
|
|
264
|
+
failUnmatchedFields(unmatched, perItem, value);
|
|
265
|
+
return projectFields(value, FIELDS);
|
|
266
|
+
}
|
|
267
|
+
/** The declared schema of the list a result holds under `key`. */
|
|
268
|
+
function listSchemaOf(schema, key) {
|
|
269
|
+
return schema?.properties?.[key];
|
|
270
|
+
}
|
|
271
|
+
/** --fields over a stream (--all, events): paths no item has matched yet.
|
|
272
|
+
* The stream is printed as it arrives, so the check fails at its end. */
|
|
273
|
+
function streamFieldsCheck(schema) {
|
|
274
|
+
let pending = null;
|
|
275
|
+
return {
|
|
276
|
+
item(value) {
|
|
277
|
+
if (FIELDS === null)
|
|
278
|
+
return value;
|
|
279
|
+
const unmatched = unmatchedFields(value, FIELDS, schema);
|
|
280
|
+
pending = pending === null ? unmatched : pending.filter((p) => unmatched.some((u) => u.path === p.path));
|
|
281
|
+
return projectFields(value, FIELDS);
|
|
282
|
+
},
|
|
283
|
+
finish() {
|
|
284
|
+
if (pending !== null && pending.length > 0)
|
|
285
|
+
failUnmatchedFields(pending, true, undefined);
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function failUnmatchedFields(unmatched, perItem, result) {
|
|
290
|
+
return failWith({
|
|
291
|
+
code: "FIELDS_UNMATCHED",
|
|
292
|
+
message: unmatchedFieldsMessage(unmatched, perItem),
|
|
293
|
+
nextSteps: [
|
|
294
|
+
FIELDS_AFTER_WRITE
|
|
295
|
+
? "This command has already run; do not run it again to change --fields." + (result !== undefined ? " Its full result is in detail.result." : "")
|
|
296
|
+
: result !== undefined ? "The full result is in detail.result; use it rather than running the command again." : "",
|
|
297
|
+
(FIELDS_AFTER_WRITE ? "Next time, use" : "To project a later run, use") + " --fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or omit --fields for the whole result.",
|
|
298
|
+
].filter((step) => step !== ""),
|
|
299
|
+
detail: { unmatched, ...(result !== undefined ? { result } : {}) },
|
|
300
|
+
});
|
|
249
301
|
}
|
|
250
302
|
/** Thrown after scheduling exit so sync callers stop; main() swallows it. */
|
|
251
303
|
class ExitPending extends Error {
|
|
@@ -328,7 +380,7 @@ let USAGE_HINT = BIN + " --help";
|
|
|
328
380
|
function fail(code, message, extra, nextSteps) {
|
|
329
381
|
const usageCode = /^Unknown command/.test(message) ? "UNKNOWN_COMMAND"
|
|
330
382
|
: /^Unknown flag/.test(message) ? "UNKNOWN_FLAG"
|
|
331
|
-
: /^(Missing required|Expected \d+ argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
383
|
+
: /^(Missing required|Expected \d+ (or \d+ )?argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
332
384
|
: "INVALID_USAGE";
|
|
333
385
|
return failWith({
|
|
334
386
|
code: code === 2 ? usageCode : "CALL_FAILED",
|
|
@@ -338,8 +390,23 @@ function fail(code, message, extra, nextSteps) {
|
|
|
338
390
|
});
|
|
339
391
|
}
|
|
340
392
|
/** An SDK error result as an envelope: status-derived code, the API's body as detail, concrete next steps. */
|
|
393
|
+
/** OAuth scopes of the operation being run, so a 403 can name them. */
|
|
394
|
+
let CURRENT_SCOPES = [];
|
|
341
395
|
function failApi(error, hadCredential) {
|
|
342
|
-
return failWith(classifyApiError(error, { bin: BIN, hadCredential, docsUrl: DOCS_URL_DEFAULT }));
|
|
396
|
+
return failWith(classifyAuthFailure(error, authFailureContext()) ?? classifyApiError(error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential, docsUrl: DOCS_URL_DEFAULT, requiredScopes: CURRENT_SCOPES, canLogin: HAS_OAUTH_LOGIN }));
|
|
397
|
+
}
|
|
398
|
+
/** The SDK raises NotModifiedError for a 304: a conditional request
|
|
399
|
+
* matched. For the CLI that is a result, not a failure. */
|
|
400
|
+
function isNotModified(error) {
|
|
401
|
+
return error?.name === "NotModifiedError";
|
|
402
|
+
}
|
|
403
|
+
function printNotModified(error) {
|
|
404
|
+
out({ ok: true, not_modified: true, ...(error.etag ? { etag: error.etag } : {}) });
|
|
405
|
+
return flushExit(0);
|
|
406
|
+
}
|
|
407
|
+
/** What a login, saved-session or credential-store failure names as alternatives. */
|
|
408
|
+
function authFailureContext() {
|
|
409
|
+
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"] : [])] };
|
|
343
410
|
}
|
|
344
411
|
// ---------------------------------------------------------------------------
|
|
345
412
|
// credentials — written by `login`, cleared by `logout`
|
|
@@ -353,7 +420,7 @@ function configDir() { return PROFILE.directory; }
|
|
|
353
420
|
function credsPath() {
|
|
354
421
|
return credentialStore().path;
|
|
355
422
|
}
|
|
356
|
-
function credentialStore() { return createCredentialStore(configDir(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
423
|
+
function credentialStore() { return createCredentialStore(configDir(), BIN, process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
357
424
|
function readCreds() { return credentialStore().read(); }
|
|
358
425
|
function sessionConfiguration(baseUrl, config = readConfig()) {
|
|
359
426
|
return {
|
|
@@ -507,9 +574,11 @@ async function deviceLogin(clientId, parsed) {
|
|
|
507
574
|
fail(2, "Set the API base URL before logging in.");
|
|
508
575
|
const loginConfiguration = sessionConfiguration(apiBaseUrl);
|
|
509
576
|
await credentialStore().prepare();
|
|
577
|
+
if (!OAUTH_DEVICE_URL && !OAUTH_ISSUER && !OAUTH_DISCOVERY_URL)
|
|
578
|
+
fail(2, "This API does not declare device authorization. Run '" + BIN + " login' without --device to sign in through the browser.");
|
|
510
579
|
const session = await withLoginCancellation((signal) => oauthDeviceLogin({
|
|
511
580
|
clientId, issuer: OAUTH_ISSUER, discoveryUrls: OAUTH_DISCOVERY_URLS,
|
|
512
|
-
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes:
|
|
581
|
+
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: loginScopes(parsed.flags),
|
|
513
582
|
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
514
583
|
}, { signal, authorize({ verificationUri, userCode, expiresIn }) {
|
|
515
584
|
process.stderr.write("Open " + paintErr("cyan", verificationUri) + " and enter code: " + paintErr("bold", userCode) + "\n");
|
|
@@ -524,12 +593,43 @@ async function deviceLogin(clientId, parsed) {
|
|
|
524
593
|
out({ ok: true, method: "device", credentials: credsPath(), ...loginIdentityReport() });
|
|
525
594
|
await flushExit(0);
|
|
526
595
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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) {
|
|
599
|
+
const requested = flags.get("scheme");
|
|
600
|
+
if (requested !== undefined) {
|
|
601
|
+
if (typeof requested !== "string" || !Object.hasOwn(NAMED_SCHEMES, requested))
|
|
602
|
+
fail(2, "--scheme expects one of this API's security schemes: " + (Object.keys(NAMED_SCHEMES).join(", ") || "none") + ".");
|
|
603
|
+
const name = requested;
|
|
604
|
+
return { label: name, basic: NAMED_SCHEMES[name].kind === "basic", storedAs: name, save(value) {
|
|
605
|
+
try {
|
|
606
|
+
return { named: parseNamedCredentials({ [name]: value }, NAMED_SCHEMES) };
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
fail(2, error.message);
|
|
610
|
+
}
|
|
611
|
+
} };
|
|
612
|
+
}
|
|
613
|
+
const candidates = [...AUTH_SCALARS.map((a) => a.option), ...(BASIC ? ["basicAuth"] : [])];
|
|
614
|
+
if (!candidates.length)
|
|
615
|
+
return null;
|
|
616
|
+
const uses = (option) => OPS.filter((op) => op.credentialOptions?.some((alternative) => alternative.length === 1 && alternative[0] === option)).length;
|
|
617
|
+
const option = candidates.reduce((best, candidate) => uses(candidate) > uses(best) ? candidate : best);
|
|
618
|
+
if (option === "basicAuth")
|
|
619
|
+
return { label: "username and password", basic: true, storedAs: "basic", save: (value) => ({ basic: value }) };
|
|
620
|
+
const scalar = AUTH_SCALARS.find((a) => a.option === option);
|
|
621
|
+
return { label: scalar.flag.replace(/-/g, " "), basic: false, storedAs: scalar.flag, save: (value) => ({ scalars: { [scalar.option]: value } }) };
|
|
622
|
+
}
|
|
623
|
+
/** Basic credentials on stdin are one line: username:password. */
|
|
624
|
+
function basicFromText(text) {
|
|
625
|
+
const separator = text.indexOf(":");
|
|
626
|
+
if (separator <= 0 || separator === text.length - 1)
|
|
627
|
+
fail(2, "--with-token expects username:password on stdin for Basic auth.");
|
|
628
|
+
return { username: text.slice(0, separator), password: text.slice(separator + 1) };
|
|
629
|
+
}
|
|
630
|
+
async function storePastedToken(value, target, flags) {
|
|
631
|
+
await saveLoginCredentials(target.save(value), flags);
|
|
632
|
+
out({ ok: true, method: "paste", stored_as: target.storedAs, credentials: credsPath(), ...loginIdentityReport() });
|
|
533
633
|
await flushExit(0);
|
|
534
634
|
}
|
|
535
635
|
/**
|
|
@@ -563,10 +663,9 @@ async function browserApprove(headless, flags) {
|
|
|
563
663
|
} }));
|
|
564
664
|
}
|
|
565
665
|
/** Store what the browser approval minted, marked as this CLI's own. */
|
|
566
|
-
async function storeMinted(minted, flags) {
|
|
567
|
-
const first = AUTH_SCALARS[0];
|
|
666
|
+
async function storeMinted(minted, target, flags) {
|
|
568
667
|
await saveLoginCredentials({
|
|
569
|
-
|
|
668
|
+
...target.save(minted.api_key),
|
|
570
669
|
minted: { via: "browser", key_name: minted.key_name, revocationUrl: minted.revocationUrl, ...(minted.org_id ? { org_id: minted.org_id } : {}) },
|
|
571
670
|
}, flags, undefined, async () => {
|
|
572
671
|
try {
|
|
@@ -584,16 +683,38 @@ async function storeMinted(minted, flags) {
|
|
|
584
683
|
}
|
|
585
684
|
});
|
|
586
685
|
}
|
|
587
|
-
async function browserLogin(headless, flags) {
|
|
686
|
+
async function browserLogin(headless, target, flags) {
|
|
588
687
|
await credentialStore().prepare();
|
|
589
688
|
const minted = await browserApprove(headless, flags);
|
|
590
|
-
await storeMinted(minted, flags);
|
|
591
|
-
out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
|
|
689
|
+
await storeMinted(minted, target, flags);
|
|
690
|
+
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() });
|
|
592
691
|
await flushExit(0);
|
|
593
692
|
}
|
|
693
|
+
/** The callback URL browser login uses: the configured redirect URI, else
|
|
694
|
+
* http://127.0.0.1:<port>/callback. --redirect-port or
|
|
695
|
+
* TYPESHIP_OAUTH_REDIRECT_PORT picks another port. */
|
|
696
|
+
function oauthRedirectUri(flags) {
|
|
697
|
+
const requested = typeof flags.get("redirect-port") === "string" ? flags.get("redirect-port") : process.env["TYPESHIP_OAUTH_REDIRECT_PORT"];
|
|
698
|
+
const redirect = new URL(OAUTH_REDIRECT_URI ?? "http://127.0.0.1:" + OAUTH_DEFAULT_REDIRECT_PORT + "/callback");
|
|
699
|
+
if (requested !== undefined) {
|
|
700
|
+
if (!/^[1-9][0-9]{0,4}$/.test(requested) || Number(requested) > 65535)
|
|
701
|
+
fail(2, "--redirect-port expects a port number from 1 to 65535.");
|
|
702
|
+
redirect.port = requested;
|
|
703
|
+
}
|
|
704
|
+
return redirect.href;
|
|
705
|
+
}
|
|
706
|
+
/** --scopes a,b narrows (or widens) what this login asks for. */
|
|
707
|
+
function loginScopes(flags) {
|
|
708
|
+
const requested = flags.get("scopes");
|
|
709
|
+
if (requested === undefined)
|
|
710
|
+
return OAUTH_SCOPES;
|
|
711
|
+
if (typeof requested !== "string" || !requested.trim())
|
|
712
|
+
fail(2, "--scopes expects a comma- or space-separated list of scopes.");
|
|
713
|
+
return [...new Set(requested.split(/[\s,]+/).filter(Boolean))];
|
|
714
|
+
}
|
|
594
715
|
async function acquireOAuthBrowserSession(parsed, clientId) {
|
|
595
|
-
if (!OAUTH_ISSUER)
|
|
596
|
-
fail(2, "Browser OAuth
|
|
716
|
+
if (!OAUTH_ISSUER && !OAUTH_AUTHORIZATION_URL)
|
|
717
|
+
fail(2, "Browser OAuth needs an authorization URL: the API Spec's authorizationCode flow, or auth.oauth_server.issuer configured by the API owner.");
|
|
597
718
|
const apiBaseUrl = resolveBaseUrl(parsed.flags);
|
|
598
719
|
if (!apiBaseUrl)
|
|
599
720
|
fail(2, "Set the API base URL before logging in.");
|
|
@@ -605,7 +726,7 @@ async function acquireOAuthBrowserSession(parsed, clientId) {
|
|
|
605
726
|
configuredClientId: loginConfiguration.clientId ?? null,
|
|
606
727
|
} }, parsed.flags, loginConfiguration);
|
|
607
728
|
}
|
|
608
|
-
/**
|
|
729
|
+
/** The native browser exchange. */
|
|
609
730
|
async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
|
|
610
731
|
const controller = new AbortController();
|
|
611
732
|
const cancel = () => controller.abort();
|
|
@@ -615,7 +736,7 @@ async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
|
|
|
615
736
|
return await oauthBrowserLogin({
|
|
616
737
|
issuer: OAUTH_ISSUER, clientId, discoveryUrl: OAUTH_DISCOVERY_URL,
|
|
617
738
|
authorizationUrl: OAUTH_AUTHORIZATION_URL, tokenUrl: OAUTH_TOKEN_URL ?? undefined,
|
|
618
|
-
redirectUri:
|
|
739
|
+
redirectUri: oauthRedirectUri(parsed.flags), scopes: loginScopes(parsed.flags),
|
|
619
740
|
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
620
741
|
organization: requestedLoginOrganization(parsed.flags),
|
|
621
742
|
}, { signal: controller.signal, timeoutMs, authorize(url) {
|
|
@@ -631,61 +752,22 @@ async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
|
|
|
631
752
|
process.off("SIGTERM", cancel);
|
|
632
753
|
}
|
|
633
754
|
}
|
|
634
|
-
async function cmdConsoleLoginCheck(parsed) {
|
|
635
|
-
const file = parsed.flags.get("console-check");
|
|
636
|
-
if (typeof file !== "string" || !file || parsed.flags.get("device") === true || parsed.flags.get("with-token") === true || explicitNonInteractive(parsed))
|
|
637
|
-
fail(2, "--console-check requires a downloaded JSON file and browser login. Use --no-browser to print the sign-in link.");
|
|
638
|
-
const baseUrl = resolveBaseUrl(parsed.flags);
|
|
639
|
-
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined) ?? process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
|
|
640
|
-
const op = WHOAMI && OPS.find((value) => value.resource === WHOAMI.resource && value.method === WHOAMI.method);
|
|
641
|
-
if (!baseUrl || !OAUTH_ISSUER || !clientId || OAUTH_LOGIN_METHOD !== "browser" || !op || op.auth !== "required" || !op.security?.length)
|
|
642
|
-
fail(2, "Configure browser OAuth and a required-authentication identity read, then regenerate this CLI.");
|
|
643
|
-
const envOptions = {}, flagOptions = {};
|
|
644
|
-
for (const scalar of AUTH_SCALARS) {
|
|
645
|
-
if (process.env[scalar.env] !== undefined)
|
|
646
|
-
envOptions[scalar.option] = process.env[scalar.env];
|
|
647
|
-
if (typeof parsed.flags.get(scalar.flag) === "string")
|
|
648
|
-
flagOptions[scalar.option] = parsed.flags.get(scalar.flag);
|
|
649
|
-
}
|
|
650
|
-
if (BASIC && process.env[BASIC.envUser] && process.env[BASIC.envPass])
|
|
651
|
-
envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
|
|
652
|
-
if (BASIC && typeof parsed.flags.get("username") === "string" && typeof parsed.flags.get("password") === "string")
|
|
653
|
-
flagOptions.basicAuth = { username: parsed.flags.get("username"), password: parsed.flags.get("password") };
|
|
654
|
-
const environment = (typeof parsed.flags.get("environment") === "string" ? parsed.flags.get("environment") : Object.entries(ENVIRONMENTS).find(([, url]) => new URL(url).href === new URL(baseUrl).href)?.[0]) ?? null;
|
|
655
|
-
const result = await checkConsoleBrowserLogin({
|
|
656
|
-
file: file, configuration: {
|
|
657
|
-
baseUrl: baseUrl, environment, operation: op.resource + "." + op.method, requirements: op.security,
|
|
658
|
-
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 },
|
|
659
|
-
issuer: OAUTH_ISSUER, clientId: clientId, discoveryUrl: OAUTH_DISCOVERY_URL ?? null,
|
|
660
|
-
authorizationUrl: OAUTH_AUTHORIZATION_URL ?? null, tokenUrl: OAUTH_TOKEN_URL,
|
|
661
|
-
redirectUri: OAUTH_REDIRECT_URI ?? "http://127.0.0.1/callback", scopes: OAUTH_SCOPES,
|
|
662
|
-
audience: OAUTH_TOKEN_PARAMS.audience ?? null, resource: OAUTH_TOKEN_PARAMS.resource ?? null,
|
|
663
|
-
}, schemes: NAMED_SCHEMES,
|
|
664
|
-
credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ options: envOptions, named: environmentCredentials() }, { options: flagOptions, named: flagCredentials(parsed.flags) }]),
|
|
665
|
-
login: (timeoutMs) => startOAuthBrowserSession(parsed, clientId, timeoutMs),
|
|
666
|
-
async verify(credentials, expectations) {
|
|
667
|
-
const kind = (value) => value === "user" ? "subject" : value;
|
|
668
|
-
const policy = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), entry.pointer]));
|
|
669
|
-
const expected = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), String(entry.expected)]));
|
|
670
|
-
await verifyClientIdentity((options) => new TypeshipClient(options), { baseUrl: baseUrl, credentials }, op, policy, expected);
|
|
671
|
-
},
|
|
672
|
-
progress: (message) => process.stderr.write(message + "\n"),
|
|
673
|
-
});
|
|
674
|
-
out(result);
|
|
675
|
-
await flushExit(0);
|
|
676
|
-
}
|
|
677
755
|
async function cmdLogin(parsed) {
|
|
678
756
|
if (parsed.help) {
|
|
757
|
+
const target = loginTarget(new Map());
|
|
679
758
|
const lines = [
|
|
680
759
|
BIN + " login — store credentials at " + credsPath(),
|
|
681
760
|
"",
|
|
682
761
|
...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
|
|
683
|
-
...(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)"] : []),
|
|
684
|
-
" " + BIN + " login --with-token read the
|
|
685
|
-
...(
|
|
686
|
-
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --
|
|
762
|
+
...(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)"] : []),
|
|
763
|
+
...(target ? [" " + BIN + " login --with-token read the " + target.label + " from stdin" + (target.basic ? " as one username:password line" : "") + " (CI)"] : []),
|
|
764
|
+
...(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"] : []),
|
|
765
|
+
...(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")] : []),
|
|
766
|
+
...(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)"] : []),
|
|
767
|
+
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --scopes <a,b> request these scopes instead of " + (OAUTH_SCOPES.length ? OAUTH_SCOPES.join(" ") : "the provider's defaults")] : []),
|
|
768
|
+
...(HAS_OAUTH_LOGIN && (OAUTH_DEVICE_URL || OAUTH_ISSUER || OAUTH_DISCOVERY_URL) ? [" " + BIN + " login --device use device authorization when your provider supports it"] : []),
|
|
687
769
|
...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
|
|
688
|
-
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
770
|
+
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN || !target ? [] : [" " + BIN + " login interactive prompt" + (target.basic ? " for username and hidden password" : "") + " (TTY only)"]),
|
|
689
771
|
"",
|
|
690
772
|
"Precedence per scheme: flags > env vars > stored credentials. Named inputs beat convenience flags within the same source.",
|
|
691
773
|
" " + BIN + " login --credentials @<JSON-file> store named credentials (use - for stdin)",
|
|
@@ -701,13 +783,9 @@ async function cmdLogin(parsed) {
|
|
|
701
783
|
}
|
|
702
784
|
if (parsed.flags.has("login-organization")) {
|
|
703
785
|
expectedLoginIdentity(parsed.flags);
|
|
704
|
-
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("
|
|
786
|
+
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("with-token") || explicitNonInteractive(parsed))
|
|
705
787
|
fail(2, "--login-organization is available only for interactive OAuth browser login, including --no-browser.");
|
|
706
788
|
}
|
|
707
|
-
if (parsed.flags.has("console-check")) {
|
|
708
|
-
await cmdConsoleLoginCheck(parsed);
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
789
|
const named = flagCredentials(parsed.flags);
|
|
712
790
|
const scalarValues = {};
|
|
713
791
|
for (const a of AUTH_SCALARS) {
|
|
@@ -730,10 +808,13 @@ async function cmdLogin(parsed) {
|
|
|
730
808
|
await flushExit(0);
|
|
731
809
|
}
|
|
732
810
|
if (parsed.flags.get("with-token") === true) {
|
|
811
|
+
const target = loginTarget(parsed.flags);
|
|
812
|
+
if (!target)
|
|
813
|
+
fail(2, "This API declares no credential the CLI can store. See '" + BIN + " login --help'.");
|
|
733
814
|
const token = (await readStdin()).trim();
|
|
734
815
|
if (!token)
|
|
735
816
|
fail(2, "--with-token expects the credential on stdin.");
|
|
736
|
-
await storePastedToken(token, parsed.flags);
|
|
817
|
+
await storePastedToken(target.basic ? basicFromText(token) : token, target, parsed.flags);
|
|
737
818
|
}
|
|
738
819
|
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined)
|
|
739
820
|
?? process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID ?? undefined;
|
|
@@ -748,8 +829,9 @@ async function cmdLogin(parsed) {
|
|
|
748
829
|
// Browser approval: the API mints a key for this CLI once a person
|
|
749
830
|
// approves in the browser. Works under an agent too (it prints the URL and
|
|
750
831
|
// polls); only the explicit non-interactive switch turns it off.
|
|
751
|
-
|
|
752
|
-
|
|
832
|
+
const target = loginTarget(parsed.flags);
|
|
833
|
+
if (CLI_AUTH_URL && target && !target.basic && !explicitNonInteractive(parsed)) {
|
|
834
|
+
await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, target, parsed.flags);
|
|
753
835
|
}
|
|
754
836
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
755
837
|
failWith({
|
|
@@ -758,19 +840,27 @@ async function cmdLogin(parsed) {
|
|
|
758
840
|
message: "login needs a terminal to prompt, and there is none.",
|
|
759
841
|
nextSteps: [
|
|
760
842
|
...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
|
|
761
|
-
"
|
|
843
|
+
...(BASIC ? ["Pass Basic credentials: '" + BIN + " login --username <u> --password <p>', or set " + BASIC.envUser + " and " + BASIC.envPass + " in the environment."] : []),
|
|
844
|
+
...(target ? ["Pipe it: echo \"" + (target.basic ? "$USERNAME:$PASSWORD" : "$TOKEN") + "\" | " + BIN + " login --with-token" + (parsed.flags.has("scheme") ? " --scheme " + target.storedAs : "")] : []),
|
|
762
845
|
...(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)."] : []),
|
|
763
846
|
...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
|
|
764
847
|
],
|
|
765
848
|
});
|
|
766
849
|
}
|
|
767
|
-
|
|
768
|
-
if (!first)
|
|
850
|
+
if (!target)
|
|
769
851
|
fail(2, "This API declares no credential the CLI can prompt for. See '" + BIN + " login --help'.");
|
|
770
|
-
|
|
852
|
+
if (target.basic) {
|
|
853
|
+
process.stderr.write("Username: ");
|
|
854
|
+
const username = (await readLine()).trim();
|
|
855
|
+
const password = await promptHidden("Password (input hidden): ");
|
|
856
|
+
if (!username || !password)
|
|
857
|
+
fail(2, "Enter both a username and a password.");
|
|
858
|
+
await storePastedToken({ username, password }, target, parsed.flags);
|
|
859
|
+
}
|
|
860
|
+
const token = (await promptHidden("Paste " + target.label + " (input hidden): ")).trim();
|
|
771
861
|
if (!token)
|
|
772
862
|
fail(2, "Nothing entered.");
|
|
773
|
-
await storePastedToken(token, parsed.flags);
|
|
863
|
+
await storePastedToken(token, target, parsed.flags);
|
|
774
864
|
}
|
|
775
865
|
async function cmdLogout(parsed) {
|
|
776
866
|
if (parsed.flags.get("local") === true) {
|
|
@@ -784,8 +874,8 @@ async function cmdLogout(parsed) {
|
|
|
784
874
|
// way out, so logging out ends the credential and not just the file. A
|
|
785
875
|
// pasted or CI key is someone else's to revoke, and is left alone.
|
|
786
876
|
let revoked = null;
|
|
787
|
-
|
|
788
|
-
const ownKey =
|
|
877
|
+
// The approval stored exactly one credential, as a scalar or a named scheme.
|
|
878
|
+
const ownKey = stored?.minted?.via === "browser" ? [...Object.values(stored.scalars ?? {}), ...Object.values(stored.named ?? {})].find((value) => typeof value === "string") : undefined;
|
|
789
879
|
if (ownKey) {
|
|
790
880
|
try {
|
|
791
881
|
const response = await oauthStatusRequest(loginEndpoint(stored.minted.revocationUrl), { method: "POST", headers: { Authorization: "Bearer " + ownKey } }, 15_000);
|
|
@@ -940,13 +1030,14 @@ function mcpEntryFor(url, readOnly = false) {
|
|
|
940
1030
|
const warnings = [];
|
|
941
1031
|
const hosted = url ?? MCP_URL ?? undefined;
|
|
942
1032
|
if (hosted) {
|
|
943
|
-
const envVar = AUTH_SCALARS[0]?.env;
|
|
944
1033
|
// The hosted endpoint serves its read-only twin at <url>/readonly; a
|
|
945
1034
|
// remote server of someone else's may not, so say so.
|
|
946
1035
|
const target = readOnly ? hosted.replace(/\/+$/, "") + "/readonly" : hosted;
|
|
947
1036
|
if (readOnly && url !== undefined && !MCP_URL)
|
|
948
|
-
warnings.push("--read-only appended /readonly to the URL
|
|
949
|
-
|
|
1037
|
+
warnings.push("--read-only appended /readonly to the URL; check that this server serves a read-only endpoint there.");
|
|
1038
|
+
if (HOSTED_MCP_NOTE)
|
|
1039
|
+
warnings.push(HOSTED_MCP_NOTE);
|
|
1040
|
+
return { entry: { url: target, ...(Object.keys(HOSTED_MCP_HEADERS).length ? { headers: { ...HOSTED_MCP_HEADERS } } : {}) }, warnings };
|
|
950
1041
|
}
|
|
951
1042
|
if (!HAS_MCP) {
|
|
952
1043
|
fail(2, "This package was generated without the MCP server target. Regenerate with it, or pass --url for a remote endpoint.");
|
|
@@ -1023,7 +1114,7 @@ async function cmdMcp(parsed) {
|
|
|
1023
1114
|
}
|
|
1024
1115
|
if (wanted.size === 0) {
|
|
1025
1116
|
out({
|
|
1026
|
-
server:
|
|
1117
|
+
server: MCP_SERVER_KEY,
|
|
1027
1118
|
entry,
|
|
1028
1119
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
1029
1120
|
detected: MCP_CLIENTS.filter((c) => c.detect(cwd)).map((c) => c.id),
|
|
@@ -1034,11 +1125,11 @@ async function cmdMcp(parsed) {
|
|
|
1034
1125
|
const results = [];
|
|
1035
1126
|
for (const id of wanted) {
|
|
1036
1127
|
const client = findMcpClient(id);
|
|
1037
|
-
results.push(writeMcpConfig(client, cwd,
|
|
1128
|
+
results.push(writeMcpConfig(client, cwd, MCP_SERVER_KEY, entry));
|
|
1038
1129
|
}
|
|
1039
1130
|
out({
|
|
1040
1131
|
ok: true,
|
|
1041
|
-
server:
|
|
1132
|
+
server: MCP_SERVER_KEY,
|
|
1042
1133
|
entry,
|
|
1043
1134
|
written: results.filter((r) => r.written).map((r) => r.file),
|
|
1044
1135
|
clients: results,
|
|
@@ -1058,10 +1149,11 @@ function agentContext() {
|
|
|
1058
1149
|
version: VERSION,
|
|
1059
1150
|
envPrefix: ENV_PREFIX,
|
|
1060
1151
|
authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? ["TYPESHIP_CREDENTIALS"] : [])],
|
|
1152
|
+
...(AUTH_UNDECLARED ? { authNotDeclared: true } : {}),
|
|
1061
1153
|
docsUrl: docsSiteUrl(),
|
|
1062
1154
|
docsIndexUrl: docsIndexUrl(),
|
|
1063
1155
|
generatedOperationCount: OPS.length,
|
|
1064
|
-
|
|
1156
|
+
omittedOperationCount: OMITTED_OPS.length,
|
|
1065
1157
|
mcpUrl: MCP_URL,
|
|
1066
1158
|
skillsRepo: SKILLS_REPO,
|
|
1067
1159
|
hasMcp: HAS_MCP,
|
|
@@ -1082,62 +1174,172 @@ function commandSummaries() {
|
|
|
1082
1174
|
}));
|
|
1083
1175
|
}
|
|
1084
1176
|
/**
|
|
1085
|
-
* help --json:
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1177
|
+
* help --json: discovery as data, bounded so one call cannot fill an agent's
|
|
1178
|
+
* context window on a large API. The default is an index of resources and
|
|
1179
|
+
* command names; "help <resource> --json" pages through one resource's
|
|
1180
|
+
* commands with their methods, paths and summaries; "help <resource>
|
|
1181
|
+
* <command> --json" is one command with its flags; "help --json --all" is
|
|
1182
|
+
* every command with every flag. Full schemas stay behind the docs command.
|
|
1088
1183
|
*/
|
|
1089
|
-
|
|
1184
|
+
const HELP_INDEX_NAMES = 40;
|
|
1185
|
+
const HELP_INDEX_BYTES = 16_000;
|
|
1186
|
+
const HELP_PAGE_SIZE = 50;
|
|
1187
|
+
function helpHeader(detail) {
|
|
1188
|
+
return { schema_version: "3", detail, name: BIN, version: VERSION, api: API_TITLE, api_version: API_VERSION, spec_format: SPEC_FORMAT };
|
|
1189
|
+
}
|
|
1190
|
+
function helpDiscovery() {
|
|
1191
|
+
return {
|
|
1192
|
+
resource: BIN + " help <resource> --json",
|
|
1193
|
+
command: BIN + " help <resource> <command> --json",
|
|
1194
|
+
search: BIN + " docs search <term> --json",
|
|
1195
|
+
operation: BIN + " docs <resource> <command> --json",
|
|
1196
|
+
all: BIN + " help --json --all",
|
|
1197
|
+
note: "Find a command by resource or search, read its flags with help <resource> <command> --json, and its complete schemas with docs. --all prints every command with every flag at once.",
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function helpCoverage() {
|
|
1201
|
+
return EXCLUDED_OPS > 0 ? { coverage: { generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS } } : {};
|
|
1202
|
+
}
|
|
1203
|
+
function opsByResource() {
|
|
1090
1204
|
const byResource = new Map();
|
|
1091
|
-
for (const
|
|
1092
|
-
const list = byResource.get(
|
|
1093
|
-
list.push(
|
|
1094
|
-
byResource.set(
|
|
1205
|
+
for (const op of OPS) {
|
|
1206
|
+
const list = byResource.get(op.command[0]) ?? [];
|
|
1207
|
+
list.push(op);
|
|
1208
|
+
byResource.set(op.command[0], list);
|
|
1095
1209
|
}
|
|
1210
|
+
return byResource;
|
|
1211
|
+
}
|
|
1212
|
+
/** One command with its flags: an entry of help --json --all. */
|
|
1213
|
+
function helpEntry(op, summary) {
|
|
1096
1214
|
return {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1215
|
+
command: op.command[1],
|
|
1216
|
+
method: summary.method,
|
|
1217
|
+
path: summary.path,
|
|
1218
|
+
...(summary.summary ? { summary: summary.summary } : {}),
|
|
1219
|
+
paginated: summary.paginated,
|
|
1220
|
+
safety: op.safety,
|
|
1221
|
+
destructive: summary.destructive,
|
|
1222
|
+
auth: summary.auth,
|
|
1223
|
+
positional: op.params.filter((p) => p.kind === "path").map((p) => p.name),
|
|
1224
|
+
flags: summary.flags,
|
|
1225
|
+
details_command: BIN + " docs " + op.command[0] + " " + op.command[1] + " --json",
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
function helpAll() {
|
|
1229
|
+
const summaries = commandSummaries();
|
|
1230
|
+
const byResource = opsByResource();
|
|
1231
|
+
return {
|
|
1232
|
+
...helpHeader("all"),
|
|
1103
1233
|
usage: BIN + " <resource> <command> [args] [--flags]",
|
|
1104
|
-
resources: [...byResource.entries()].map(([resource,
|
|
1234
|
+
resources: [...byResource.entries()].map(([resource, ops]) => ({
|
|
1105
1235
|
resource,
|
|
1106
|
-
commands:
|
|
1107
|
-
const op = OPS.find((o) => o.command[0] === resource && o.command[1] === c.command);
|
|
1108
|
-
return {
|
|
1109
|
-
command: c.command,
|
|
1110
|
-
method: c.method,
|
|
1111
|
-
path: c.path,
|
|
1112
|
-
...(c.summary ? { summary: c.summary } : {}),
|
|
1113
|
-
paginated: c.paginated,
|
|
1114
|
-
safety: op.safety,
|
|
1115
|
-
destructive: c.destructive,
|
|
1116
|
-
auth: c.auth,
|
|
1117
|
-
positional: op.params.filter((p) => p.kind === "path").map((p) => p.name),
|
|
1118
|
-
flags: c.flags,
|
|
1119
|
-
details_command: BIN + " docs " + resource + " " + c.command + " --json",
|
|
1120
|
-
};
|
|
1121
|
-
}),
|
|
1236
|
+
commands: ops.map((op) => helpEntry(op, summaries[OPS.indexOf(op)])),
|
|
1122
1237
|
})),
|
|
1123
|
-
...(
|
|
1124
|
-
|
|
1125
|
-
generated_operations: OPS.length,
|
|
1126
|
-
total_operations: OPS.length + EXCLUDED_OPS,
|
|
1127
|
-
omitted_operations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
|
|
1128
|
-
reason: "plan_limit",
|
|
1129
|
-
},
|
|
1130
|
-
} : {}),
|
|
1131
|
-
discovery: {
|
|
1132
|
-
search: BIN + " docs search <term> --json",
|
|
1133
|
-
operation: BIN + " docs <resource> <command> --json",
|
|
1134
|
-
note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
|
|
1135
|
-
},
|
|
1238
|
+
...helpCoverage(),
|
|
1239
|
+
discovery: helpDiscovery(),
|
|
1136
1240
|
builtins: BUILTIN_COMMANDS,
|
|
1137
|
-
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>")],
|
|
1241
|
+
global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--header 'Name: value'", "--timeout <seconds>", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--dry-run", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
|
|
1138
1242
|
auth_env_vars: agentContext().authEnvVars,
|
|
1139
1243
|
};
|
|
1140
1244
|
}
|
|
1245
|
+
/** Resources and command names. Past HELP_INDEX_BYTES a resource keeps only its count. */
|
|
1246
|
+
function helpIndex() {
|
|
1247
|
+
const all = helpAll();
|
|
1248
|
+
let bytes = 0;
|
|
1249
|
+
let overBudget = false;
|
|
1250
|
+
let truncated = false;
|
|
1251
|
+
const resources = [...opsByResource().entries()].map(([resource, ops]) => {
|
|
1252
|
+
const names = ops.map((op) => op.command[1]);
|
|
1253
|
+
const entry = { resource, command_count: names.length, commands: names.slice(0, HELP_INDEX_NAMES) };
|
|
1254
|
+
const size = JSON.stringify(entry).length;
|
|
1255
|
+
if (overBudget || bytes + size > HELP_INDEX_BYTES) {
|
|
1256
|
+
overBudget = truncated = true;
|
|
1257
|
+
return { resource, command_count: names.length };
|
|
1258
|
+
}
|
|
1259
|
+
bytes += size;
|
|
1260
|
+
if (names.length > HELP_INDEX_NAMES)
|
|
1261
|
+
truncated = true;
|
|
1262
|
+
return entry;
|
|
1263
|
+
});
|
|
1264
|
+
return {
|
|
1265
|
+
...helpHeader("index"),
|
|
1266
|
+
usage: all.usage,
|
|
1267
|
+
command_count: OPS.length,
|
|
1268
|
+
resource_count: resources.length,
|
|
1269
|
+
resources,
|
|
1270
|
+
...(truncated ? { truncated: "Some resources list only their first " + HELP_INDEX_NAMES + " command names or only a count. Run " + BIN + " help <resource> --json for a resource's commands." } : {}),
|
|
1271
|
+
...helpCoverage(),
|
|
1272
|
+
discovery: all.discovery,
|
|
1273
|
+
builtins: all.builtins,
|
|
1274
|
+
global_flags: all.global_flags,
|
|
1275
|
+
auth_env_vars: all.auth_env_vars,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function helpTarget(resource, method) {
|
|
1279
|
+
const ops = opsByResource().get(resource);
|
|
1280
|
+
if (!ops) {
|
|
1281
|
+
const omitted = omittedCommand(resource, method);
|
|
1282
|
+
if (omitted)
|
|
1283
|
+
failOmitted(omitted);
|
|
1284
|
+
const suggestion = didYouMean(resource, opsByResource().keys());
|
|
1285
|
+
fail(2, "Unknown command: " + resource + "." + (suggestion ? " Did you mean '" + BIN + " help " + suggestion + " --json'?" : ""), undefined, ["Run '" + BIN + " help --json' for the resources and their commands."]);
|
|
1286
|
+
}
|
|
1287
|
+
if (method === undefined)
|
|
1288
|
+
return ops;
|
|
1289
|
+
const op = findOp(resource, method);
|
|
1290
|
+
if (!op) {
|
|
1291
|
+
const omitted = omittedCommand(resource, method);
|
|
1292
|
+
if (omitted)
|
|
1293
|
+
failOmitted(omitted);
|
|
1294
|
+
const suggestion = didYouMean(method, ops.map((o) => o.command[1]));
|
|
1295
|
+
fail(2, "Unknown command: " + resource + " " + method + "." + (suggestion ? " Did you mean '" + BIN + " help " + resource + " " + suggestion + " --json'?" : ""), undefined, ["Run '" + BIN + " help " + resource + " --json' for its commands."]);
|
|
1296
|
+
}
|
|
1297
|
+
return [op];
|
|
1298
|
+
}
|
|
1299
|
+
async function cmdHelpJson(parsed) {
|
|
1300
|
+
const [, resource, method, extra] = parsed.positionals;
|
|
1301
|
+
if (extra !== undefined)
|
|
1302
|
+
fail(2, "help --json takes at most a resource and a command.", undefined, ["Run '" + BIN + " help --json' for the index."]);
|
|
1303
|
+
const all = parsed.flags.get("all") === true;
|
|
1304
|
+
const pageFlag = parsed.flags.get("page");
|
|
1305
|
+
const page = pageFlag === undefined ? 1 : Number(pageFlag);
|
|
1306
|
+
if (!Number.isInteger(page) || page < 1)
|
|
1307
|
+
fail(2, "--page expects a whole number from 1.");
|
|
1308
|
+
if (resource === undefined) {
|
|
1309
|
+
if (pageFlag !== undefined)
|
|
1310
|
+
fail(2, "--page applies to help <resource> --json.");
|
|
1311
|
+
out(all ? helpAll() : helpIndex());
|
|
1312
|
+
await flushExit(0);
|
|
1313
|
+
}
|
|
1314
|
+
const ops = helpTarget(resource, method);
|
|
1315
|
+
const summaries = commandSummaries();
|
|
1316
|
+
if (method !== undefined) {
|
|
1317
|
+
out({ ...helpHeader("command"), resource, ...helpEntry(ops[0], summaries[OPS.indexOf(ops[0])]) });
|
|
1318
|
+
await flushExit(0);
|
|
1319
|
+
}
|
|
1320
|
+
if (all) {
|
|
1321
|
+
out({ ...helpHeader("resource"), resource, command_count: ops.length, commands: ops.map((op) => helpEntry(op, summaries[OPS.indexOf(op)])) });
|
|
1322
|
+
await flushExit(0);
|
|
1323
|
+
}
|
|
1324
|
+
const pages = Math.max(1, Math.ceil(ops.length / HELP_PAGE_SIZE));
|
|
1325
|
+
if (page > pages)
|
|
1326
|
+
fail(2, "--page " + page + " is past the last page (" + pages + ") of " + resource + ".", undefined, ["Run '" + BIN + " help " + resource + " --json' for the first page."]);
|
|
1327
|
+
const commands = ops.slice((page - 1) * HELP_PAGE_SIZE, page * HELP_PAGE_SIZE).map((op) => {
|
|
1328
|
+
const s = summaries[OPS.indexOf(op)];
|
|
1329
|
+
return { command: op.command[1], method: s.method, path: s.path, ...(s.summary ? { summary: s.summary } : {}), paginated: s.paginated, safety: op.safety, positional: op.params.filter((p) => p.kind === "path").map((p) => p.name) };
|
|
1330
|
+
});
|
|
1331
|
+
out({
|
|
1332
|
+
...helpHeader("resource"),
|
|
1333
|
+
resource,
|
|
1334
|
+
command_count: ops.length,
|
|
1335
|
+
page,
|
|
1336
|
+
pages,
|
|
1337
|
+
commands,
|
|
1338
|
+
...(page < pages ? { next_command: BIN + " help " + resource + " --json --page " + (page + 1) } : {}),
|
|
1339
|
+
discovery: { command: BIN + " help " + resource + " <command> --json", operation: BIN + " docs " + resource + " <command> --json", all: BIN + " help " + resource + " --json --all" },
|
|
1340
|
+
});
|
|
1341
|
+
await flushExit(0);
|
|
1342
|
+
}
|
|
1141
1343
|
async function cmdAgentGuide(parsed) {
|
|
1142
1344
|
if (parsed.help) {
|
|
1143
1345
|
process.stdout.write(BIN + " agent-guide [--format json] — how an agent should drive this CLI: conventions, first command, docs, MCP, skills, next steps. JSON.\n");
|
|
@@ -1167,25 +1369,27 @@ async function cmdAuth(parsed) {
|
|
|
1167
1369
|
}
|
|
1168
1370
|
if (parsed.help || sub !== "check") {
|
|
1169
1371
|
process.stdout.write([
|
|
1170
|
-
BIN + " auth check [--
|
|
1372
|
+
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, ...}",
|
|
1171
1373
|
" " + BIN + " auth profiles list profiles without unlocking credentials",
|
|
1172
1374
|
" " + BIN + " auth use <name> select the default profile",
|
|
1173
1375
|
" " + BIN + " auth remove <name> remove a profile after logout",
|
|
1174
1376
|
" --profile <name> overrides TYPESHIP_PROFILE, then the saved selection, then default.",
|
|
1175
|
-
" --
|
|
1377
|
+
" --offline skip the identity read; the credential is then reported as unverified" + (WHOAMI ? "" : " (this API has no identity read, so credentials are always unverified)"),
|
|
1176
1378
|
"",
|
|
1177
1379
|
"Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
|
|
1178
1380
|
].join("\n") + "\n");
|
|
1179
1381
|
await flushExit(parsed.help ? 0 : 2);
|
|
1180
1382
|
}
|
|
1181
1383
|
const source = credentialSource(parsed.flags) ?? "none";
|
|
1182
|
-
const
|
|
1384
|
+
const present = source !== "none";
|
|
1183
1385
|
const savedIdentity = source === "login" ? readCreds() : null;
|
|
1184
1386
|
if (savedIdentity)
|
|
1185
1387
|
assertStoredIdentity(savedIdentity, identityConfiguration());
|
|
1388
|
+
// A credential counts as authenticated only once the API accepted it.
|
|
1186
1389
|
const report = {
|
|
1187
|
-
status:
|
|
1188
|
-
authenticated,
|
|
1390
|
+
status: present ? "ok" : "action_required",
|
|
1391
|
+
authenticated: false,
|
|
1392
|
+
verification: present ? "unverified" : "none",
|
|
1189
1393
|
source,
|
|
1190
1394
|
credentials_path: existsSync(credsPath()) ? credsPath() : null,
|
|
1191
1395
|
credential_storage: credentialStore().backend,
|
|
@@ -1193,12 +1397,13 @@ async function cmdAuth(parsed) {
|
|
|
1193
1397
|
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
1194
1398
|
auth_env_vars: agentContext().authEnvVars,
|
|
1195
1399
|
base_url: resolveBaseUrl(parsed.flags) ?? null,
|
|
1196
|
-
next_steps:
|
|
1400
|
+
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."] : [
|
|
1197
1401
|
...AUTH_SCALARS.map((a) => "Set " + a.env + " in the environment, or run '" + BIN + " login --" + a.flag + " <value>'."),
|
|
1198
|
-
|
|
1402
|
+
...(AUTH_UNDECLARED ? ["The API Spec does not declare authentication. Add another header with --header \"Name: value\" or TYPESHIP_HEADERS."] : []),
|
|
1403
|
+
...(WHOAMI ? ["Then run '" + BIN + " auth check'."] : []),
|
|
1199
1404
|
],
|
|
1200
1405
|
};
|
|
1201
|
-
if (
|
|
1406
|
+
if (present && parsed.flags.get("offline") !== true && WHOAMI) {
|
|
1202
1407
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1203
1408
|
if (op) {
|
|
1204
1409
|
const client = await makeClient(parsed.flags, op);
|
|
@@ -1208,9 +1413,16 @@ async function cmdAuth(parsed) {
|
|
|
1208
1413
|
if (identityConfiguration() && savedIdentity?.identity)
|
|
1209
1414
|
assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
|
|
1210
1415
|
report.identity = result.data;
|
|
1416
|
+
// An identity read that also answers anonymous callers proves nothing.
|
|
1417
|
+
if (op.auth !== "none") {
|
|
1418
|
+
report.authenticated = true;
|
|
1419
|
+
report.verification = "verified";
|
|
1420
|
+
report.next_steps = [];
|
|
1421
|
+
}
|
|
1211
1422
|
}
|
|
1212
1423
|
else {
|
|
1213
|
-
const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1424
|
+
const why = classifyApiError(result.error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1425
|
+
report.verification = "rejected";
|
|
1214
1426
|
report.status = "action_required";
|
|
1215
1427
|
report.live = { ok: false, code: why.code, message: why.message };
|
|
1216
1428
|
report.next_steps = why.nextSteps ?? [];
|
|
@@ -1243,7 +1455,13 @@ async function cmdDoctor(parsed) {
|
|
|
1243
1455
|
if (baseUrl) {
|
|
1244
1456
|
try {
|
|
1245
1457
|
const response = await fetch(baseUrl, { method: "GET", signal: AbortSignal.timeout(8_000) });
|
|
1246
|
-
|
|
1458
|
+
void response.body?.cancel().catch(() => { });
|
|
1459
|
+
const reachable = response.status >= 200 && response.status < 300;
|
|
1460
|
+
checks.push({ name: "base_url", ok: reachable, detail: baseUrl + " → HTTP " + response.status, ...(reachable ? {} : { fix: response.status === 404 || response.status === 405
|
|
1461
|
+
? "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."
|
|
1462
|
+
: response.status === 401 || response.status === 403
|
|
1463
|
+
? "The API answered without a credential with HTTP " + response.status + "; the identity check below tests the credential."
|
|
1464
|
+
: "The API answered HTTP " + response.status + ". Check the base URL and the API's status." }) });
|
|
1247
1465
|
}
|
|
1248
1466
|
catch (e) {
|
|
1249
1467
|
checks.push({ name: "base_url", ok: false, detail: baseUrl + ": " + e.message, fix: "Check the network, or set --base-url / " + ENV_PREFIX + "_BASE_URL." });
|
|
@@ -1259,7 +1477,13 @@ async function cmdDoctor(parsed) {
|
|
|
1259
1477
|
const client = await makeClient(parsed.flags, op);
|
|
1260
1478
|
const target = client[op.resource];
|
|
1261
1479
|
const result = await asApiResult(target[op.method]());
|
|
1262
|
-
|
|
1480
|
+
const status = result.error?.status;
|
|
1481
|
+
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,
|
|
1482
|
+
// Only 401 and 403 are about the credential. A 404 or 405 means the
|
|
1483
|
+
// API no longer has this endpoint where this CLI version expects it.
|
|
1484
|
+
fix: status === 401 || status === 403 ? "The credential was rejected; run '" + BIN + " login' with a current one."
|
|
1485
|
+
: status === 404 || status === 405 ? "The API does not know " + wireOf(op) + ", which this CLI version calls. Run '" + BIN + " upgrade', and check the base URL."
|
|
1486
|
+
: "The identity read failed; the detail says why." });
|
|
1263
1487
|
}
|
|
1264
1488
|
catch (e) {
|
|
1265
1489
|
checks.push({ name: "identity", ok: false, detail: e.message });
|
|
@@ -1273,7 +1497,7 @@ async function cmdDoctor(parsed) {
|
|
|
1273
1497
|
}
|
|
1274
1498
|
if (HAS_MCP || MCP_URL) {
|
|
1275
1499
|
const cwd = process.cwd();
|
|
1276
|
-
const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd,
|
|
1500
|
+
const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd, MCP_SERVER_KEY)).map((c) => c.id);
|
|
1277
1501
|
const detected = MCP_CLIENTS.filter((c) => c.detect(cwd) && !c.incompatible).map((c) => c.id);
|
|
1278
1502
|
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'." } : {}) });
|
|
1279
1503
|
}
|
|
@@ -1312,14 +1536,20 @@ async function cmdInit(parsed) {
|
|
|
1312
1536
|
const first = AUTH_SCALARS[0];
|
|
1313
1537
|
const named = flagCredentials(parsed.flags), envNamed = environmentCredentials();
|
|
1314
1538
|
const stored = Object.keys(named).length || Object.keys(envNamed).length || first && process.env[first.env] ? {} : readCreds() ?? {};
|
|
1315
|
-
|
|
1316
|
-
|
|
1539
|
+
// -k stores the credential login would store; --<flag> stores that scalar.
|
|
1540
|
+
const target = loginTarget(parsed.flags);
|
|
1541
|
+
const keyValue = typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") : undefined;
|
|
1542
|
+
const flagValue = first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) : undefined;
|
|
1317
1543
|
if (Object.keys(named).length) {
|
|
1318
1544
|
await saveLoginCredentials({ named }, parsed.flags);
|
|
1319
1545
|
report.credential = { status: "stored", path: credsPath() };
|
|
1320
1546
|
}
|
|
1321
|
-
else if (
|
|
1322
|
-
await saveLoginCredentials(
|
|
1547
|
+
else if (keyValue && target && !target.basic) {
|
|
1548
|
+
await saveLoginCredentials(target.save(keyValue), parsed.flags);
|
|
1549
|
+
report.credential = { status: "stored", path: credsPath() };
|
|
1550
|
+
}
|
|
1551
|
+
else if ((keyValue ?? flagValue) && first) {
|
|
1552
|
+
await saveLoginCredentials({ scalars: { [first.option]: (keyValue ?? flagValue) } }, parsed.flags);
|
|
1323
1553
|
report.credential = { status: "stored", path: credsPath() };
|
|
1324
1554
|
}
|
|
1325
1555
|
else if (first && process.env[first.env]) {
|
|
@@ -1335,13 +1565,13 @@ async function cmdInit(parsed) {
|
|
|
1335
1565
|
await acquireOAuthBrowserSession(parsed, (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID));
|
|
1336
1566
|
report.credential = { status: "stored", method: "oauth_browser", path: credsPath() };
|
|
1337
1567
|
}
|
|
1338
|
-
else if (
|
|
1568
|
+
else if (target && !target.basic && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
|
|
1339
1569
|
// Nothing anywhere: approve a credential in the browser, as `login`
|
|
1340
1570
|
// would, then carry on. Under an agent the URL is printed for the person
|
|
1341
1571
|
// and polled; only the explicit non-interactive switch skips this.
|
|
1342
1572
|
await credentialStore().prepare();
|
|
1343
1573
|
const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
1344
|
-
await storeMinted(minted, parsed.flags);
|
|
1574
|
+
await storeMinted(minted, target, parsed.flags);
|
|
1345
1575
|
report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
|
|
1346
1576
|
}
|
|
1347
1577
|
else {
|
|
@@ -1363,7 +1593,7 @@ async function cmdInit(parsed) {
|
|
|
1363
1593
|
else {
|
|
1364
1594
|
const { entry, warnings } = mcpEntryFor(undefined);
|
|
1365
1595
|
const clients = MCP_CLIENTS.filter((c) => !c.incompatible && c.detect(cwd) && !(c.id === "claude-desktop" && entry.url));
|
|
1366
|
-
const results = clients.map((c) => writeMcpConfig(c, cwd,
|
|
1596
|
+
const results = clients.map((c) => writeMcpConfig(c, cwd, MCP_SERVER_KEY, entry));
|
|
1367
1597
|
report.mcp = { status: results.length > 0 ? "written" : "no-clients", entry, clients: results, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
1368
1598
|
if (results.length === 0)
|
|
1369
1599
|
nextSteps.push("No MCP client was found on this machine; run '" + BIN + " mcp' to print the entry.");
|
|
@@ -1377,7 +1607,7 @@ async function cmdInit(parsed) {
|
|
|
1377
1607
|
const result = upsertAgentBlock(file, BIN + " agent-contract", agentBlock(agentContext(), commandSummaries()));
|
|
1378
1608
|
report.agents_md = { status: result.updated ? "updated" : "written", file: result.file };
|
|
1379
1609
|
}
|
|
1380
|
-
nextSteps.push("Run '" + BIN + " auth check
|
|
1610
|
+
nextSteps.push("Run '" + BIN + " auth check'" + (WHOAMI ? "" : " (or any read command)") + " to confirm the connection.");
|
|
1381
1611
|
nextSteps.push("Run '" + BIN + " agent-guide' for the conventions, or '" + BIN + " --help' for commands.");
|
|
1382
1612
|
out({ ...report, next_steps: nextSteps });
|
|
1383
1613
|
await flushExit(0);
|
|
@@ -1400,6 +1630,8 @@ function registryBase() {
|
|
|
1400
1630
|
return (process.env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
1401
1631
|
}
|
|
1402
1632
|
async function latestVersion(timeoutMs) {
|
|
1633
|
+
if (!PKG_CONFIRMED)
|
|
1634
|
+
return null;
|
|
1403
1635
|
try {
|
|
1404
1636
|
const response = await fetch(registryBase() + "/" + PKG_NAME, {
|
|
1405
1637
|
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
@@ -1427,6 +1659,9 @@ async function cmdUpgrade(parsed) {
|
|
|
1427
1659
|
process.stdout.write(lines.join("\n") + "\n");
|
|
1428
1660
|
await flushExit(0);
|
|
1429
1661
|
}
|
|
1662
|
+
if (!PKG_CONFIRMED) {
|
|
1663
|
+
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.");
|
|
1664
|
+
}
|
|
1430
1665
|
const latest = await latestVersion(5000);
|
|
1431
1666
|
if (latest === null) {
|
|
1432
1667
|
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.");
|
|
@@ -1480,7 +1715,7 @@ function completionFlagsFor(op) {
|
|
|
1480
1715
|
}
|
|
1481
1716
|
return { flags, values };
|
|
1482
1717
|
}
|
|
1483
|
-
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)];
|
|
1718
|
+
const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--header", "--timeout", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--dry-run", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
|
|
1484
1719
|
const BUILTIN_WORDS = {
|
|
1485
1720
|
config: ["list", "get", "set", "unset", "path"],
|
|
1486
1721
|
completion: ["bash", "zsh", "fish"],
|
|
@@ -1620,40 +1855,6 @@ async function fetchDocs(pathOrFile) {
|
|
|
1620
1855
|
}
|
|
1621
1856
|
}
|
|
1622
1857
|
}
|
|
1623
|
-
function searchTerms(query) {
|
|
1624
|
-
return [...new Set(query.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 2))];
|
|
1625
|
-
}
|
|
1626
|
-
/** Token search across names, prose, paths, and arguments. A phrase such as
|
|
1627
|
-
* "create project" should find the projects create command, even though that exact
|
|
1628
|
-
* substring never occurs in the generated command. */
|
|
1629
|
-
function referenceSearchScore(op, query) {
|
|
1630
|
-
const terms = searchTerms(query);
|
|
1631
|
-
if (terms.length === 0)
|
|
1632
|
-
return 0;
|
|
1633
|
-
const names = [...op.command, op.tool].join("_").toLowerCase().split(/[^a-z0-9]+/);
|
|
1634
|
-
const summary = (op.summary ?? "").toLowerCase();
|
|
1635
|
-
const description = (op.description ?? "").toLowerCase();
|
|
1636
|
-
const path = op.path.toLowerCase();
|
|
1637
|
-
const params = op.params.flatMap((p) => [p.name.toLowerCase(), p.flag.toLowerCase()]);
|
|
1638
|
-
let score = 0;
|
|
1639
|
-
for (const term of terms) {
|
|
1640
|
-
if (names.includes(term))
|
|
1641
|
-
score += 10;
|
|
1642
|
-
if (summary.split(/[^a-z0-9]+/).includes(term))
|
|
1643
|
-
score += 5;
|
|
1644
|
-
else if (summary.includes(term))
|
|
1645
|
-
score += 3;
|
|
1646
|
-
if (path.includes(term))
|
|
1647
|
-
score += 3;
|
|
1648
|
-
if (params.includes(term))
|
|
1649
|
-
score += 3;
|
|
1650
|
-
else if (params.some((param) => param.includes(term)))
|
|
1651
|
-
score += 1;
|
|
1652
|
-
if (description.includes(term))
|
|
1653
|
-
score += 1;
|
|
1654
|
-
}
|
|
1655
|
-
return score;
|
|
1656
|
-
}
|
|
1657
1858
|
function referenceFor(op, includeSchemas = false) {
|
|
1658
1859
|
const lines = [];
|
|
1659
1860
|
lines.push(paintOut("bold", usageLine(op)));
|
|
@@ -1675,7 +1876,9 @@ function referenceFor(op, includeSchemas = false) {
|
|
|
1675
1876
|
lines.push("", paintOut("bold", label + ":"));
|
|
1676
1877
|
for (const p of params) {
|
|
1677
1878
|
const name = p.kind === "path" ? "<" + p.name + ">" : "--" + p.flag;
|
|
1678
|
-
|
|
1879
|
+
// An object flag reads as its named type, which docs read explains.
|
|
1880
|
+
const named = objectTypeName(op, p);
|
|
1881
|
+
lines.push(" " + padPaint("cyan", name, 30) + (named ? INPUT_TYPES.args[op.tool][p.name] : typeLabel(p)) + (p.required ? " " + paintOut("yellow", "(required)") : ""));
|
|
1679
1882
|
const values = p.type === "array" ? p.items?.enum : p.enum;
|
|
1680
1883
|
if (values && values.join("|").length > 24)
|
|
1681
1884
|
lines.push(" one of: " + values.join(", "));
|
|
@@ -1699,10 +1902,31 @@ function referenceFor(op, includeSchemas = false) {
|
|
|
1699
1902
|
lines.push("", paintOut("bold", "Output schema:"), JSON.stringify(op.outputSchema, null, 2));
|
|
1700
1903
|
}
|
|
1701
1904
|
else {
|
|
1905
|
+
const nested = op.params.find((p) => objectTypeName(op, p) || p.type === "object" || (p.type === "array" && p.items?.type === "object"));
|
|
1906
|
+
if (nested) {
|
|
1907
|
+
const named = objectTypeName(op, nested);
|
|
1908
|
+
lines.push("", "Nested fields: " + BIN + " docs " + op.command.join(" ") + " --path " + nested.name + " gives a flag's type and all its fields; extend the path (" + nested.name + ".<field>) to go deeper."
|
|
1909
|
+
+ (named ? " Named types: " + BIN + " docs read " + named + "." : ""));
|
|
1910
|
+
}
|
|
1702
1911
|
lines.push("", "Add --schema for the complete input/output schemas, or --json for the machine contract.");
|
|
1703
1912
|
}
|
|
1704
1913
|
return lines.join("\n");
|
|
1705
1914
|
}
|
|
1915
|
+
/** The named input object a flag takes (IssueFilter), when it has one. */
|
|
1916
|
+
function objectTypeName(op, p) {
|
|
1917
|
+
return namedTypesIn(INPUT_TYPES, INPUT_TYPES.args[op.tool]?.[p.name]).find((name) => INPUT_TYPES.types[name].fields);
|
|
1918
|
+
}
|
|
1919
|
+
function docsTypeHint(name) {
|
|
1920
|
+
return BIN + " docs read " + name;
|
|
1921
|
+
}
|
|
1922
|
+
/** docs --path: one nested argument, or a usage error naming the fields. */
|
|
1923
|
+
async function docsPathExit(root, path) {
|
|
1924
|
+
const found = argumentPathText(INPUT_TYPES, root, path, docsTypeHint);
|
|
1925
|
+
if (!found.ok)
|
|
1926
|
+
fail(2, found.message + (found.available.length > 0 ? " Fields: " + found.available.join(", ") + "." : ""));
|
|
1927
|
+
process.stdout.write(found.text + "\n");
|
|
1928
|
+
return flushExit(0);
|
|
1929
|
+
}
|
|
1706
1930
|
/** Opens a browser for a person; under an agent it prints the URL instead of opening anything. */
|
|
1707
1931
|
function openInBrowser(url, parsed) {
|
|
1708
1932
|
if (parsed && nonInteractive(parsed)) {
|
|
@@ -1721,11 +1945,12 @@ async function cmdDocs(parsed) {
|
|
|
1721
1945
|
"",
|
|
1722
1946
|
" " + BIN + " docs overview",
|
|
1723
1947
|
" " + BIN + " docs <resource> <command> operation contract and example",
|
|
1948
|
+
" --path <a.b> one nested argument's type and fields",
|
|
1724
1949
|
" --schema include input/output JSON Schema",
|
|
1725
1950
|
" --json print the machine contract as JSON",
|
|
1726
1951
|
" " + BIN + " docs search <term> search reference and guides",
|
|
1727
1952
|
" --json / --format json print structured matches and availability",
|
|
1728
|
-
" " + BIN + " docs read <page> print a docs-site page
|
|
1953
|
+
" " + BIN + " docs read <page> print a named input type or a docs-site page",
|
|
1729
1954
|
" " + BIN + " docs --web open the docs site in a browser",
|
|
1730
1955
|
"",
|
|
1731
1956
|
"Guides come from the docs site's llms.txt (set with '" + BIN + " config set docs-url <url>').",
|
|
@@ -1748,23 +1973,22 @@ async function cmdDocs(parsed) {
|
|
|
1748
1973
|
if (!term)
|
|
1749
1974
|
fail(2, "docs search expects a term");
|
|
1750
1975
|
const jsonOutput = parsed.flags.get("json") === true || parsed.flags.get("format") === "json";
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
.sort((a, b) => b.score - a.score || a.op.command.join(" ").localeCompare(b.op.command.join(" ")))
|
|
1754
|
-
.map((match) => match.op);
|
|
1976
|
+
// The MCP server's search_docs ranking, so both surfaces agree.
|
|
1977
|
+
const refMatches = rankOperations(OPS, term).map((match) => match.op);
|
|
1755
1978
|
const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
|
|
1756
1979
|
if (jsonOutput) {
|
|
1757
1980
|
out({
|
|
1758
1981
|
schema_version: "1",
|
|
1759
1982
|
query: term,
|
|
1760
|
-
reference: refMatches.slice(0,
|
|
1983
|
+
reference: refMatches.slice(0, SEARCH_PAGE_SIZE).map((op) => ({
|
|
1761
1984
|
command: op.command.join(" "),
|
|
1762
1985
|
method: op.httpMethod,
|
|
1763
1986
|
path: op.path,
|
|
1764
1987
|
...(op.summary ? { summary: op.summary } : {}),
|
|
1988
|
+
...(op.deprecated ? { deprecated: true } : {}),
|
|
1765
1989
|
details_command: BIN + " docs " + op.command.join(" ") + " --json",
|
|
1766
1990
|
})),
|
|
1767
|
-
guides: proseMatches.slice(0,
|
|
1991
|
+
guides: proseMatches.slice(0, SEARCH_PAGE_SIZE).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
|
|
1768
1992
|
totals: { reference: refMatches.length, guides: proseMatches.length },
|
|
1769
1993
|
guides_status: docsStatus,
|
|
1770
1994
|
...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
|
|
@@ -1775,12 +1999,12 @@ async function cmdDocs(parsed) {
|
|
|
1775
1999
|
const lines = [];
|
|
1776
2000
|
if (refMatches.length > 0) {
|
|
1777
2001
|
lines.push(paintOut("bold", "Reference:"));
|
|
1778
|
-
for (const op of refMatches.slice(0,
|
|
1779
|
-
lines.push(" " + padPaint("cyan", op.command.join(" "), 34) + (op.summary ?? wireOf(op)));
|
|
2002
|
+
for (const op of refMatches.slice(0, SEARCH_PAGE_SIZE))
|
|
2003
|
+
lines.push(" " + padPaint("cyan", op.command.join(" "), 34) + (op.deprecated ? "(deprecated) " : "") + (op.summary ?? wireOf(op)));
|
|
1780
2004
|
}
|
|
1781
2005
|
if (proseMatches.length > 0) {
|
|
1782
2006
|
lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
|
|
1783
|
-
for (const match of proseMatches.slice(0,
|
|
2007
|
+
for (const match of proseMatches.slice(0, SEARCH_PAGE_SIZE))
|
|
1784
2008
|
lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
|
|
1785
2009
|
}
|
|
1786
2010
|
else if (docsStatus === "not_configured") {
|
|
@@ -1798,6 +2022,14 @@ async function cmdDocs(parsed) {
|
|
|
1798
2022
|
const page = parsed.positionals[2];
|
|
1799
2023
|
if (page === undefined)
|
|
1800
2024
|
fail(2, "docs read expects a page path or URL");
|
|
2025
|
+
const typeName = findInputType(INPUT_TYPES, page);
|
|
2026
|
+
if (typeName) {
|
|
2027
|
+
const path = parsed.flags.get("path");
|
|
2028
|
+
if (typeof path === "string" && path.trim())
|
|
2029
|
+
await docsPathExit({ type: typeName }, path);
|
|
2030
|
+
process.stdout.write(inputTypeText(INPUT_TYPES, typeName, docsTypeHint) + "\n");
|
|
2031
|
+
await flushExit(0);
|
|
2032
|
+
}
|
|
1801
2033
|
let target = page;
|
|
1802
2034
|
if (!/^https?:\/\//.test(target)) {
|
|
1803
2035
|
const index = await fetchDocs("llms.txt");
|
|
@@ -1837,11 +2069,14 @@ async function cmdDocs(parsed) {
|
|
|
1837
2069
|
}, null, 2) + "\n");
|
|
1838
2070
|
await flushExit(0);
|
|
1839
2071
|
}
|
|
2072
|
+
const path = parsed.flags.get("path");
|
|
2073
|
+
if (typeof path === "string" && path.trim())
|
|
2074
|
+
await docsPathExit({ tool: op.tool, inputSchema: op.inputSchema, label: op.command.join(" ") }, path);
|
|
1840
2075
|
process.stdout.write(referenceFor(op, parsed.flags.get("schema") === true) + "\n");
|
|
1841
2076
|
await flushExit(0);
|
|
1842
2077
|
}
|
|
1843
2078
|
const lines = [];
|
|
1844
|
-
lines.push(paintOut("bold", "
|
|
2079
|
+
lines.push(paintOut("bold", "Typeship") + " (v" + API_VERSION + ")");
|
|
1845
2080
|
if (API_DESCRIPTION)
|
|
1846
2081
|
lines.push("", API_DESCRIPTION.trim());
|
|
1847
2082
|
lines.push("", paintOut("bold", "Reference:") + " " + BIN + " docs <resource> <command>");
|
|
@@ -1890,7 +2125,7 @@ function shellQuote(value) {
|
|
|
1890
2125
|
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : "'" + value.replace(/'/g, "'\\''") + "'";
|
|
1891
2126
|
}
|
|
1892
2127
|
function usageLine(op) {
|
|
1893
|
-
const paths = op.params.filter((p) => p.kind === "path").map((p) => "<" + p.name + ">").join(" ");
|
|
2128
|
+
const paths = op.params.filter((p) => p.kind === "path").map((p) => p.credential ? "[<" + p.name + ">]" : "<" + p.name + ">").join(" ");
|
|
1894
2129
|
return BIN + " " + op.command[0] + " " + op.command[1] + (paths ? " " + paths : "");
|
|
1895
2130
|
}
|
|
1896
2131
|
/** How the command reaches the wire: "GET /users/{id}", or for GraphQL the
|
|
@@ -1943,7 +2178,7 @@ function typeLabel(p) {
|
|
|
1943
2178
|
if (p.nullable)
|
|
1944
2179
|
return typeLabel({ ...p, nullable: false }) + "|null";
|
|
1945
2180
|
if (p.type === "file")
|
|
1946
|
-
return "path (uploaded)";
|
|
2181
|
+
return p.multiple ? "paths (uploaded, repeatable)" : "path (uploaded)";
|
|
1947
2182
|
if (p.format && p.type === "string")
|
|
1948
2183
|
return p.format;
|
|
1949
2184
|
const inlineEnum = (values) => values && values.join("|").length <= 24 ? values.join("|") : undefined;
|
|
@@ -2022,7 +2257,7 @@ function printRoot(stream = process.stdout) {
|
|
|
2022
2257
|
}
|
|
2023
2258
|
const width = termWidth();
|
|
2024
2259
|
const lines = [];
|
|
2025
|
-
lines.push(paintOut("bold", BIN) + ": " + "
|
|
2260
|
+
lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.24.0");
|
|
2026
2261
|
lines.push("");
|
|
2027
2262
|
lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
|
|
2028
2263
|
lines.push("");
|
|
@@ -2042,18 +2277,19 @@ function printRoot(stream = process.stdout) {
|
|
|
2042
2277
|
}
|
|
2043
2278
|
if (EXCLUDED_OPS > 0) {
|
|
2044
2279
|
lines.push("");
|
|
2045
|
-
lines.push(...labeled(paintOut("yellow", "
|
|
2046
|
-
lines.push(...labeled("Omitted: ", OMITTED_OPS.map((op) => op.command.join(" ") + " (" + op.httpMethod + " " + op.path + ")").join(", "), width, 14));
|
|
2047
|
-
lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
|
|
2280
|
+
lines.push(...labeled(paintOut("yellow", "Coverage:") + " ", "this build includes " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations; api.json lists the rest", width, 14));
|
|
2048
2281
|
}
|
|
2049
2282
|
lines.push("");
|
|
2050
|
-
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)" +
|
|
2283
|
+
const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --header \"Name: value\", --timeout <seconds>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --format table (results as text), --validate (schema-check parameters and JSON bodies), --dry-run (print the request, send nothing)" +
|
|
2051
2284
|
(AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
|
|
2052
2285
|
lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
|
|
2053
2286
|
lines.push(...labeled("Credential env vars: ", [
|
|
2054
2287
|
"TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
|
|
2055
2288
|
...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
|
|
2056
2289
|
].join(", ") || "none", width, 21));
|
|
2290
|
+
if (AUTH_UNDECLARED)
|
|
2291
|
+
lines.push(...labeled("Auth: ", "not declared by the API Spec; " + AUTH_SCALARS[0].env + " is sent as Authorization: Bearer when set", width, 6));
|
|
2292
|
+
lines.push(...labeled("Extra headers: ", "--header \"Name: value\" (repeatable) or TYPESHIP_HEADERS", width, 15));
|
|
2057
2293
|
lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
|
|
2058
2294
|
lines.push(...labeled("Sign-in: ", BIN + " login | logout | whoami | auth check (stored at " + credsPath() + ")", width, 9));
|
|
2059
2295
|
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));
|
|
@@ -2100,6 +2336,8 @@ function commandExtras(op) {
|
|
|
2100
2336
|
extras.push(["--credentials @<file>|-", "named credentials as JSON; use - for stdin"]);
|
|
2101
2337
|
if (op.hasBody && op.bodyKind === "binary")
|
|
2102
2338
|
extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
|
|
2339
|
+
if (op.rawResponse)
|
|
2340
|
+
extras.push(["--output <file>", "write the " + (op.rawResponse === "binary" ? "binary " : "") + "response body to a file (- for stdout) and print what was written"]);
|
|
2103
2341
|
else if (op.hasBody)
|
|
2104
2342
|
extras.push(["--data '<json>'", "raw JSON body" + (op.bodyStyle === "fields" ? " (merged under field flags)" : "") + "; @<file> reads a file, - reads stdin"]);
|
|
2105
2343
|
if (op.select)
|
|
@@ -2110,6 +2348,9 @@ function commandExtras(op) {
|
|
|
2110
2348
|
extras.push(["--fields <a,b.c>", "keep only these fields of the result" + (op.paginated ? " (per item)" : collectionField ? " (per item in " + collectionField + ")" : "")]);
|
|
2111
2349
|
if ((op.fileBundleProperty ?? bundleProperty(op.outputSchema)) !== null)
|
|
2112
2350
|
extras.push(["--out <dir>", "write the response's files ({path, content}) into a directory"]);
|
|
2351
|
+
extras.push(["--dry-run", "print the resolved request (method, URL, headers, body) with credentials redacted; nothing is sent" + (op.safety === "destructive" ? ", so no --force is needed" : "")]);
|
|
2352
|
+
if (!op.rawResponse && !op.sse)
|
|
2353
|
+
extras.push(["--format table", "print the result as a table for reading instead of JSON"]);
|
|
2113
2354
|
if (op.safety === "destructive")
|
|
2114
2355
|
extras.push(["--force, -y", "destructive: required without a terminal, skips the prompt with one"]);
|
|
2115
2356
|
return extras;
|
|
@@ -2118,6 +2359,8 @@ function commandExtras(op) {
|
|
|
2118
2359
|
function exampleLine(op) {
|
|
2119
2360
|
const parts = [BIN, op.command[0], op.command[1]];
|
|
2120
2361
|
for (const p of op.params) {
|
|
2362
|
+
if (p.credential)
|
|
2363
|
+
continue; // defaulted from the configured credential
|
|
2121
2364
|
const hasExample = p.type !== "file" && Object.hasOwn(op.exampleArguments, p.name);
|
|
2122
2365
|
if (!p.required && !hasExample)
|
|
2123
2366
|
continue;
|
|
@@ -2151,6 +2394,8 @@ function exampleLine(op) {
|
|
|
2151
2394
|
}
|
|
2152
2395
|
/** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
|
|
2153
2396
|
function authNote(op) {
|
|
2397
|
+
if (AUTH_UNDECLARED)
|
|
2398
|
+
return op.auth === "none" ? " (no auth needed)" : " (auth not declared)";
|
|
2154
2399
|
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
|
|
2155
2400
|
return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
|
|
2156
2401
|
}
|
|
@@ -2160,6 +2405,9 @@ function printOp(op) {
|
|
|
2160
2405
|
if (op.summary)
|
|
2161
2406
|
lines.push(helpSentence(op.summary));
|
|
2162
2407
|
lines.push(wireOf(op) + authNote(op));
|
|
2408
|
+
const scopes = requiredScopes(op.security);
|
|
2409
|
+
if (scopes.length)
|
|
2410
|
+
lines.push("Requires OAuth scopes: " + scopes.join(", ") + (HAS_OAUTH_LOGIN ? " (login --scopes " + scopes.join(",") + ")" : ""));
|
|
2163
2411
|
lines.push("");
|
|
2164
2412
|
const rows = op.params.filter((p) => p.kind !== "path");
|
|
2165
2413
|
const extras = commandExtras(op);
|
|
@@ -2212,7 +2460,7 @@ function readStdinBytes() {
|
|
|
2212
2460
|
/** A local file as an upload part; the SDK's multipart encoder takes Blobs. */
|
|
2213
2461
|
function fileFromPath(flag, path) {
|
|
2214
2462
|
try {
|
|
2215
|
-
return new File([readFileSync(path)], basename(path));
|
|
2463
|
+
return new File([readFileSync(path)], basename(path), { type: mediaTypeForPath(path) });
|
|
2216
2464
|
}
|
|
2217
2465
|
catch (e) {
|
|
2218
2466
|
return fail(2, "--" + flag + ": cannot read " + path + " (" + e.message + ")");
|
|
@@ -2270,6 +2518,8 @@ function coerce(spec, raw, repeated) {
|
|
|
2270
2518
|
if (spec.type === "file") {
|
|
2271
2519
|
if (raw === true)
|
|
2272
2520
|
fail(2, "--" + spec.flag + " expects a file path");
|
|
2521
|
+
if (spec.multiple)
|
|
2522
|
+
return (repeated ?? [String(raw)]).map((path) => fileFromPath(spec.flag, path));
|
|
2273
2523
|
return fileFromPath(spec.flag, String(raw));
|
|
2274
2524
|
}
|
|
2275
2525
|
if (spec.type === "boolean") {
|
|
@@ -2362,19 +2612,91 @@ function validateParameters(op, values, flags) {
|
|
|
2362
2612
|
}
|
|
2363
2613
|
/** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
|
|
2364
2614
|
let LAST_CLIENT_HAD_CREDENTIAL = false;
|
|
2615
|
+
/** The last client's Basic credentials, for path arguments that default to the username. */
|
|
2616
|
+
let LAST_CLIENT_BASIC = {};
|
|
2617
|
+
/** --dry-run: the client's fetch records the first request and sends
|
|
2618
|
+
* nothing. Set before makeClient; secrets are the credential values it
|
|
2619
|
+
* resolved, redacted wherever they appear in the preview. */
|
|
2620
|
+
let DRY_RUN = null;
|
|
2621
|
+
class DryRunStop extends Error {
|
|
2622
|
+
}
|
|
2623
|
+
function dryRunCapture() {
|
|
2624
|
+
const capture = {
|
|
2625
|
+
secrets: [],
|
|
2626
|
+
fetch: async (input, init) => {
|
|
2627
|
+
if (!capture.request) {
|
|
2628
|
+
const headers = {};
|
|
2629
|
+
new Headers(init?.headers).forEach((value, name) => { headers[name] = value; });
|
|
2630
|
+
capture.request = { method: init?.method ?? "GET", url: typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url, headers, body: init?.body ?? undefined };
|
|
2631
|
+
}
|
|
2632
|
+
throw new DryRunStop("dry run: not sent");
|
|
2633
|
+
},
|
|
2634
|
+
};
|
|
2635
|
+
return capture;
|
|
2636
|
+
}
|
|
2637
|
+
/** Every credential value a client was built with (not a Basic username,
|
|
2638
|
+
* which is an account identifier such as Twilio's AccountSid). */
|
|
2639
|
+
function credentialSecrets(options) {
|
|
2640
|
+
const found = [];
|
|
2641
|
+
const walk = (value, key) => {
|
|
2642
|
+
if (typeof value === "string") {
|
|
2643
|
+
if (key !== "username")
|
|
2644
|
+
found.push(value);
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
if (value && typeof value === "object")
|
|
2648
|
+
for (const [k, v] of Object.entries(value))
|
|
2649
|
+
walk(v, k);
|
|
2650
|
+
};
|
|
2651
|
+
for (const a of AUTH_SCALARS)
|
|
2652
|
+
walk(options[a.option], a.option);
|
|
2653
|
+
walk(options.basicAuth, "basicAuth");
|
|
2654
|
+
walk(options.credentials, "credentials");
|
|
2655
|
+
walk(options.bearerToken, "bearerToken");
|
|
2656
|
+
return found;
|
|
2657
|
+
}
|
|
2658
|
+
/** Drive the command's call until its request reaches the dry-run fetch,
|
|
2659
|
+
* then print that request (JSON in agent mode, text for a person) and exit.
|
|
2660
|
+
* A failure before any request (a --validate violation) is reported as usual. */
|
|
2661
|
+
async function printDryRun(parsed, callResult) {
|
|
2662
|
+
let failure;
|
|
2663
|
+
try {
|
|
2664
|
+
const pending = callResult;
|
|
2665
|
+
if (typeof pending.then === "function")
|
|
2666
|
+
await callResult;
|
|
2667
|
+
else if (typeof pending[Symbol.asyncIterator] === "function")
|
|
2668
|
+
await pending[Symbol.asyncIterator]().next();
|
|
2669
|
+
}
|
|
2670
|
+
catch (e) {
|
|
2671
|
+
failure = e;
|
|
2672
|
+
}
|
|
2673
|
+
const request = DRY_RUN?.request;
|
|
2674
|
+
if (!request) {
|
|
2675
|
+
failApi(failure ?? new Error("--dry-run: the command made no request"), LAST_CLIENT_HAD_CREDENTIAL);
|
|
2676
|
+
}
|
|
2677
|
+
const preview = await requestPreview(request, { sensitiveNames: CREDENTIAL_NAMES, secrets: DRY_RUN.secrets });
|
|
2678
|
+
if (isAgentMode(parsed))
|
|
2679
|
+
out(preview);
|
|
2680
|
+
else
|
|
2681
|
+
process.stdout.write(formatRequestPreview(preview));
|
|
2682
|
+
return await flushExit(0);
|
|
2683
|
+
}
|
|
2684
|
+
/** The configured Basic-auth username: the named scheme's, else basicAuth's. */
|
|
2685
|
+
function credentialUsername(scheme) {
|
|
2686
|
+
const named = LAST_CLIENT_BASIC.credentials?.[scheme];
|
|
2687
|
+
const username = named && typeof named === "object" ? named.username
|
|
2688
|
+
: LAST_CLIENT_BASIC.basicAuth?.username;
|
|
2689
|
+
return typeof username === "string" && username !== "" ? username : undefined;
|
|
2690
|
+
}
|
|
2365
2691
|
/** Check the same complete alternatives the request runtime can select, after
|
|
2366
2692
|
* per-scheme flag, environment, profile, and OAuth resolution. */
|
|
2367
2693
|
function requireOperationCredentials(op, options) {
|
|
2368
2694
|
if (op.auth !== "required")
|
|
2369
2695
|
return;
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
const named = Object.fromEntries(Object.entries(options.credentials ?? {}).filter(([, value]) => supplied(value)));
|
|
2373
|
-
const available = namedCredentialAvailability(NAMED_SCHEMES, new Set(Object.keys(options).filter((key) => supplied(options[key]))), named);
|
|
2374
|
-
if (op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => available.has(option))))
|
|
2696
|
+
const gap = missingCredentials(NAMED_SCHEMES, op.credentialOptions, options);
|
|
2697
|
+
if (!gap)
|
|
2375
2698
|
return;
|
|
2376
|
-
const alternatives
|
|
2377
|
-
const missing = alternatives.map((alternative) => alternative.filter((option) => !available.has(option)).map((option) => option.slice("credentials.".length)));
|
|
2699
|
+
const { alternatives, missing } = gap;
|
|
2378
2700
|
const names = new Set(missing.flat());
|
|
2379
2701
|
const relevant = AUTH_SCALARS.filter((scalar) => [...names].some((name) => NAMED_SCHEMES[name]?.options.includes(scalar.option)));
|
|
2380
2702
|
const needsBasic = [...names].some((name) => NAMED_SCHEMES[name]?.options.includes("basicAuth"));
|
|
@@ -2387,7 +2709,8 @@ function requireOperationCredentials(op, options) {
|
|
|
2387
2709
|
nextSteps: alternatives.length ? [
|
|
2388
2710
|
...relevant.map((a) => "Set " + a.env + " in the environment, pass --" + a.flag + " <value>, or run '" + BIN + " login'."),
|
|
2389
2711
|
...(needsBasic && BASIC ? ["Set " + BASIC.envUser + " and " + BASIC.envPass + ", or pass --username and --password."] : []),
|
|
2390
|
-
|
|
2712
|
+
...(HAS_OAUTH_LOGIN && [...names].some((name) => OAUTH_SESSION_SCHEMES.includes(name)) ? ["Sign in with OAuth: '" + BIN + " login'."] : []),
|
|
2713
|
+
"Supply all schemes in one alternative through " + "TYPESHIP_CREDENTIALS" + " or --credentials @<JSON-file>: " + alternatives.map((alternative) => alternative.join(" + ")).join(" OR ") + ".",
|
|
2391
2714
|
] : ["Check the operation's security schemes in the API Spec and regenerate with a supported, compatible alternative."],
|
|
2392
2715
|
});
|
|
2393
2716
|
}
|
|
@@ -2426,6 +2749,27 @@ function environmentCredentials() {
|
|
|
2426
2749
|
const value = process.env["TYPESHIP_CREDENTIALS"];
|
|
2427
2750
|
return value === undefined ? {} : parseNamedCredentials(value, NAMED_SCHEMES);
|
|
2428
2751
|
}
|
|
2752
|
+
/** --timeout <seconds> or TYPESHIP_TIMEOUT: the per-attempt deadline
|
|
2753
|
+
* (default 60 seconds) for slow operations. */
|
|
2754
|
+
function requestTimeoutMs(flags) {
|
|
2755
|
+
const raw = typeof flags.get("timeout") === "string" ? flags.get("timeout") : process.env["TYPESHIP_TIMEOUT"];
|
|
2756
|
+
if (raw === undefined)
|
|
2757
|
+
return undefined;
|
|
2758
|
+
const seconds = Number(raw);
|
|
2759
|
+
if (!/^\d+(\.\d+)?$/.test(raw.trim()) || !Number.isFinite(seconds) || seconds <= 0 || seconds > 3600)
|
|
2760
|
+
fail(2, "--timeout expects seconds between 0 and 3600, such as --timeout 120.");
|
|
2761
|
+
return Math.round(seconds * 1000);
|
|
2762
|
+
}
|
|
2763
|
+
/** --header flags and TYPESHIP_HEADERS: sent on every API request, after
|
|
2764
|
+
* (and in place of) any generated header of the same name. */
|
|
2765
|
+
function extraRequestHeaders() {
|
|
2766
|
+
try {
|
|
2767
|
+
return parseExtraHeaders(process.env["TYPESHIP_HEADERS"], HEADER_FLAGS, "TYPESHIP_HEADERS");
|
|
2768
|
+
}
|
|
2769
|
+
catch (error) {
|
|
2770
|
+
fail(2, error.message);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2429
2773
|
/** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
|
|
2430
2774
|
function credentialSource(flags) {
|
|
2431
2775
|
if (Object.keys(flagCredentials(flags)).length)
|
|
@@ -2454,7 +2798,31 @@ function resolveBaseUrl(flags, config = readConfig()) {
|
|
|
2454
2798
|
?? (config.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
|
|
2455
2799
|
?? DEFAULT_BASE_URL ?? undefined;
|
|
2456
2800
|
}
|
|
2801
|
+
/** OAuth schemes the login session authenticates by name; empty when the
|
|
2802
|
+
* session is the convenience bearer token (see oauthSessionSchemes). */
|
|
2803
|
+
const OAUTH_SESSION_SCHEMES = oauthSessionSchemes(NAMED_SCHEMES);
|
|
2804
|
+
/** Send a login session to the OAuth scheme, never to a separate http bearer
|
|
2805
|
+
* scheme. Explicit credentials for the same scheme keep precedence. */
|
|
2806
|
+
function applyOAuthSession(options, token, replace = false) {
|
|
2807
|
+
if (!OAUTH_SESSION_SCHEMES.length) {
|
|
2808
|
+
if (replace || options.bearerToken === undefined)
|
|
2809
|
+
options.bearerToken = token;
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
const credentials = (options.credentials ??= {});
|
|
2813
|
+
for (const name of OAUTH_SESSION_SCHEMES)
|
|
2814
|
+
if (replace || !Object.hasOwn(credentials, name))
|
|
2815
|
+
credentials[name] = token;
|
|
2816
|
+
}
|
|
2817
|
+
function withOAuthSession(options, accessToken) {
|
|
2818
|
+
const next = { ...options, credentials: { ...options.credentials } };
|
|
2819
|
+
applyOAuthSession(next, accessToken, true);
|
|
2820
|
+
if (!OAUTH_SESSION_SCHEMES.length)
|
|
2821
|
+
next.credentials = { ...next.credentials, ...resolveNamedCredentials(NAMED_SCHEMES, [{ options: { bearerToken: accessToken } }]) };
|
|
2822
|
+
return next;
|
|
2823
|
+
}
|
|
2457
2824
|
async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
2825
|
+
CURRENT_SCOPES = requiredScopes(op.security);
|
|
2458
2826
|
const flagNamed = flagCredentials(flags), envNamed = environmentCredentials();
|
|
2459
2827
|
const explicitOptions = new Set(AUTH_SCALARS.filter((a) => typeof flags.get(a.flag) === "string" || process.env[a.env] !== undefined).map((a) => a.option));
|
|
2460
2828
|
if (BASIC && (typeof flags.get("username") === "string" || process.env[BASIC.envUser] !== undefined) && (typeof flags.get("password") === "string" || process.env[BASIC.envPass] !== undefined))
|
|
@@ -2502,20 +2870,24 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
|
2502
2870
|
options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
|
|
2503
2871
|
{ named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
|
|
2504
2872
|
{ named: envNamed, options: envOptions }, { named: flagNamed, options: flagOptions },
|
|
2505
|
-
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2873
|
+
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth && !OAUTH_SESSION_SCHEMES.length ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2506
2874
|
]);
|
|
2507
|
-
if (stored?.oauth
|
|
2875
|
+
if (stored?.oauth) {
|
|
2508
2876
|
const sessionId = stored.oauth.sessionId;
|
|
2509
|
-
|
|
2877
|
+
const token = forIdentity ? stored.oauth.accessToken : sessionCredential((rejected) => oauthSessionToken(credentialStore(), {
|
|
2510
2878
|
...sessionConfiguration(baseUrl, config),
|
|
2511
|
-
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken) => verifyClientIdentity((values) => new TypeshipClient(values),
|
|
2512
|
-
}, sessionId, OAUTH_TOKEN_PARAMS);
|
|
2879
|
+
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken) => verifyClientIdentity((values) => new TypeshipClient(values), withOAuthSession(options, accessToken), WHOAMI, IDENTITY_POLICY) } : {}),
|
|
2880
|
+
}, sessionId, OAUTH_TOKEN_PARAMS, rejected));
|
|
2881
|
+
applyOAuthSession(options, token, forIdentity);
|
|
2513
2882
|
}
|
|
2514
2883
|
if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
|
|
2515
2884
|
options.debug = (event) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
|
|
2516
2885
|
}
|
|
2517
2886
|
if (flags.get("validate") === true)
|
|
2518
2887
|
options.validate = true;
|
|
2888
|
+
const timeout = requestTimeoutMs(flags);
|
|
2889
|
+
if (timeout !== undefined)
|
|
2890
|
+
options.timeoutMs = timeout;
|
|
2519
2891
|
for (const g of GLOBALS) {
|
|
2520
2892
|
const flagValue = flags.get(g.flag);
|
|
2521
2893
|
const value = typeof flagValue === "string" ? flagValue : process.env["TYPESHIP_" + g.envSuffix];
|
|
@@ -2523,6 +2895,7 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
|
2523
2895
|
options[g.option] = value;
|
|
2524
2896
|
}
|
|
2525
2897
|
LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
|
|
2898
|
+
LAST_CLIENT_BASIC = { basicAuth: options.basicAuth, credentials: options.credentials };
|
|
2526
2899
|
requireOperationCredentials(op, options);
|
|
2527
2900
|
// Identify the package and version. Optional harness and caller details
|
|
2528
2901
|
// let the API distinguish agent traffic from other non-interactive use.
|
|
@@ -2540,6 +2913,15 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
|
2540
2913
|
options.maxRetries = 0;
|
|
2541
2914
|
options.timeoutMs = 10_000;
|
|
2542
2915
|
}
|
|
2916
|
+
const extraHeaders = extraRequestHeaders();
|
|
2917
|
+
if (Object.keys(extraHeaders).length)
|
|
2918
|
+
options.onRequest = (context) => { applyExtraHeaders(context.headers, extraHeaders); };
|
|
2919
|
+
// --dry-run swaps fetch on this client only: an OAuth refresh (and its
|
|
2920
|
+
// identity check) still uses the real transport, the command does not.
|
|
2921
|
+
if (DRY_RUN && !forIdentity) {
|
|
2922
|
+
DRY_RUN.secrets = credentialSecrets(options);
|
|
2923
|
+
return new TypeshipClient({ ...options, fetch: DRY_RUN.fetch, maxRetries: 0 });
|
|
2924
|
+
}
|
|
2543
2925
|
return new TypeshipClient(options);
|
|
2544
2926
|
}
|
|
2545
2927
|
function editDistance(a, b) {
|
|
@@ -2576,15 +2958,19 @@ function failOmitted(op) {
|
|
|
2576
2958
|
return failWith({
|
|
2577
2959
|
status: "action_required",
|
|
2578
2960
|
code: "PLAN_LIMIT",
|
|
2579
|
-
message: "The command '" + BIN + " " + op.command.join(" ") + "'
|
|
2580
|
-
detail: { operation: op.tool, method: op.httpMethod, path: op.path, generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
|
|
2581
|
-
nextSteps: ["
|
|
2961
|
+
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.",
|
|
2962
|
+
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 },
|
|
2963
|
+
nextSteps: ["The package's publisher can regenerate it with every operation.", "Do not invent or retry an omitted command against this package."],
|
|
2582
2964
|
});
|
|
2583
2965
|
}
|
|
2584
2966
|
const BUILTIN_COMMANDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
|
|
2585
2967
|
async function main() {
|
|
2586
2968
|
const argv = process.argv.slice(2);
|
|
2587
2969
|
const parsed = parseArgv(argv);
|
|
2970
|
+
const headerFlag = parsed.flags.get("header");
|
|
2971
|
+
HEADER_FLAGS = parsed.repeated.get("header") ?? (typeof headerFlag === "string" ? [headerFlag] : []);
|
|
2972
|
+
if (headerFlag === true)
|
|
2973
|
+
fail(2, "--header expects \"Name: value\".");
|
|
2588
2974
|
if (!parsed.help)
|
|
2589
2975
|
validateCredentialsInput(parsed.flags);
|
|
2590
2976
|
const profileFlag = parsed.flags.get("profile");
|
|
@@ -2594,22 +2980,26 @@ async function main() {
|
|
|
2594
2980
|
if (!parsed.help && ["login", "init"].includes(parsed.positionals[0] ?? ""))
|
|
2595
2981
|
expectedLoginIdentity(parsed.flags);
|
|
2596
2982
|
if (parsed.positionals[0] === "help") {
|
|
2597
|
-
// help --json:
|
|
2983
|
+
// help [<resource> [<command>]] --json: bounded discovery as data; --all is exhaustive.
|
|
2598
2984
|
if (parsed.flags.get("json") === true || parsed.flags.get("format") === "json") {
|
|
2599
|
-
|
|
2600
|
-
await flushExit(0);
|
|
2985
|
+
await cmdHelpJson(parsed);
|
|
2601
2986
|
}
|
|
2602
2987
|
parsed.positionals.shift();
|
|
2603
2988
|
parsed.help = true;
|
|
2604
2989
|
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2990
|
+
const formatFlag = parsed.flags.get("format");
|
|
2991
|
+
if (formatFlag !== undefined && formatFlag !== "json" && formatFlag !== "table") {
|
|
2992
|
+
fail(2, "--format takes json (the default) or table.");
|
|
2993
|
+
}
|
|
2994
|
+
TABLE = formatFlag === "table";
|
|
2995
|
+
if (TABLE && !parsed.help && (BUILTIN_COMMANDS.includes(parsed.positionals[0] ?? "") || parsed.positionals.length === 0)) {
|
|
2996
|
+
fail(2, "--format table applies to API commands; " + (parsed.positionals[0] ? BIN + " " + parsed.positionals[0] : BIN) + " prints JSON.");
|
|
2607
2997
|
}
|
|
2608
2998
|
if (parsed.flags.has("version") || parsed.positionals[0] === "version") {
|
|
2609
2999
|
// "acme 1.0.0 (acme 1.0.0)" would say the name twice; when the API's
|
|
2610
3000
|
// title is the bin, name the API version as such.
|
|
2611
3001
|
const apiLabel = API_TITLE.toLowerCase().replace(/[^a-z0-9]/g, "") === BIN.toLowerCase().replace(/[^a-z0-9]/g, "") ? "API " + API_VERSION : API_TITLE + " " + API_VERSION;
|
|
2612
|
-
process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by
|
|
3002
|
+
process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by Typeship)\n");
|
|
2613
3003
|
await flushExit(0);
|
|
2614
3004
|
}
|
|
2615
3005
|
const [resourceCmd, methodCmd] = parsed.positionals;
|
|
@@ -2698,11 +3088,17 @@ async function main() {
|
|
|
2698
3088
|
USAGE_HINT = BIN + " " + op.command[0] + " " + op.command[1] + " --help";
|
|
2699
3089
|
const pathSpecs = op.params.filter((p) => p.kind === "path");
|
|
2700
3090
|
const pathValues = parsed.positionals.slice(2);
|
|
2701
|
-
|
|
2702
|
-
|
|
3091
|
+
// A path argument that is the Basic-auth username (Twilio's AccountSid)
|
|
3092
|
+
// may be left out; it defaults to the configured credential below.
|
|
3093
|
+
const pathGiven = pathValues.length === pathSpecs.length ? pathSpecs
|
|
3094
|
+
: pathValues.length === pathSpecs.filter((p) => !p.credential).length ? pathSpecs.filter((p) => !p.credential)
|
|
3095
|
+
: null;
|
|
3096
|
+
if (!pathGiven) {
|
|
3097
|
+
const optional = pathSpecs.filter((p) => p.credential).length;
|
|
3098
|
+
fail(2, "Expected " + (optional ? (pathSpecs.length - optional) + " or " : "") + pathSpecs.length + " argument(s): " + usageLine(op));
|
|
2703
3099
|
}
|
|
2704
3100
|
const values = {};
|
|
2705
|
-
|
|
3101
|
+
pathGiven.forEach((spec, i) => { values[spec.name] = pathValues[i]; });
|
|
2706
3102
|
let dataBody;
|
|
2707
3103
|
const dataRaw = parsed.flags.get("data");
|
|
2708
3104
|
if (dataRaw === true)
|
|
@@ -2727,7 +3123,7 @@ async function main() {
|
|
|
2727
3123
|
// Mirrors opReservedFlags() in the generator: API parameters never use these
|
|
2728
3124
|
// names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
|
|
2729
3125
|
// check can be exact.
|
|
2730
|
-
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)]);
|
|
3126
|
+
const RESERVED_FLAGS = new Set(["data", "credentials", "header", "timeout", "all", "select", "base-url", "profile", "debug", "validate", "dry-run", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
|
|
2731
3127
|
for (const spec of op.params) {
|
|
2732
3128
|
if (spec.kind === "path")
|
|
2733
3129
|
continue;
|
|
@@ -2738,7 +3134,7 @@ async function main() {
|
|
|
2738
3134
|
// every value (each coerced to the element type); loosely typed (json)
|
|
2739
3135
|
// params become an array of the parsed values.
|
|
2740
3136
|
const all = parsed.repeated.get(spec.flag);
|
|
2741
|
-
values[spec.name] = spec.type === "array"
|
|
3137
|
+
values[spec.name] = spec.type === "array" || (spec.type === "file" && spec.multiple)
|
|
2742
3138
|
? coerce(spec, raw, all)
|
|
2743
3139
|
: all !== undefined && spec.type === "json"
|
|
2744
3140
|
? all.map((v) => coerce(spec, v))
|
|
@@ -2749,11 +3145,39 @@ async function main() {
|
|
|
2749
3145
|
continue;
|
|
2750
3146
|
if (key === "file" && op.bodyKind === "binary")
|
|
2751
3147
|
continue;
|
|
3148
|
+
if (key === "output" && op.rawResponse)
|
|
3149
|
+
continue;
|
|
2752
3150
|
if (!op.params.some((p) => p.flag === key)) {
|
|
2753
3151
|
const suggestion = didYouMean(key, [...op.params.filter((p) => p.kind !== "path").map((p) => p.flag), ...RESERVED_FLAGS]);
|
|
2754
3152
|
fail(2, "Unknown flag --" + key + "." + (suggestion ? " Did you mean --" + suggestion + "?" : ""));
|
|
2755
3153
|
}
|
|
2756
3154
|
}
|
|
3155
|
+
// Object and array values (a GraphQL input, a JSON flag, --data fields)
|
|
3156
|
+
// get the checks the MCP server applies: nested types, enums, required
|
|
3157
|
+
// properties, patterns and a closed object's unknown keys, all at once.
|
|
3158
|
+
const argumentSchemas = (op.inputSchema.properties ?? {});
|
|
3159
|
+
const nestedIssues = [];
|
|
3160
|
+
const checkNested = (name, value) => value !== null && typeof value === "object" && !(value instanceof Blob) && argumentSchemas[name]
|
|
3161
|
+
? checkValue(value, argumentSchemas[name], name, nestedIssues) : value;
|
|
3162
|
+
for (const spec of op.params) {
|
|
3163
|
+
if (spec.kind !== "path" && spec.type !== "file" && values[spec.name] !== undefined)
|
|
3164
|
+
values[spec.name] = checkNested(spec.name, values[spec.name]);
|
|
3165
|
+
}
|
|
3166
|
+
if (op.bodyStyle === "fields" && dataBody !== null && typeof dataBody === "object" && !Array.isArray(dataBody) && !(dataBody instanceof Blob)) {
|
|
3167
|
+
const body = dataBody;
|
|
3168
|
+
for (const name of Object.keys(body)) {
|
|
3169
|
+
if (op.params.some((p) => p.kind === "body" && p.name === name && p.type !== "file"))
|
|
3170
|
+
body[name] = checkNested(name, body[name]);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
if (nestedIssues.length > 0) {
|
|
3174
|
+
failWith({
|
|
3175
|
+
code: nestedIssues.every((issue) => issue.code === "MISSING_ARGUMENT") ? "MISSING_ARGUMENT" : "INVALID_USAGE",
|
|
3176
|
+
message: nestedIssues.length + (nestedIssues.length === 1 ? " problem" : " problems") + " in the arguments; nothing was sent: " + nestedIssues.map((issue) => issue.message).join("; "),
|
|
3177
|
+
detail: { issues: nestedIssues },
|
|
3178
|
+
nextSteps: ["Fix the values listed in detail.issues and run again.", "Run '" + USAGE_HINT + "' for each argument's type."],
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
2757
3181
|
const missing = missingRequired(op, values).filter((name) => !(op.bodyStyle === "fields" && dataBody !== undefined && typeof dataBody === "object" && dataBody !== null && name in dataBody));
|
|
2758
3182
|
if (missing.length > 0)
|
|
2759
3183
|
fail(2, "Missing required: " + missing.map((m) => "--" + (op.params.find((p) => p.name === m)?.flag ?? m)).join(", "));
|
|
@@ -2773,6 +3197,7 @@ async function main() {
|
|
|
2773
3197
|
FIELDS = fieldsRaw.split(",").map((f) => f.trim()).filter((f) => f !== "").map((f) => f.split("."));
|
|
2774
3198
|
if (FIELDS.length === 0)
|
|
2775
3199
|
fail(2, "--fields expects at least one field path");
|
|
3200
|
+
FIELDS_AFTER_WRITE = op.safety !== "read";
|
|
2776
3201
|
}
|
|
2777
3202
|
// --out <dir> materializes a file-shaped response (see cli-agent.ts bundleProperty).
|
|
2778
3203
|
const bundleField = op.fileBundleProperty ?? bundleProperty(op.outputSchema);
|
|
@@ -2782,12 +3207,58 @@ async function main() {
|
|
|
2782
3207
|
fail(2, "--out applies to commands whose response carries files ({path, content}); " + op.command.join(" ") + " does not.");
|
|
2783
3208
|
}
|
|
2784
3209
|
validateParameters(op, values, parsed.flags);
|
|
3210
|
+
// Raw bytes on a terminal are unreadable and can garble it: ask for a
|
|
3211
|
+
// destination before the request runs (it may be billed, like speech).
|
|
3212
|
+
const outputFlag = op.rawResponse ? parsed.flags.get("output") : undefined;
|
|
3213
|
+
if (outputFlag === true)
|
|
3214
|
+
fail(2, "--output expects a file path, or - for stdout.");
|
|
3215
|
+
if (op.rawResponse === "binary" && outputFlag === undefined && process.stdout.isTTY) {
|
|
3216
|
+
fail(2, op.command.join(" ") + " returns binary data. Pass --output <file>, or redirect stdout to a file.");
|
|
3217
|
+
}
|
|
3218
|
+
// --all on a list the generator could not page would print one page and
|
|
3219
|
+
// exit 0, which reads as "that is everything".
|
|
3220
|
+
if (!op.paginated && parsed.flags.get("all") === true) {
|
|
3221
|
+
fail(2, op.command.join(" ") + " does not paginate, so --all has nothing to walk. Run it without --all; it returns the whole response.");
|
|
3222
|
+
}
|
|
3223
|
+
// --dry-run resolves everything a real call would (arguments, defaults,
|
|
3224
|
+
// credentials, the body encoding) and prints the request instead of
|
|
3225
|
+
// sending it. A destructive command needs no --force: nothing runs.
|
|
3226
|
+
if (parsed.flags.get("dry-run") === true) {
|
|
3227
|
+
if (op.paginated && parsed.flags.get("all") === true)
|
|
3228
|
+
fail(2, "--dry-run previews one request; run it without --all to see the first page's.");
|
|
3229
|
+
DRY_RUN = dryRunCapture();
|
|
3230
|
+
}
|
|
3231
|
+
// A table needs the whole result: streams (--all, events) and raw bodies stay as they are.
|
|
3232
|
+
if (TABLE) {
|
|
3233
|
+
const streams = parsed.flags.get("all") === true || op.sse || (op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value);
|
|
3234
|
+
if (streams || op.rawResponse)
|
|
3235
|
+
fail(2, "--format table needs one complete JSON result; " + op.command.join(" ") + (op.rawResponse ? " returns a raw body." : " streams NDJSON here.") + " Drop --format table" + (parsed.flags.get("all") === true ? " or --all." : "."));
|
|
3236
|
+
}
|
|
2785
3237
|
const client = await makeClient(parsed.flags, op);
|
|
3238
|
+
for (const spec of pathSpecs) {
|
|
3239
|
+
if (!spec.credential || values[spec.name] !== undefined)
|
|
3240
|
+
continue;
|
|
3241
|
+
const username = credentialUsername(spec.credential.scheme);
|
|
3242
|
+
if (username === undefined) {
|
|
3243
|
+
fail(2, "Missing required: <" + spec.name + ">. It defaults to the Basic-auth username (" + spec.credential.env + "), and none is configured.", undefined, ["Pass " + spec.name + " as an argument: " + usageLine(op) + ".", "Or set " + spec.credential.env + (BASIC ? " and " + BASIC.envPass : "") + ", or run '" + BIN + " login'."]);
|
|
3244
|
+
}
|
|
3245
|
+
values[spec.name] = username;
|
|
3246
|
+
}
|
|
2786
3247
|
// Destructive commands need --force. A person gets asked; an agent gets
|
|
2787
3248
|
// an action_required envelope with the exact command to run, so nothing
|
|
2788
3249
|
// is deleted on a guess.
|
|
2789
|
-
if (op.safety === "destructive" && !assumeYes(parsed)) {
|
|
2790
|
-
|
|
3250
|
+
if (op.safety === "destructive" && !assumeYes(parsed) && !DRY_RUN) {
|
|
3251
|
+
// Credential flags are replaced by placeholders: the rerun is shown to
|
|
3252
|
+
// agents and logged, so it must never repeat a key.
|
|
3253
|
+
const secretFlags = new Set([...AUTH_SCALARS.map((a) => "--" + a.flag), "--header"]);
|
|
3254
|
+
const rerun = BIN + " " + process.argv.slice(2).map((a, i, all) => {
|
|
3255
|
+
const eq = a.indexOf("=");
|
|
3256
|
+
if (a.startsWith("--") && eq > 0 && secretFlags.has(a.slice(0, eq)))
|
|
3257
|
+
return a.slice(0, eq) + "=<" + a.slice(2, eq) + ">";
|
|
3258
|
+
if (i > 0 && secretFlags.has(all[i - 1]))
|
|
3259
|
+
return "<" + all[i - 1].slice(2) + ">";
|
|
3260
|
+
return /\s/.test(a) ? JSON.stringify(a) : a;
|
|
3261
|
+
}).join(" ") + " --force";
|
|
2791
3262
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
2792
3263
|
failWith({
|
|
2793
3264
|
status: "action_required",
|
|
@@ -2804,19 +3275,32 @@ async function main() {
|
|
|
2804
3275
|
const selectValue = typeof parsed.flags.get("select") === "string" ? parsed.flags.get("select") : undefined;
|
|
2805
3276
|
const args = buildArgs(op, values, dataBody, selectValue);
|
|
2806
3277
|
const target = client[op.resource];
|
|
2807
|
-
|
|
3278
|
+
// --stream true on an operation with a streaming twin prints its events
|
|
3279
|
+
// as NDJSON as they arrive, instead of failing on the event stream.
|
|
3280
|
+
const streaming = op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value;
|
|
3281
|
+
const callResult = target[streaming ? op.streamMethod.method : op.method](...args);
|
|
3282
|
+
if (DRY_RUN)
|
|
3283
|
+
await printDryRun(parsed, callResult);
|
|
2808
3284
|
if (op.paginated && parsed.flags.get("all") === true) {
|
|
3285
|
+
const fieldsCheck = streamFieldsCheck(listSchemaOf(op.outputSchema, "items")?.items);
|
|
2809
3286
|
try {
|
|
2810
3287
|
for await (const item of callResult) {
|
|
2811
|
-
process.stdout.write(JSON.stringify(
|
|
3288
|
+
process.stdout.write(JSON.stringify(fieldsCheck.item(item)) + "\n");
|
|
2812
3289
|
}
|
|
3290
|
+
fieldsCheck.finish();
|
|
2813
3291
|
await flushExit(0);
|
|
2814
3292
|
}
|
|
2815
3293
|
catch (e) {
|
|
3294
|
+
if (isNotModified(e))
|
|
3295
|
+
await printNotModified(e);
|
|
2816
3296
|
failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
|
|
2817
3297
|
}
|
|
2818
3298
|
}
|
|
2819
3299
|
let result = await asApiResult(callResult);
|
|
3300
|
+
// 304 Not Modified: the conditional request matched, so there is no
|
|
3301
|
+
// body. Say so, with the ETag to send next time.
|
|
3302
|
+
if (!result.ok && isNotModified(result.error))
|
|
3303
|
+
await printNotModified(result.error);
|
|
2820
3304
|
if (result.ok && op.httpMethod === "POST" && op.path === "/projects/{project_id}/generate") {
|
|
2821
3305
|
const batch = result.data;
|
|
2822
3306
|
const generations = client.generations;
|
|
@@ -2830,16 +3314,40 @@ async function main() {
|
|
|
2830
3314
|
result = { ...result, data: { ...batch, data: completed } };
|
|
2831
3315
|
}
|
|
2832
3316
|
if (result.ok) {
|
|
2833
|
-
if (op.sse) {
|
|
3317
|
+
if (op.sse || streaming) {
|
|
2834
3318
|
// Server-sent events as NDJSON, one line per event, until the stream ends.
|
|
3319
|
+
const fieldsCheck = streamFieldsCheck(undefined);
|
|
2835
3320
|
try {
|
|
2836
3321
|
for await (const event of result.data) {
|
|
2837
|
-
process.stdout.write(JSON.stringify(
|
|
3322
|
+
process.stdout.write(JSON.stringify(fieldsCheck.item(event)) + "\n");
|
|
2838
3323
|
}
|
|
2839
3324
|
}
|
|
2840
3325
|
catch (e) {
|
|
2841
3326
|
failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
|
|
2842
3327
|
}
|
|
3328
|
+
fieldsCheck.finish();
|
|
3329
|
+
await flushExit(0);
|
|
3330
|
+
}
|
|
3331
|
+
// A non-JSON body (audio, a file, CSV): raw bytes to stdout, or to the
|
|
3332
|
+
// --output file with a JSON note of what was written. Never "{}".
|
|
3333
|
+
if (op.rawResponse || result.data instanceof Blob) {
|
|
3334
|
+
const data = result.data;
|
|
3335
|
+
const bytes = data instanceof Blob
|
|
3336
|
+
? new Uint8Array(await data.arrayBuffer())
|
|
3337
|
+
: new TextEncoder().encode(typeof data === "string" ? data : JSON.stringify(data ?? ""));
|
|
3338
|
+
if (typeof outputFlag === "string" && outputFlag !== "-") {
|
|
3339
|
+
try {
|
|
3340
|
+
writeFileSync(outputFlag, bytes);
|
|
3341
|
+
}
|
|
3342
|
+
catch (e) {
|
|
3343
|
+
fail(1, "--output: cannot write " + outputFlag + " (" + e.message + ")");
|
|
3344
|
+
}
|
|
3345
|
+
const mediaType = data instanceof Blob && data.type ? data.type : op.rawResponse === "text" ? "text/plain" : "application/octet-stream";
|
|
3346
|
+
out({ saved_to: resolvePath(outputFlag), bytes: bytes.length, media_type: mediaType });
|
|
3347
|
+
}
|
|
3348
|
+
else {
|
|
3349
|
+
process.stdout.write(bytes);
|
|
3350
|
+
}
|
|
2843
3351
|
await flushExit(0);
|
|
2844
3352
|
}
|
|
2845
3353
|
// A claim-shaped response (claim.url): say where to claim it, and leave
|
|
@@ -2857,18 +3365,18 @@ async function main() {
|
|
|
2857
3365
|
// agent never has to reconstruct the cursor flag.
|
|
2858
3366
|
const page = result.data;
|
|
2859
3367
|
const next = page.nextPageParams();
|
|
2860
|
-
|
|
2861
|
-
items: project(page.items),
|
|
3368
|
+
printResult({
|
|
3369
|
+
items: project(page.items, true, listSchemaOf(op.outputSchema, "items")),
|
|
2862
3370
|
hasMore: next !== null,
|
|
2863
3371
|
...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
|
|
2864
3372
|
...(page.response.requestId ? { request_id: page.response.requestId } : {}),
|
|
2865
|
-
});
|
|
3373
|
+
}, op.command[0]);
|
|
2866
3374
|
}
|
|
2867
3375
|
else if (FIELDS !== null && collectionField !== null && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) && Array.isArray(result.data[collectionField])) {
|
|
2868
3376
|
// Batch-style collection envelopes ({data: [...]}) use item-relative
|
|
2869
3377
|
// fields, matching paginated results, while retaining envelope metadata.
|
|
2870
3378
|
const data = result.data;
|
|
2871
|
-
|
|
3379
|
+
printResult({ ...data, [collectionField]: project(data[collectionField], true, listSchemaOf(op.outputSchema, collectionField)) }, op.command[0], collectionField);
|
|
2872
3380
|
}
|
|
2873
3381
|
else if (outDir !== undefined && bundleField !== null) {
|
|
2874
3382
|
// --out: a file-shaped response (an array of {path, content}) lands
|
|
@@ -2877,10 +3385,10 @@ async function main() {
|
|
|
2877
3385
|
const files = data[bundleField] ?? [];
|
|
2878
3386
|
const written = writeBundle(outDir, files);
|
|
2879
3387
|
const { [bundleField]: _omitted, ...rest } = data;
|
|
2880
|
-
|
|
3388
|
+
printResult({ ...project(rest, false, op.outputSchema), out: written }, op.command[0]);
|
|
2881
3389
|
}
|
|
2882
3390
|
else {
|
|
2883
|
-
|
|
3391
|
+
printResult(result.data === undefined || result.data === null ? { ok: true } : project(result.data, Array.isArray(result.data), op.outputSchema), op.command[0], collectionField);
|
|
2884
3392
|
}
|
|
2885
3393
|
await flushExit(0);
|
|
2886
3394
|
}
|
|
@@ -2897,6 +3405,9 @@ main().catch((e) => {
|
|
|
2897
3405
|
if (e instanceof ExitPending)
|
|
2898
3406
|
return;
|
|
2899
3407
|
try {
|
|
3408
|
+
const auth = classifyAuthFailure(e, authFailureContext());
|
|
3409
|
+
if (auth)
|
|
3410
|
+
failWith(auth);
|
|
2900
3411
|
fail(1, errorMessage(e));
|
|
2901
3412
|
}
|
|
2902
3413
|
catch {
|