@typeship-ax/cli 0.22.0 → 0.23.1
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 +12 -8
- package/README.md +14 -27
- package/api.json +8898 -9026
- package/api.md +369 -327
- package/dist/arguments.d.ts +47 -0
- package/dist/arguments.d.ts.map +1 -0
- package/dist/arguments.js +254 -0
- package/dist/cli-agent.d.ts +31 -8
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +146 -28
- package/dist/cli.js +483 -237
- 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 +29 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +101 -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 +53 -5
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +49 -40
- 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 +88 -4
- package/dist/resources/deliveries.d.ts.map +1 -1
- package/dist/resources/deliveries.js +95 -18
- package/dist/resources/drafts.d.ts +15 -15
- package/dist/resources/drafts.d.ts.map +1 -1
- package/dist/resources/drafts.js +11 -64
- 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 +14 -14
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +21 -45
- 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 +21 -16
- package/dist/resources/releases.d.ts.map +1 -1
- package/dist/resources/releases.js +18 -39
- package/dist/resources/spec-revisions.d.ts +15 -6
- package/dist/resources/spec-revisions.d.ts.map +1 -1
- package/dist/resources/spec-revisions.js +6 -28
- 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 +48 -48
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +58 -114
- 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/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 +242 -0
- package/src/cli-agent.ts +156 -30
- package/src/cli.ts +444 -211
- 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 +91 -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 +82 -44
- package/src/polling-login.ts +24 -11
- package/src/resources/api-keys.ts +34 -48
- package/src/resources/deliveries.ts +211 -30
- package/src/resources/drafts.ts +60 -107
- package/src/resources/files.ts +19 -20
- package/src/resources/generations.ts +57 -75
- 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 +48 -65
- package/src/resources/spec-revisions.ts +38 -47
- package/src/resources/specs.ts +39 -59
- package/src/resources/targets.ts +143 -193
- package/src/schemas.ts +83 -81
- package/src/search.ts +434 -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,46 @@ 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
24
|
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";
|
|
25
|
+
import { MCP_CLIENTS, requiredScopes, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, classifyAuthFailure, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
|
|
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 { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
|
|
29
31
|
const BIN = "typeship";
|
|
32
|
+
/** The MCP server's key in client configs: the API's name, even when this
|
|
33
|
+
* command was renamed away from a vendor's. It reads this command's login. */
|
|
34
|
+
const MCP_SERVER_KEY = "typeship";
|
|
30
35
|
const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
|
|
31
36
|
const NAMED_SCHEMES = { "apiKey": { "kind": "bearer", "options": ["bearerToken"] } };
|
|
32
37
|
const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESHIP_TOKEN" }];
|
|
38
|
+
/** Hosted MCP request headers as name → env reference, never a literal. */
|
|
39
|
+
const HOSTED_MCP_HEADERS = { "Authorization": "Bearer ${TYPESHIP_TOKEN}" };
|
|
40
|
+
const HOSTED_MCP_NOTE = null;
|
|
33
41
|
const BASIC = null;
|
|
42
|
+
/** The spec declares no security, so the token is offered, never required. */
|
|
43
|
+
const AUTH_UNDECLARED = false;
|
|
44
|
+
/** Repeated --header "Name: value" flags for this invocation. */
|
|
45
|
+
let HEADER_FLAGS = [];
|
|
34
46
|
/** Operations omitted from the generated package by its plan cap. */
|
|
35
47
|
const EXCLUDED_OPS = 0;
|
|
36
48
|
/** Generated CLI operations that are intentionally unavailable to MCP. */
|
|
37
49
|
const MCP_EXCLUDED_OPS = 0;
|
|
38
|
-
const VERSION = "0.
|
|
50
|
+
const VERSION = "0.23.1";
|
|
39
51
|
const API_VERSION = "1.0.0";
|
|
40
52
|
const SPEC_FORMAT = "openapi";
|
|
41
53
|
const IDENTITY_POLICY = {};
|
|
@@ -44,10 +56,13 @@ const WHOAMI = null;
|
|
|
44
56
|
const ENVIRONMENTS = {};
|
|
45
57
|
const HAS_MCP = false;
|
|
46
58
|
const PKG_NAME = "@typeship-ax/cli";
|
|
59
|
+
/** False when the package name was derived rather than chosen: the npm
|
|
60
|
+
* package of that name may be someone else's, so upgrade never installs it. */
|
|
61
|
+
const PKG_CONFIRMED = true;
|
|
47
62
|
const UPDATE_NOTICE = false;
|
|
48
63
|
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 =
|
|
64
|
+
const DOCS_URL_DEFAULT = "https://typeship.dev/docs";
|
|
65
|
+
const DOCS_INDEX_URL_DEFAULT = "https://typeship.dev/llms.txt";
|
|
51
66
|
const RELAY = null;
|
|
52
67
|
const SUPPORT_URL = null;
|
|
53
68
|
const OAUTH_TOKEN_URL = null;
|
|
@@ -57,6 +72,9 @@ const OAUTH_DISCOVERY_URLS = [];
|
|
|
57
72
|
const OAUTH_DEVICE_URL = null;
|
|
58
73
|
const HAS_OAUTH_LOGIN = OAUTH_TOKEN_URL !== null || OAUTH_DISCOVERY_URLS.length > 0;
|
|
59
74
|
const OAUTH_LOGIN_METHOD = "device";
|
|
75
|
+
/** The loopback port browser login listens on unless the redirect URI names
|
|
76
|
+
* one: fixed per CLI, so it can be registered with the provider. */
|
|
77
|
+
const OAUTH_DEFAULT_REDIRECT_PORT = 49910;
|
|
60
78
|
const OAUTH_REDIRECT_URI = undefined;
|
|
61
79
|
const OAUTH_ORGANIZATION_PARAMETER = undefined;
|
|
62
80
|
const OAUTH_AUTHORIZATION_URL = undefined;
|
|
@@ -67,7 +85,7 @@ const MCP_URL = "https://typeship.dev/mcp";
|
|
|
67
85
|
const SKILLS_REPO = "typeship-ax/skills";
|
|
68
86
|
const CLI_AUTH_URL = "https://typeship.dev/api/auth/cli";
|
|
69
87
|
const ENV_PREFIX = "TYPESHIP";
|
|
70
|
-
const API_TITLE = "
|
|
88
|
+
const API_TITLE = "Typeship";
|
|
71
89
|
/** Flags that never take a value, so they don't swallow the next positional
|
|
72
90
|
* (`--non-interactive accounts list`). Built-in commands' own switches
|
|
73
91
|
* (`mcp --cursor`, `upgrade --check`) count only under that command, so an
|
|
@@ -82,7 +100,7 @@ const BUILTIN_BOOLEAN_FLAGS = {
|
|
|
82
100
|
mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
|
|
83
101
|
docs: ["web", "schema"],
|
|
84
102
|
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
|
|
85
|
-
auth: ["live"],
|
|
103
|
+
auth: ["live", "offline"],
|
|
86
104
|
doctor: [],
|
|
87
105
|
};
|
|
88
106
|
function isBooleanFlag(name, positionals) {
|
|
@@ -217,35 +235,47 @@ function out(value) {
|
|
|
217
235
|
}
|
|
218
236
|
/** --fields a,b.c: the dotted paths to keep in API results (null = everything). Set in main(). */
|
|
219
237
|
let FIELDS = null;
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
/** Whether the command being run writes: an --fields mistake on a write
|
|
239
|
+
* must not tempt anyone into running it again. Set in main(). */
|
|
240
|
+
let FIELDS_AFTER_WRITE = false;
|
|
241
|
+
/** Keep only FIELDS of a result: arrays item by item, objects by dotted path;
|
|
242
|
+
* scalars untouched. A path that matches nothing is an error naming the keys
|
|
243
|
+
* that exist, never a silent {}. */
|
|
244
|
+
function project(value, perItem) {
|
|
245
|
+
if (FIELDS === null)
|
|
223
246
|
return value;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return
|
|
247
|
+
const unmatched = unmatchedFields(value, FIELDS);
|
|
248
|
+
if (unmatched.length > 0)
|
|
249
|
+
failUnmatchedFields(unmatched, perItem, value);
|
|
250
|
+
return projectFields(value, FIELDS);
|
|
251
|
+
}
|
|
252
|
+
/** --fields over a stream (--all, events): paths no item has matched yet.
|
|
253
|
+
* The stream is printed as it arrives, so the check fails at its end. */
|
|
254
|
+
function streamFieldsCheck() {
|
|
255
|
+
let pending = null;
|
|
256
|
+
return {
|
|
257
|
+
item(value) {
|
|
258
|
+
if (FIELDS === null)
|
|
259
|
+
return value;
|
|
260
|
+
const unmatched = unmatchedFields(value, FIELDS);
|
|
261
|
+
pending = pending === null ? unmatched : pending.filter((p) => unmatched.some((u) => u.path === p.path));
|
|
262
|
+
return projectFields(value, FIELDS);
|
|
263
|
+
},
|
|
264
|
+
finish() {
|
|
265
|
+
if (pending !== null && pending.length > 0)
|
|
266
|
+
failUnmatchedFields(pending, true, undefined);
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function failUnmatchedFields(unmatched, perItem, result) {
|
|
271
|
+
return failWith({
|
|
272
|
+
code: "FIELDS_UNMATCHED",
|
|
273
|
+
message: unmatchedFieldsMessage(unmatched, perItem),
|
|
274
|
+
nextSteps: FIELDS_AFTER_WRITE
|
|
275
|
+
? ["This command has already run; do not run it again to change --fields." + (result !== undefined ? " Its full result is in detail.result." : "")]
|
|
276
|
+
: ["Run the command again with --fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or without --fields for the whole result."],
|
|
277
|
+
detail: { unmatched, ...(FIELDS_AFTER_WRITE && result !== undefined ? { result } : {}) },
|
|
278
|
+
});
|
|
249
279
|
}
|
|
250
280
|
/** Thrown after scheduling exit so sync callers stop; main() swallows it. */
|
|
251
281
|
class ExitPending extends Error {
|
|
@@ -328,7 +358,7 @@ let USAGE_HINT = BIN + " --help";
|
|
|
328
358
|
function fail(code, message, extra, nextSteps) {
|
|
329
359
|
const usageCode = /^Unknown command/.test(message) ? "UNKNOWN_COMMAND"
|
|
330
360
|
: /^Unknown flag/.test(message) ? "UNKNOWN_FLAG"
|
|
331
|
-
: /^(Missing required|Expected \d+ argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
361
|
+
: /^(Missing required|Expected \d+ (or \d+ )?argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
332
362
|
: "INVALID_USAGE";
|
|
333
363
|
return failWith({
|
|
334
364
|
code: code === 2 ? usageCode : "CALL_FAILED",
|
|
@@ -338,8 +368,23 @@ function fail(code, message, extra, nextSteps) {
|
|
|
338
368
|
});
|
|
339
369
|
}
|
|
340
370
|
/** An SDK error result as an envelope: status-derived code, the API's body as detail, concrete next steps. */
|
|
371
|
+
/** OAuth scopes of the operation being run, so a 403 can name them. */
|
|
372
|
+
let CURRENT_SCOPES = [];
|
|
341
373
|
function failApi(error, hadCredential) {
|
|
342
|
-
return failWith(classifyApiError(error, { bin: BIN, hadCredential, docsUrl: DOCS_URL_DEFAULT }));
|
|
374
|
+
return failWith(classifyAuthFailure(error, authFailureContext()) ?? classifyApiError(error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential, docsUrl: DOCS_URL_DEFAULT, requiredScopes: CURRENT_SCOPES, canLogin: HAS_OAUTH_LOGIN }));
|
|
375
|
+
}
|
|
376
|
+
/** The SDK raises NotModifiedError for a 304: a conditional request
|
|
377
|
+
* matched. For the CLI that is a result, not a failure. */
|
|
378
|
+
function isNotModified(error) {
|
|
379
|
+
return error?.name === "NotModifiedError";
|
|
380
|
+
}
|
|
381
|
+
function printNotModified(error) {
|
|
382
|
+
out({ ok: true, not_modified: true, ...(error.etag ? { etag: error.etag } : {}) });
|
|
383
|
+
return flushExit(0);
|
|
384
|
+
}
|
|
385
|
+
/** What a login, saved-session or credential-store failure names as alternatives. */
|
|
386
|
+
function authFailureContext() {
|
|
387
|
+
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
388
|
}
|
|
344
389
|
// ---------------------------------------------------------------------------
|
|
345
390
|
// credentials — written by `login`, cleared by `logout`
|
|
@@ -353,7 +398,7 @@ function configDir() { return PROFILE.directory; }
|
|
|
353
398
|
function credsPath() {
|
|
354
399
|
return credentialStore().path;
|
|
355
400
|
}
|
|
356
|
-
function credentialStore() { return createCredentialStore(configDir(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
401
|
+
function credentialStore() { return createCredentialStore(configDir(), BIN, process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
357
402
|
function readCreds() { return credentialStore().read(); }
|
|
358
403
|
function sessionConfiguration(baseUrl, config = readConfig()) {
|
|
359
404
|
return {
|
|
@@ -507,9 +552,11 @@ async function deviceLogin(clientId, parsed) {
|
|
|
507
552
|
fail(2, "Set the API base URL before logging in.");
|
|
508
553
|
const loginConfiguration = sessionConfiguration(apiBaseUrl);
|
|
509
554
|
await credentialStore().prepare();
|
|
555
|
+
if (!OAUTH_DEVICE_URL && !OAUTH_ISSUER && !OAUTH_DISCOVERY_URL)
|
|
556
|
+
fail(2, "This API does not declare device authorization. Run '" + BIN + " login' without --device to sign in through the browser.");
|
|
510
557
|
const session = await withLoginCancellation((signal) => oauthDeviceLogin({
|
|
511
558
|
clientId, issuer: OAUTH_ISSUER, discoveryUrls: OAUTH_DISCOVERY_URLS,
|
|
512
|
-
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes:
|
|
559
|
+
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: loginScopes(parsed.flags),
|
|
513
560
|
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
514
561
|
}, { signal, authorize({ verificationUri, userCode, expiresIn }) {
|
|
515
562
|
process.stderr.write("Open " + paintErr("cyan", verificationUri) + " and enter code: " + paintErr("bold", userCode) + "\n");
|
|
@@ -524,12 +571,43 @@ async function deviceLogin(clientId, parsed) {
|
|
|
524
571
|
out({ ok: true, method: "device", credentials: credsPath(), ...loginIdentityReport() });
|
|
525
572
|
await flushExit(0);
|
|
526
573
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
574
|
+
/** The scheme named by --scheme, else the convenience credential that the
|
|
575
|
+
* most operations accept on its own (declared order breaks ties), else null. */
|
|
576
|
+
function loginTarget(flags) {
|
|
577
|
+
const requested = flags.get("scheme");
|
|
578
|
+
if (requested !== undefined) {
|
|
579
|
+
if (typeof requested !== "string" || !Object.hasOwn(NAMED_SCHEMES, requested))
|
|
580
|
+
fail(2, "--scheme expects one of this API's security schemes: " + (Object.keys(NAMED_SCHEMES).join(", ") || "none") + ".");
|
|
581
|
+
const name = requested;
|
|
582
|
+
return { label: name, basic: NAMED_SCHEMES[name].kind === "basic", storedAs: name, save(value) {
|
|
583
|
+
try {
|
|
584
|
+
return { named: parseNamedCredentials({ [name]: value }, NAMED_SCHEMES) };
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
fail(2, error.message);
|
|
588
|
+
}
|
|
589
|
+
} };
|
|
590
|
+
}
|
|
591
|
+
const candidates = [...AUTH_SCALARS.map((a) => a.option), ...(BASIC ? ["basicAuth"] : [])];
|
|
592
|
+
if (!candidates.length)
|
|
593
|
+
return null;
|
|
594
|
+
const uses = (option) => OPS.filter((op) => op.credentialOptions?.some((alternative) => alternative.length === 1 && alternative[0] === option)).length;
|
|
595
|
+
const option = candidates.reduce((best, candidate) => uses(candidate) > uses(best) ? candidate : best);
|
|
596
|
+
if (option === "basicAuth")
|
|
597
|
+
return { label: "username and password", basic: true, storedAs: "basic", save: (value) => ({ basic: value }) };
|
|
598
|
+
const scalar = AUTH_SCALARS.find((a) => a.option === option);
|
|
599
|
+
return { label: scalar.flag.replace(/-/g, " "), basic: false, storedAs: scalar.flag, save: (value) => ({ scalars: { [scalar.option]: value } }) };
|
|
600
|
+
}
|
|
601
|
+
/** Basic credentials on stdin are one line: username:password. */
|
|
602
|
+
function basicFromText(text) {
|
|
603
|
+
const separator = text.indexOf(":");
|
|
604
|
+
if (separator <= 0 || separator === text.length - 1)
|
|
605
|
+
fail(2, "--with-token expects username:password on stdin for Basic auth.");
|
|
606
|
+
return { username: text.slice(0, separator), password: text.slice(separator + 1) };
|
|
607
|
+
}
|
|
608
|
+
async function storePastedToken(value, target, flags) {
|
|
609
|
+
await saveLoginCredentials(target.save(value), flags);
|
|
610
|
+
out({ ok: true, method: "paste", stored_as: target.storedAs, credentials: credsPath(), ...loginIdentityReport() });
|
|
533
611
|
await flushExit(0);
|
|
534
612
|
}
|
|
535
613
|
/**
|
|
@@ -563,10 +641,9 @@ async function browserApprove(headless, flags) {
|
|
|
563
641
|
} }));
|
|
564
642
|
}
|
|
565
643
|
/** Store what the browser approval minted, marked as this CLI's own. */
|
|
566
|
-
async function storeMinted(minted, flags) {
|
|
567
|
-
const first = AUTH_SCALARS[0];
|
|
644
|
+
async function storeMinted(minted, target, flags) {
|
|
568
645
|
await saveLoginCredentials({
|
|
569
|
-
|
|
646
|
+
...target.save(minted.api_key),
|
|
570
647
|
minted: { via: "browser", key_name: minted.key_name, revocationUrl: minted.revocationUrl, ...(minted.org_id ? { org_id: minted.org_id } : {}) },
|
|
571
648
|
}, flags, undefined, async () => {
|
|
572
649
|
try {
|
|
@@ -584,16 +661,38 @@ async function storeMinted(minted, flags) {
|
|
|
584
661
|
}
|
|
585
662
|
});
|
|
586
663
|
}
|
|
587
|
-
async function browserLogin(headless, flags) {
|
|
664
|
+
async function browserLogin(headless, target, flags) {
|
|
588
665
|
await credentialStore().prepare();
|
|
589
666
|
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() });
|
|
667
|
+
await storeMinted(minted, target, flags);
|
|
668
|
+
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
669
|
await flushExit(0);
|
|
593
670
|
}
|
|
671
|
+
/** The callback URL browser login uses: the configured redirect URI, else
|
|
672
|
+
* http://127.0.0.1:<port>/callback. --redirect-port or
|
|
673
|
+
* TYPESHIP_OAUTH_REDIRECT_PORT picks another port. */
|
|
674
|
+
function oauthRedirectUri(flags) {
|
|
675
|
+
const requested = typeof flags.get("redirect-port") === "string" ? flags.get("redirect-port") : process.env["TYPESHIP_OAUTH_REDIRECT_PORT"];
|
|
676
|
+
const redirect = new URL(OAUTH_REDIRECT_URI ?? "http://127.0.0.1:" + OAUTH_DEFAULT_REDIRECT_PORT + "/callback");
|
|
677
|
+
if (requested !== undefined) {
|
|
678
|
+
if (!/^[1-9][0-9]{0,4}$/.test(requested) || Number(requested) > 65535)
|
|
679
|
+
fail(2, "--redirect-port expects a port number from 1 to 65535.");
|
|
680
|
+
redirect.port = requested;
|
|
681
|
+
}
|
|
682
|
+
return redirect.href;
|
|
683
|
+
}
|
|
684
|
+
/** --scopes a,b narrows (or widens) what this login asks for. */
|
|
685
|
+
function loginScopes(flags) {
|
|
686
|
+
const requested = flags.get("scopes");
|
|
687
|
+
if (requested === undefined)
|
|
688
|
+
return OAUTH_SCOPES;
|
|
689
|
+
if (typeof requested !== "string" || !requested.trim())
|
|
690
|
+
fail(2, "--scopes expects a comma- or space-separated list of scopes.");
|
|
691
|
+
return [...new Set(requested.split(/[\s,]+/).filter(Boolean))];
|
|
692
|
+
}
|
|
594
693
|
async function acquireOAuthBrowserSession(parsed, clientId) {
|
|
595
|
-
if (!OAUTH_ISSUER)
|
|
596
|
-
fail(2, "Browser OAuth
|
|
694
|
+
if (!OAUTH_ISSUER && !OAUTH_AUTHORIZATION_URL)
|
|
695
|
+
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
696
|
const apiBaseUrl = resolveBaseUrl(parsed.flags);
|
|
598
697
|
if (!apiBaseUrl)
|
|
599
698
|
fail(2, "Set the API base URL before logging in.");
|
|
@@ -605,7 +704,7 @@ async function acquireOAuthBrowserSession(parsed, clientId) {
|
|
|
605
704
|
configuredClientId: loginConfiguration.clientId ?? null,
|
|
606
705
|
} }, parsed.flags, loginConfiguration);
|
|
607
706
|
}
|
|
608
|
-
/**
|
|
707
|
+
/** The native browser exchange. */
|
|
609
708
|
async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
|
|
610
709
|
const controller = new AbortController();
|
|
611
710
|
const cancel = () => controller.abort();
|
|
@@ -615,7 +714,7 @@ async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
|
|
|
615
714
|
return await oauthBrowserLogin({
|
|
616
715
|
issuer: OAUTH_ISSUER, clientId, discoveryUrl: OAUTH_DISCOVERY_URL,
|
|
617
716
|
authorizationUrl: OAUTH_AUTHORIZATION_URL, tokenUrl: OAUTH_TOKEN_URL ?? undefined,
|
|
618
|
-
redirectUri:
|
|
717
|
+
redirectUri: oauthRedirectUri(parsed.flags), scopes: loginScopes(parsed.flags),
|
|
619
718
|
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
620
719
|
organization: requestedLoginOrganization(parsed.flags),
|
|
621
720
|
}, { signal: controller.signal, timeoutMs, authorize(url) {
|
|
@@ -631,61 +730,22 @@ async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
|
|
|
631
730
|
process.off("SIGTERM", cancel);
|
|
632
731
|
}
|
|
633
732
|
}
|
|
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
733
|
async function cmdLogin(parsed) {
|
|
678
734
|
if (parsed.help) {
|
|
735
|
+
const target = loginTarget(new Map());
|
|
679
736
|
const lines = [
|
|
680
737
|
BIN + " login — store credentials at " + credsPath(),
|
|
681
738
|
"",
|
|
682
739
|
...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 --
|
|
740
|
+
...(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)"] : []),
|
|
741
|
+
...(target ? [" " + BIN + " login --with-token read the " + target.label + " from stdin" + (target.basic ? " as one username:password line" : "") + " (CI)"] : []),
|
|
742
|
+
...(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"] : []),
|
|
743
|
+
...(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")] : []),
|
|
744
|
+
...(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)"] : []),
|
|
745
|
+
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --scopes <a,b> request these scopes instead of " + (OAUTH_SCOPES.length ? OAUTH_SCOPES.join(" ") : "the provider's defaults")] : []),
|
|
746
|
+
...(HAS_OAUTH_LOGIN && (OAUTH_DEVICE_URL || OAUTH_ISSUER || OAUTH_DISCOVERY_URL) ? [" " + BIN + " login --device use device authorization when your provider supports it"] : []),
|
|
687
747
|
...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
|
|
688
|
-
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
748
|
+
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN || !target ? [] : [" " + BIN + " login interactive prompt" + (target.basic ? " for username and hidden password" : "") + " (TTY only)"]),
|
|
689
749
|
"",
|
|
690
750
|
"Precedence per scheme: flags > env vars > stored credentials. Named inputs beat convenience flags within the same source.",
|
|
691
751
|
" " + BIN + " login --credentials @<JSON-file> store named credentials (use - for stdin)",
|
|
@@ -701,13 +761,9 @@ async function cmdLogin(parsed) {
|
|
|
701
761
|
}
|
|
702
762
|
if (parsed.flags.has("login-organization")) {
|
|
703
763
|
expectedLoginIdentity(parsed.flags);
|
|
704
|
-
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("
|
|
764
|
+
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("with-token") || explicitNonInteractive(parsed))
|
|
705
765
|
fail(2, "--login-organization is available only for interactive OAuth browser login, including --no-browser.");
|
|
706
766
|
}
|
|
707
|
-
if (parsed.flags.has("console-check")) {
|
|
708
|
-
await cmdConsoleLoginCheck(parsed);
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
767
|
const named = flagCredentials(parsed.flags);
|
|
712
768
|
const scalarValues = {};
|
|
713
769
|
for (const a of AUTH_SCALARS) {
|
|
@@ -730,10 +786,13 @@ async function cmdLogin(parsed) {
|
|
|
730
786
|
await flushExit(0);
|
|
731
787
|
}
|
|
732
788
|
if (parsed.flags.get("with-token") === true) {
|
|
789
|
+
const target = loginTarget(parsed.flags);
|
|
790
|
+
if (!target)
|
|
791
|
+
fail(2, "This API declares no credential the CLI can store. See '" + BIN + " login --help'.");
|
|
733
792
|
const token = (await readStdin()).trim();
|
|
734
793
|
if (!token)
|
|
735
794
|
fail(2, "--with-token expects the credential on stdin.");
|
|
736
|
-
await storePastedToken(token, parsed.flags);
|
|
795
|
+
await storePastedToken(target.basic ? basicFromText(token) : token, target, parsed.flags);
|
|
737
796
|
}
|
|
738
797
|
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined)
|
|
739
798
|
?? process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID ?? undefined;
|
|
@@ -748,8 +807,9 @@ async function cmdLogin(parsed) {
|
|
|
748
807
|
// Browser approval: the API mints a key for this CLI once a person
|
|
749
808
|
// approves in the browser. Works under an agent too (it prints the URL and
|
|
750
809
|
// polls); only the explicit non-interactive switch turns it off.
|
|
751
|
-
|
|
752
|
-
|
|
810
|
+
const target = loginTarget(parsed.flags);
|
|
811
|
+
if (CLI_AUTH_URL && target && !target.basic && !explicitNonInteractive(parsed)) {
|
|
812
|
+
await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, target, parsed.flags);
|
|
753
813
|
}
|
|
754
814
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
755
815
|
failWith({
|
|
@@ -758,19 +818,27 @@ async function cmdLogin(parsed) {
|
|
|
758
818
|
message: "login needs a terminal to prompt, and there is none.",
|
|
759
819
|
nextSteps: [
|
|
760
820
|
...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
|
|
761
|
-
"
|
|
821
|
+
...(BASIC ? ["Pass Basic credentials: '" + BIN + " login --username <u> --password <p>', or set " + BASIC.envUser + " and " + BASIC.envPass + " in the environment."] : []),
|
|
822
|
+
...(target ? ["Pipe it: echo \"" + (target.basic ? "$USERNAME:$PASSWORD" : "$TOKEN") + "\" | " + BIN + " login --with-token" + (parsed.flags.has("scheme") ? " --scheme " + target.storedAs : "")] : []),
|
|
762
823
|
...(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
824
|
...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
|
|
764
825
|
],
|
|
765
826
|
});
|
|
766
827
|
}
|
|
767
|
-
|
|
768
|
-
if (!first)
|
|
828
|
+
if (!target)
|
|
769
829
|
fail(2, "This API declares no credential the CLI can prompt for. See '" + BIN + " login --help'.");
|
|
770
|
-
|
|
830
|
+
if (target.basic) {
|
|
831
|
+
process.stderr.write("Username: ");
|
|
832
|
+
const username = (await readLine()).trim();
|
|
833
|
+
const password = await promptHidden("Password (input hidden): ");
|
|
834
|
+
if (!username || !password)
|
|
835
|
+
fail(2, "Enter both a username and a password.");
|
|
836
|
+
await storePastedToken({ username, password }, target, parsed.flags);
|
|
837
|
+
}
|
|
838
|
+
const token = (await promptHidden("Paste " + target.label + " (input hidden): ")).trim();
|
|
771
839
|
if (!token)
|
|
772
840
|
fail(2, "Nothing entered.");
|
|
773
|
-
await storePastedToken(token, parsed.flags);
|
|
841
|
+
await storePastedToken(token, target, parsed.flags);
|
|
774
842
|
}
|
|
775
843
|
async function cmdLogout(parsed) {
|
|
776
844
|
if (parsed.flags.get("local") === true) {
|
|
@@ -784,8 +852,8 @@ async function cmdLogout(parsed) {
|
|
|
784
852
|
// way out, so logging out ends the credential and not just the file. A
|
|
785
853
|
// pasted or CI key is someone else's to revoke, and is left alone.
|
|
786
854
|
let revoked = null;
|
|
787
|
-
|
|
788
|
-
const ownKey =
|
|
855
|
+
// The approval stored exactly one credential, as a scalar or a named scheme.
|
|
856
|
+
const ownKey = stored?.minted?.via === "browser" ? [...Object.values(stored.scalars ?? {}), ...Object.values(stored.named ?? {})].find((value) => typeof value === "string") : undefined;
|
|
789
857
|
if (ownKey) {
|
|
790
858
|
try {
|
|
791
859
|
const response = await oauthStatusRequest(loginEndpoint(stored.minted.revocationUrl), { method: "POST", headers: { Authorization: "Bearer " + ownKey } }, 15_000);
|
|
@@ -940,13 +1008,14 @@ function mcpEntryFor(url, readOnly = false) {
|
|
|
940
1008
|
const warnings = [];
|
|
941
1009
|
const hosted = url ?? MCP_URL ?? undefined;
|
|
942
1010
|
if (hosted) {
|
|
943
|
-
const envVar = AUTH_SCALARS[0]?.env;
|
|
944
1011
|
// The hosted endpoint serves its read-only twin at <url>/readonly; a
|
|
945
1012
|
// remote server of someone else's may not, so say so.
|
|
946
1013
|
const target = readOnly ? hosted.replace(/\/+$/, "") + "/readonly" : hosted;
|
|
947
1014
|
if (readOnly && url !== undefined && !MCP_URL)
|
|
948
|
-
warnings.push("--read-only appended /readonly to the URL
|
|
949
|
-
|
|
1015
|
+
warnings.push("--read-only appended /readonly to the URL; check that this server serves a read-only endpoint there.");
|
|
1016
|
+
if (HOSTED_MCP_NOTE)
|
|
1017
|
+
warnings.push(HOSTED_MCP_NOTE);
|
|
1018
|
+
return { entry: { url: target, ...(Object.keys(HOSTED_MCP_HEADERS).length ? { headers: { ...HOSTED_MCP_HEADERS } } : {}) }, warnings };
|
|
950
1019
|
}
|
|
951
1020
|
if (!HAS_MCP) {
|
|
952
1021
|
fail(2, "This package was generated without the MCP server target. Regenerate with it, or pass --url for a remote endpoint.");
|
|
@@ -1023,7 +1092,7 @@ async function cmdMcp(parsed) {
|
|
|
1023
1092
|
}
|
|
1024
1093
|
if (wanted.size === 0) {
|
|
1025
1094
|
out({
|
|
1026
|
-
server:
|
|
1095
|
+
server: MCP_SERVER_KEY,
|
|
1027
1096
|
entry,
|
|
1028
1097
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
1029
1098
|
detected: MCP_CLIENTS.filter((c) => c.detect(cwd)).map((c) => c.id),
|
|
@@ -1034,11 +1103,11 @@ async function cmdMcp(parsed) {
|
|
|
1034
1103
|
const results = [];
|
|
1035
1104
|
for (const id of wanted) {
|
|
1036
1105
|
const client = findMcpClient(id);
|
|
1037
|
-
results.push(writeMcpConfig(client, cwd,
|
|
1106
|
+
results.push(writeMcpConfig(client, cwd, MCP_SERVER_KEY, entry));
|
|
1038
1107
|
}
|
|
1039
1108
|
out({
|
|
1040
1109
|
ok: true,
|
|
1041
|
-
server:
|
|
1110
|
+
server: MCP_SERVER_KEY,
|
|
1042
1111
|
entry,
|
|
1043
1112
|
written: results.filter((r) => r.written).map((r) => r.file),
|
|
1044
1113
|
clients: results,
|
|
@@ -1058,10 +1127,11 @@ function agentContext() {
|
|
|
1058
1127
|
version: VERSION,
|
|
1059
1128
|
envPrefix: ENV_PREFIX,
|
|
1060
1129
|
authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? ["TYPESHIP_CREDENTIALS"] : [])],
|
|
1130
|
+
...(AUTH_UNDECLARED ? { authNotDeclared: true } : {}),
|
|
1061
1131
|
docsUrl: docsSiteUrl(),
|
|
1062
1132
|
docsIndexUrl: docsIndexUrl(),
|
|
1063
1133
|
generatedOperationCount: OPS.length,
|
|
1064
|
-
|
|
1134
|
+
omittedOperationCount: OMITTED_OPS.length,
|
|
1065
1135
|
mcpUrl: MCP_URL,
|
|
1066
1136
|
skillsRepo: SKILLS_REPO,
|
|
1067
1137
|
hasMcp: HAS_MCP,
|
|
@@ -1124,8 +1194,6 @@ function helpJson() {
|
|
|
1124
1194
|
coverage: {
|
|
1125
1195
|
generated_operations: OPS.length,
|
|
1126
1196
|
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
1197
|
},
|
|
1130
1198
|
} : {}),
|
|
1131
1199
|
discovery: {
|
|
@@ -1134,7 +1202,7 @@ function helpJson() {
|
|
|
1134
1202
|
note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
|
|
1135
1203
|
},
|
|
1136
1204
|
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>")],
|
|
1205
|
+
global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--header 'Name: value'", "--timeout <seconds>", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
|
|
1138
1206
|
auth_env_vars: agentContext().authEnvVars,
|
|
1139
1207
|
};
|
|
1140
1208
|
}
|
|
@@ -1167,25 +1235,27 @@ async function cmdAuth(parsed) {
|
|
|
1167
1235
|
}
|
|
1168
1236
|
if (parsed.help || sub !== "check") {
|
|
1169
1237
|
process.stdout.write([
|
|
1170
|
-
BIN + " auth check [--
|
|
1238
|
+
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
1239
|
" " + BIN + " auth profiles list profiles without unlocking credentials",
|
|
1172
1240
|
" " + BIN + " auth use <name> select the default profile",
|
|
1173
1241
|
" " + BIN + " auth remove <name> remove a profile after logout",
|
|
1174
1242
|
" --profile <name> overrides TYPESHIP_PROFILE, then the saved selection, then default.",
|
|
1175
|
-
" --
|
|
1243
|
+
" --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
1244
|
"",
|
|
1177
1245
|
"Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
|
|
1178
1246
|
].join("\n") + "\n");
|
|
1179
1247
|
await flushExit(parsed.help ? 0 : 2);
|
|
1180
1248
|
}
|
|
1181
1249
|
const source = credentialSource(parsed.flags) ?? "none";
|
|
1182
|
-
const
|
|
1250
|
+
const present = source !== "none";
|
|
1183
1251
|
const savedIdentity = source === "login" ? readCreds() : null;
|
|
1184
1252
|
if (savedIdentity)
|
|
1185
1253
|
assertStoredIdentity(savedIdentity, identityConfiguration());
|
|
1254
|
+
// A credential counts as authenticated only once the API accepted it.
|
|
1186
1255
|
const report = {
|
|
1187
|
-
status:
|
|
1188
|
-
authenticated,
|
|
1256
|
+
status: present ? "ok" : "action_required",
|
|
1257
|
+
authenticated: false,
|
|
1258
|
+
verification: present ? "unverified" : "none",
|
|
1189
1259
|
source,
|
|
1190
1260
|
credentials_path: existsSync(credsPath()) ? credsPath() : null,
|
|
1191
1261
|
credential_storage: credentialStore().backend,
|
|
@@ -1193,12 +1263,13 @@ async function cmdAuth(parsed) {
|
|
|
1193
1263
|
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
1194
1264
|
auth_env_vars: agentContext().authEnvVars,
|
|
1195
1265
|
base_url: resolveBaseUrl(parsed.flags) ?? null,
|
|
1196
|
-
next_steps:
|
|
1266
|
+
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
1267
|
...AUTH_SCALARS.map((a) => "Set " + a.env + " in the environment, or run '" + BIN + " login --" + a.flag + " <value>'."),
|
|
1198
|
-
|
|
1268
|
+
...(AUTH_UNDECLARED ? ["The API Spec does not declare authentication. Add another header with --header \"Name: value\" or TYPESHIP_HEADERS."] : []),
|
|
1269
|
+
...(WHOAMI ? ["Then run '" + BIN + " auth check'."] : []),
|
|
1199
1270
|
],
|
|
1200
1271
|
};
|
|
1201
|
-
if (
|
|
1272
|
+
if (present && parsed.flags.get("offline") !== true && WHOAMI) {
|
|
1202
1273
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1203
1274
|
if (op) {
|
|
1204
1275
|
const client = await makeClient(parsed.flags, op);
|
|
@@ -1208,9 +1279,16 @@ async function cmdAuth(parsed) {
|
|
|
1208
1279
|
if (identityConfiguration() && savedIdentity?.identity)
|
|
1209
1280
|
assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
|
|
1210
1281
|
report.identity = result.data;
|
|
1282
|
+
// An identity read that also answers anonymous callers proves nothing.
|
|
1283
|
+
if (op.auth !== "none") {
|
|
1284
|
+
report.authenticated = true;
|
|
1285
|
+
report.verification = "verified";
|
|
1286
|
+
report.next_steps = [];
|
|
1287
|
+
}
|
|
1211
1288
|
}
|
|
1212
1289
|
else {
|
|
1213
|
-
const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1290
|
+
const why = classifyApiError(result.error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1291
|
+
report.verification = "rejected";
|
|
1214
1292
|
report.status = "action_required";
|
|
1215
1293
|
report.live = { ok: false, code: why.code, message: why.message };
|
|
1216
1294
|
report.next_steps = why.nextSteps ?? [];
|
|
@@ -1243,7 +1321,13 @@ async function cmdDoctor(parsed) {
|
|
|
1243
1321
|
if (baseUrl) {
|
|
1244
1322
|
try {
|
|
1245
1323
|
const response = await fetch(baseUrl, { method: "GET", signal: AbortSignal.timeout(8_000) });
|
|
1246
|
-
|
|
1324
|
+
void response.body?.cancel().catch(() => { });
|
|
1325
|
+
const reachable = response.status >= 200 && response.status < 300;
|
|
1326
|
+
checks.push({ name: "base_url", ok: reachable, detail: baseUrl + " → HTTP " + response.status, ...(reachable ? {} : { fix: response.status === 404 || response.status === 405
|
|
1327
|
+
? "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."
|
|
1328
|
+
: response.status === 401 || response.status === 403
|
|
1329
|
+
? "The API answered without a credential with HTTP " + response.status + "; the identity check below tests the credential."
|
|
1330
|
+
: "The API answered HTTP " + response.status + ". Check the base URL and the API's status." }) });
|
|
1247
1331
|
}
|
|
1248
1332
|
catch (e) {
|
|
1249
1333
|
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 +1343,13 @@ async function cmdDoctor(parsed) {
|
|
|
1259
1343
|
const client = await makeClient(parsed.flags, op);
|
|
1260
1344
|
const target = client[op.resource];
|
|
1261
1345
|
const result = await asApiResult(target[op.method]());
|
|
1262
|
-
|
|
1346
|
+
const status = result.error?.status;
|
|
1347
|
+
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,
|
|
1348
|
+
// Only 401 and 403 are about the credential. A 404 or 405 means the
|
|
1349
|
+
// API no longer has this endpoint where this CLI version expects it.
|
|
1350
|
+
fix: status === 401 || status === 403 ? "The credential was rejected; run '" + BIN + " login' with a current one."
|
|
1351
|
+
: status === 404 || status === 405 ? "The API does not know " + wireOf(op) + ", which this CLI version calls. Run '" + BIN + " upgrade', and check the base URL."
|
|
1352
|
+
: "The identity read failed; the detail says why." });
|
|
1263
1353
|
}
|
|
1264
1354
|
catch (e) {
|
|
1265
1355
|
checks.push({ name: "identity", ok: false, detail: e.message });
|
|
@@ -1273,7 +1363,7 @@ async function cmdDoctor(parsed) {
|
|
|
1273
1363
|
}
|
|
1274
1364
|
if (HAS_MCP || MCP_URL) {
|
|
1275
1365
|
const cwd = process.cwd();
|
|
1276
|
-
const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd,
|
|
1366
|
+
const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd, MCP_SERVER_KEY)).map((c) => c.id);
|
|
1277
1367
|
const detected = MCP_CLIENTS.filter((c) => c.detect(cwd) && !c.incompatible).map((c) => c.id);
|
|
1278
1368
|
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
1369
|
}
|
|
@@ -1312,14 +1402,20 @@ async function cmdInit(parsed) {
|
|
|
1312
1402
|
const first = AUTH_SCALARS[0];
|
|
1313
1403
|
const named = flagCredentials(parsed.flags), envNamed = environmentCredentials();
|
|
1314
1404
|
const stored = Object.keys(named).length || Object.keys(envNamed).length || first && process.env[first.env] ? {} : readCreds() ?? {};
|
|
1315
|
-
|
|
1316
|
-
|
|
1405
|
+
// -k stores the credential login would store; --<flag> stores that scalar.
|
|
1406
|
+
const target = loginTarget(parsed.flags);
|
|
1407
|
+
const keyValue = typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") : undefined;
|
|
1408
|
+
const flagValue = first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) : undefined;
|
|
1317
1409
|
if (Object.keys(named).length) {
|
|
1318
1410
|
await saveLoginCredentials({ named }, parsed.flags);
|
|
1319
1411
|
report.credential = { status: "stored", path: credsPath() };
|
|
1320
1412
|
}
|
|
1321
|
-
else if (
|
|
1322
|
-
await saveLoginCredentials(
|
|
1413
|
+
else if (keyValue && target && !target.basic) {
|
|
1414
|
+
await saveLoginCredentials(target.save(keyValue), parsed.flags);
|
|
1415
|
+
report.credential = { status: "stored", path: credsPath() };
|
|
1416
|
+
}
|
|
1417
|
+
else if ((keyValue ?? flagValue) && first) {
|
|
1418
|
+
await saveLoginCredentials({ scalars: { [first.option]: (keyValue ?? flagValue) } }, parsed.flags);
|
|
1323
1419
|
report.credential = { status: "stored", path: credsPath() };
|
|
1324
1420
|
}
|
|
1325
1421
|
else if (first && process.env[first.env]) {
|
|
@@ -1335,13 +1431,13 @@ async function cmdInit(parsed) {
|
|
|
1335
1431
|
await acquireOAuthBrowserSession(parsed, (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID));
|
|
1336
1432
|
report.credential = { status: "stored", method: "oauth_browser", path: credsPath() };
|
|
1337
1433
|
}
|
|
1338
|
-
else if (
|
|
1434
|
+
else if (target && !target.basic && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
|
|
1339
1435
|
// Nothing anywhere: approve a credential in the browser, as `login`
|
|
1340
1436
|
// would, then carry on. Under an agent the URL is printed for the person
|
|
1341
1437
|
// and polled; only the explicit non-interactive switch skips this.
|
|
1342
1438
|
await credentialStore().prepare();
|
|
1343
1439
|
const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
1344
|
-
await storeMinted(minted, parsed.flags);
|
|
1440
|
+
await storeMinted(minted, target, parsed.flags);
|
|
1345
1441
|
report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
|
|
1346
1442
|
}
|
|
1347
1443
|
else {
|
|
@@ -1363,7 +1459,7 @@ async function cmdInit(parsed) {
|
|
|
1363
1459
|
else {
|
|
1364
1460
|
const { entry, warnings } = mcpEntryFor(undefined);
|
|
1365
1461
|
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,
|
|
1462
|
+
const results = clients.map((c) => writeMcpConfig(c, cwd, MCP_SERVER_KEY, entry));
|
|
1367
1463
|
report.mcp = { status: results.length > 0 ? "written" : "no-clients", entry, clients: results, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
1368
1464
|
if (results.length === 0)
|
|
1369
1465
|
nextSteps.push("No MCP client was found on this machine; run '" + BIN + " mcp' to print the entry.");
|
|
@@ -1377,7 +1473,7 @@ async function cmdInit(parsed) {
|
|
|
1377
1473
|
const result = upsertAgentBlock(file, BIN + " agent-contract", agentBlock(agentContext(), commandSummaries()));
|
|
1378
1474
|
report.agents_md = { status: result.updated ? "updated" : "written", file: result.file };
|
|
1379
1475
|
}
|
|
1380
|
-
nextSteps.push("Run '" + BIN + " auth check
|
|
1476
|
+
nextSteps.push("Run '" + BIN + " auth check'" + (WHOAMI ? "" : " (or any read command)") + " to confirm the connection.");
|
|
1381
1477
|
nextSteps.push("Run '" + BIN + " agent-guide' for the conventions, or '" + BIN + " --help' for commands.");
|
|
1382
1478
|
out({ ...report, next_steps: nextSteps });
|
|
1383
1479
|
await flushExit(0);
|
|
@@ -1400,6 +1496,8 @@ function registryBase() {
|
|
|
1400
1496
|
return (process.env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
1401
1497
|
}
|
|
1402
1498
|
async function latestVersion(timeoutMs) {
|
|
1499
|
+
if (!PKG_CONFIRMED)
|
|
1500
|
+
return null;
|
|
1403
1501
|
try {
|
|
1404
1502
|
const response = await fetch(registryBase() + "/" + PKG_NAME, {
|
|
1405
1503
|
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
@@ -1427,6 +1525,9 @@ async function cmdUpgrade(parsed) {
|
|
|
1427
1525
|
process.stdout.write(lines.join("\n") + "\n");
|
|
1428
1526
|
await flushExit(0);
|
|
1429
1527
|
}
|
|
1528
|
+
if (!PKG_CONFIRMED) {
|
|
1529
|
+
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.");
|
|
1530
|
+
}
|
|
1430
1531
|
const latest = await latestVersion(5000);
|
|
1431
1532
|
if (latest === null) {
|
|
1432
1533
|
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 +1581,7 @@ function completionFlagsFor(op) {
|
|
|
1480
1581
|
}
|
|
1481
1582
|
return { flags, values };
|
|
1482
1583
|
}
|
|
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)];
|
|
1584
|
+
const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--header", "--timeout", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
|
|
1484
1585
|
const BUILTIN_WORDS = {
|
|
1485
1586
|
config: ["list", "get", "set", "unset", "path"],
|
|
1486
1587
|
completion: ["bash", "zsh", "fish"],
|
|
@@ -1620,40 +1721,6 @@ async function fetchDocs(pathOrFile) {
|
|
|
1620
1721
|
}
|
|
1621
1722
|
}
|
|
1622
1723
|
}
|
|
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
1724
|
function referenceFor(op, includeSchemas = false) {
|
|
1658
1725
|
const lines = [];
|
|
1659
1726
|
lines.push(paintOut("bold", usageLine(op)));
|
|
@@ -1748,23 +1815,22 @@ async function cmdDocs(parsed) {
|
|
|
1748
1815
|
if (!term)
|
|
1749
1816
|
fail(2, "docs search expects a term");
|
|
1750
1817
|
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);
|
|
1818
|
+
// The MCP server's search_docs ranking, so both surfaces agree.
|
|
1819
|
+
const refMatches = rankOperations(OPS, term).map((match) => match.op);
|
|
1755
1820
|
const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
|
|
1756
1821
|
if (jsonOutput) {
|
|
1757
1822
|
out({
|
|
1758
1823
|
schema_version: "1",
|
|
1759
1824
|
query: term,
|
|
1760
|
-
reference: refMatches.slice(0,
|
|
1825
|
+
reference: refMatches.slice(0, SEARCH_PAGE_SIZE).map((op) => ({
|
|
1761
1826
|
command: op.command.join(" "),
|
|
1762
1827
|
method: op.httpMethod,
|
|
1763
1828
|
path: op.path,
|
|
1764
1829
|
...(op.summary ? { summary: op.summary } : {}),
|
|
1830
|
+
...(op.deprecated ? { deprecated: true } : {}),
|
|
1765
1831
|
details_command: BIN + " docs " + op.command.join(" ") + " --json",
|
|
1766
1832
|
})),
|
|
1767
|
-
guides: proseMatches.slice(0,
|
|
1833
|
+
guides: proseMatches.slice(0, SEARCH_PAGE_SIZE).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
|
|
1768
1834
|
totals: { reference: refMatches.length, guides: proseMatches.length },
|
|
1769
1835
|
guides_status: docsStatus,
|
|
1770
1836
|
...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
|
|
@@ -1775,12 +1841,12 @@ async function cmdDocs(parsed) {
|
|
|
1775
1841
|
const lines = [];
|
|
1776
1842
|
if (refMatches.length > 0) {
|
|
1777
1843
|
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)));
|
|
1844
|
+
for (const op of refMatches.slice(0, SEARCH_PAGE_SIZE))
|
|
1845
|
+
lines.push(" " + padPaint("cyan", op.command.join(" "), 34) + (op.deprecated ? "(deprecated) " : "") + (op.summary ?? wireOf(op)));
|
|
1780
1846
|
}
|
|
1781
1847
|
if (proseMatches.length > 0) {
|
|
1782
1848
|
lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
|
|
1783
|
-
for (const match of proseMatches.slice(0,
|
|
1849
|
+
for (const match of proseMatches.slice(0, SEARCH_PAGE_SIZE))
|
|
1784
1850
|
lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
|
|
1785
1851
|
}
|
|
1786
1852
|
else if (docsStatus === "not_configured") {
|
|
@@ -1841,7 +1907,7 @@ async function cmdDocs(parsed) {
|
|
|
1841
1907
|
await flushExit(0);
|
|
1842
1908
|
}
|
|
1843
1909
|
const lines = [];
|
|
1844
|
-
lines.push(paintOut("bold", "
|
|
1910
|
+
lines.push(paintOut("bold", "Typeship") + " (v" + API_VERSION + ")");
|
|
1845
1911
|
if (API_DESCRIPTION)
|
|
1846
1912
|
lines.push("", API_DESCRIPTION.trim());
|
|
1847
1913
|
lines.push("", paintOut("bold", "Reference:") + " " + BIN + " docs <resource> <command>");
|
|
@@ -1890,7 +1956,7 @@ function shellQuote(value) {
|
|
|
1890
1956
|
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : "'" + value.replace(/'/g, "'\\''") + "'";
|
|
1891
1957
|
}
|
|
1892
1958
|
function usageLine(op) {
|
|
1893
|
-
const paths = op.params.filter((p) => p.kind === "path").map((p) => "<" + p.name + ">").join(" ");
|
|
1959
|
+
const paths = op.params.filter((p) => p.kind === "path").map((p) => p.credential ? "[<" + p.name + ">]" : "<" + p.name + ">").join(" ");
|
|
1894
1960
|
return BIN + " " + op.command[0] + " " + op.command[1] + (paths ? " " + paths : "");
|
|
1895
1961
|
}
|
|
1896
1962
|
/** How the command reaches the wire: "GET /users/{id}", or for GraphQL the
|
|
@@ -1943,7 +2009,7 @@ function typeLabel(p) {
|
|
|
1943
2009
|
if (p.nullable)
|
|
1944
2010
|
return typeLabel({ ...p, nullable: false }) + "|null";
|
|
1945
2011
|
if (p.type === "file")
|
|
1946
|
-
return "path (uploaded)";
|
|
2012
|
+
return p.multiple ? "paths (uploaded, repeatable)" : "path (uploaded)";
|
|
1947
2013
|
if (p.format && p.type === "string")
|
|
1948
2014
|
return p.format;
|
|
1949
2015
|
const inlineEnum = (values) => values && values.join("|").length <= 24 ? values.join("|") : undefined;
|
|
@@ -2022,7 +2088,7 @@ function printRoot(stream = process.stdout) {
|
|
|
2022
2088
|
}
|
|
2023
2089
|
const width = termWidth();
|
|
2024
2090
|
const lines = [];
|
|
2025
|
-
lines.push(paintOut("bold", BIN) + ": " + "
|
|
2091
|
+
lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.23.1");
|
|
2026
2092
|
lines.push("");
|
|
2027
2093
|
lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
|
|
2028
2094
|
lines.push("");
|
|
@@ -2042,18 +2108,19 @@ function printRoot(stream = process.stdout) {
|
|
|
2042
2108
|
}
|
|
2043
2109
|
if (EXCLUDED_OPS > 0) {
|
|
2044
2110
|
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));
|
|
2111
|
+
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
2112
|
}
|
|
2049
2113
|
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)" +
|
|
2114
|
+
const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --header \"Name: value\", --timeout <seconds>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check parameters and JSON bodies)" +
|
|
2051
2115
|
(AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
|
|
2052
2116
|
lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
|
|
2053
2117
|
lines.push(...labeled("Credential env vars: ", [
|
|
2054
2118
|
"TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
|
|
2055
2119
|
...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
|
|
2056
2120
|
].join(", ") || "none", width, 21));
|
|
2121
|
+
if (AUTH_UNDECLARED)
|
|
2122
|
+
lines.push(...labeled("Auth: ", "not declared by the API Spec; " + AUTH_SCALARS[0].env + " is sent as Authorization: Bearer when set", width, 6));
|
|
2123
|
+
lines.push(...labeled("Extra headers: ", "--header \"Name: value\" (repeatable) or TYPESHIP_HEADERS", width, 15));
|
|
2057
2124
|
lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
|
|
2058
2125
|
lines.push(...labeled("Sign-in: ", BIN + " login | logout | whoami | auth check (stored at " + credsPath() + ")", width, 9));
|
|
2059
2126
|
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 +2167,8 @@ function commandExtras(op) {
|
|
|
2100
2167
|
extras.push(["--credentials @<file>|-", "named credentials as JSON; use - for stdin"]);
|
|
2101
2168
|
if (op.hasBody && op.bodyKind === "binary")
|
|
2102
2169
|
extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
|
|
2170
|
+
if (op.rawResponse)
|
|
2171
|
+
extras.push(["--output <file>", "write the " + (op.rawResponse === "binary" ? "binary " : "") + "response body to a file (- for stdout) and print what was written"]);
|
|
2103
2172
|
else if (op.hasBody)
|
|
2104
2173
|
extras.push(["--data '<json>'", "raw JSON body" + (op.bodyStyle === "fields" ? " (merged under field flags)" : "") + "; @<file> reads a file, - reads stdin"]);
|
|
2105
2174
|
if (op.select)
|
|
@@ -2118,6 +2187,8 @@ function commandExtras(op) {
|
|
|
2118
2187
|
function exampleLine(op) {
|
|
2119
2188
|
const parts = [BIN, op.command[0], op.command[1]];
|
|
2120
2189
|
for (const p of op.params) {
|
|
2190
|
+
if (p.credential)
|
|
2191
|
+
continue; // defaulted from the configured credential
|
|
2121
2192
|
const hasExample = p.type !== "file" && Object.hasOwn(op.exampleArguments, p.name);
|
|
2122
2193
|
if (!p.required && !hasExample)
|
|
2123
2194
|
continue;
|
|
@@ -2151,6 +2222,8 @@ function exampleLine(op) {
|
|
|
2151
2222
|
}
|
|
2152
2223
|
/** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
|
|
2153
2224
|
function authNote(op) {
|
|
2225
|
+
if (AUTH_UNDECLARED)
|
|
2226
|
+
return op.auth === "none" ? " (no auth needed)" : " (auth not declared)";
|
|
2154
2227
|
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
|
|
2155
2228
|
return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
|
|
2156
2229
|
}
|
|
@@ -2160,6 +2233,9 @@ function printOp(op) {
|
|
|
2160
2233
|
if (op.summary)
|
|
2161
2234
|
lines.push(helpSentence(op.summary));
|
|
2162
2235
|
lines.push(wireOf(op) + authNote(op));
|
|
2236
|
+
const scopes = requiredScopes(op.security);
|
|
2237
|
+
if (scopes.length)
|
|
2238
|
+
lines.push("Requires OAuth scopes: " + scopes.join(", ") + (HAS_OAUTH_LOGIN ? " (login --scopes " + scopes.join(",") + ")" : ""));
|
|
2163
2239
|
lines.push("");
|
|
2164
2240
|
const rows = op.params.filter((p) => p.kind !== "path");
|
|
2165
2241
|
const extras = commandExtras(op);
|
|
@@ -2212,7 +2288,7 @@ function readStdinBytes() {
|
|
|
2212
2288
|
/** A local file as an upload part; the SDK's multipart encoder takes Blobs. */
|
|
2213
2289
|
function fileFromPath(flag, path) {
|
|
2214
2290
|
try {
|
|
2215
|
-
return new File([readFileSync(path)], basename(path));
|
|
2291
|
+
return new File([readFileSync(path)], basename(path), { type: mediaTypeForPath(path) });
|
|
2216
2292
|
}
|
|
2217
2293
|
catch (e) {
|
|
2218
2294
|
return fail(2, "--" + flag + ": cannot read " + path + " (" + e.message + ")");
|
|
@@ -2270,6 +2346,8 @@ function coerce(spec, raw, repeated) {
|
|
|
2270
2346
|
if (spec.type === "file") {
|
|
2271
2347
|
if (raw === true)
|
|
2272
2348
|
fail(2, "--" + spec.flag + " expects a file path");
|
|
2349
|
+
if (spec.multiple)
|
|
2350
|
+
return (repeated ?? [String(raw)]).map((path) => fileFromPath(spec.flag, path));
|
|
2273
2351
|
return fileFromPath(spec.flag, String(raw));
|
|
2274
2352
|
}
|
|
2275
2353
|
if (spec.type === "boolean") {
|
|
@@ -2362,19 +2440,24 @@ function validateParameters(op, values, flags) {
|
|
|
2362
2440
|
}
|
|
2363
2441
|
/** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
|
|
2364
2442
|
let LAST_CLIENT_HAD_CREDENTIAL = false;
|
|
2443
|
+
/** The last client's Basic credentials, for path arguments that default to the username. */
|
|
2444
|
+
let LAST_CLIENT_BASIC = {};
|
|
2445
|
+
/** The configured Basic-auth username: the named scheme's, else basicAuth's. */
|
|
2446
|
+
function credentialUsername(scheme) {
|
|
2447
|
+
const named = LAST_CLIENT_BASIC.credentials?.[scheme];
|
|
2448
|
+
const username = named && typeof named === "object" ? named.username
|
|
2449
|
+
: LAST_CLIENT_BASIC.basicAuth?.username;
|
|
2450
|
+
return typeof username === "string" && username !== "" ? username : undefined;
|
|
2451
|
+
}
|
|
2365
2452
|
/** Check the same complete alternatives the request runtime can select, after
|
|
2366
2453
|
* per-scheme flag, environment, profile, and OAuth resolution. */
|
|
2367
2454
|
function requireOperationCredentials(op, options) {
|
|
2368
2455
|
if (op.auth !== "required")
|
|
2369
2456
|
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))))
|
|
2457
|
+
const gap = missingCredentials(NAMED_SCHEMES, op.credentialOptions, options);
|
|
2458
|
+
if (!gap)
|
|
2375
2459
|
return;
|
|
2376
|
-
const alternatives
|
|
2377
|
-
const missing = alternatives.map((alternative) => alternative.filter((option) => !available.has(option)).map((option) => option.slice("credentials.".length)));
|
|
2460
|
+
const { alternatives, missing } = gap;
|
|
2378
2461
|
const names = new Set(missing.flat());
|
|
2379
2462
|
const relevant = AUTH_SCALARS.filter((scalar) => [...names].some((name) => NAMED_SCHEMES[name]?.options.includes(scalar.option)));
|
|
2380
2463
|
const needsBasic = [...names].some((name) => NAMED_SCHEMES[name]?.options.includes("basicAuth"));
|
|
@@ -2387,7 +2470,8 @@ function requireOperationCredentials(op, options) {
|
|
|
2387
2470
|
nextSteps: alternatives.length ? [
|
|
2388
2471
|
...relevant.map((a) => "Set " + a.env + " in the environment, pass --" + a.flag + " <value>, or run '" + BIN + " login'."),
|
|
2389
2472
|
...(needsBasic && BASIC ? ["Set " + BASIC.envUser + " and " + BASIC.envPass + ", or pass --username and --password."] : []),
|
|
2390
|
-
|
|
2473
|
+
...(HAS_OAUTH_LOGIN && [...names].some((name) => OAUTH_SESSION_SCHEMES.includes(name)) ? ["Sign in with OAuth: '" + BIN + " login'."] : []),
|
|
2474
|
+
"Supply all schemes in one alternative through " + "TYPESHIP_CREDENTIALS" + " or --credentials @<JSON-file>: " + alternatives.map((alternative) => alternative.join(" + ")).join(" OR ") + ".",
|
|
2391
2475
|
] : ["Check the operation's security schemes in the API Spec and regenerate with a supported, compatible alternative."],
|
|
2392
2476
|
});
|
|
2393
2477
|
}
|
|
@@ -2426,6 +2510,27 @@ function environmentCredentials() {
|
|
|
2426
2510
|
const value = process.env["TYPESHIP_CREDENTIALS"];
|
|
2427
2511
|
return value === undefined ? {} : parseNamedCredentials(value, NAMED_SCHEMES);
|
|
2428
2512
|
}
|
|
2513
|
+
/** --timeout <seconds> or TYPESHIP_TIMEOUT: the per-attempt deadline
|
|
2514
|
+
* (default 60 seconds) for slow operations. */
|
|
2515
|
+
function requestTimeoutMs(flags) {
|
|
2516
|
+
const raw = typeof flags.get("timeout") === "string" ? flags.get("timeout") : process.env["TYPESHIP_TIMEOUT"];
|
|
2517
|
+
if (raw === undefined)
|
|
2518
|
+
return undefined;
|
|
2519
|
+
const seconds = Number(raw);
|
|
2520
|
+
if (!/^\d+(\.\d+)?$/.test(raw.trim()) || !Number.isFinite(seconds) || seconds <= 0 || seconds > 3600)
|
|
2521
|
+
fail(2, "--timeout expects seconds between 0 and 3600, such as --timeout 120.");
|
|
2522
|
+
return Math.round(seconds * 1000);
|
|
2523
|
+
}
|
|
2524
|
+
/** --header flags and TYPESHIP_HEADERS: sent on every API request, after
|
|
2525
|
+
* (and in place of) any generated header of the same name. */
|
|
2526
|
+
function extraRequestHeaders() {
|
|
2527
|
+
try {
|
|
2528
|
+
return parseExtraHeaders(process.env["TYPESHIP_HEADERS"], HEADER_FLAGS, "TYPESHIP_HEADERS");
|
|
2529
|
+
}
|
|
2530
|
+
catch (error) {
|
|
2531
|
+
fail(2, error.message);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2429
2534
|
/** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
|
|
2430
2535
|
function credentialSource(flags) {
|
|
2431
2536
|
if (Object.keys(flagCredentials(flags)).length)
|
|
@@ -2454,7 +2559,31 @@ function resolveBaseUrl(flags, config = readConfig()) {
|
|
|
2454
2559
|
?? (config.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
|
|
2455
2560
|
?? DEFAULT_BASE_URL ?? undefined;
|
|
2456
2561
|
}
|
|
2562
|
+
/** OAuth schemes the login session authenticates by name; empty when the
|
|
2563
|
+
* session is the convenience bearer token (see oauthSessionSchemes). */
|
|
2564
|
+
const OAUTH_SESSION_SCHEMES = oauthSessionSchemes(NAMED_SCHEMES);
|
|
2565
|
+
/** Send a login session to the OAuth scheme, never to a separate http bearer
|
|
2566
|
+
* scheme. Explicit credentials for the same scheme keep precedence. */
|
|
2567
|
+
function applyOAuthSession(options, token, replace = false) {
|
|
2568
|
+
if (!OAUTH_SESSION_SCHEMES.length) {
|
|
2569
|
+
if (replace || options.bearerToken === undefined)
|
|
2570
|
+
options.bearerToken = token;
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
const credentials = (options.credentials ??= {});
|
|
2574
|
+
for (const name of OAUTH_SESSION_SCHEMES)
|
|
2575
|
+
if (replace || !Object.hasOwn(credentials, name))
|
|
2576
|
+
credentials[name] = token;
|
|
2577
|
+
}
|
|
2578
|
+
function withOAuthSession(options, accessToken) {
|
|
2579
|
+
const next = { ...options, credentials: { ...options.credentials } };
|
|
2580
|
+
applyOAuthSession(next, accessToken, true);
|
|
2581
|
+
if (!OAUTH_SESSION_SCHEMES.length)
|
|
2582
|
+
next.credentials = { ...next.credentials, ...resolveNamedCredentials(NAMED_SCHEMES, [{ options: { bearerToken: accessToken } }]) };
|
|
2583
|
+
return next;
|
|
2584
|
+
}
|
|
2457
2585
|
async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
2586
|
+
CURRENT_SCOPES = requiredScopes(op.security);
|
|
2458
2587
|
const flagNamed = flagCredentials(flags), envNamed = environmentCredentials();
|
|
2459
2588
|
const explicitOptions = new Set(AUTH_SCALARS.filter((a) => typeof flags.get(a.flag) === "string" || process.env[a.env] !== undefined).map((a) => a.option));
|
|
2460
2589
|
if (BASIC && (typeof flags.get("username") === "string" || process.env[BASIC.envUser] !== undefined) && (typeof flags.get("password") === "string" || process.env[BASIC.envPass] !== undefined))
|
|
@@ -2502,20 +2631,24 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
|
2502
2631
|
options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
|
|
2503
2632
|
{ named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
|
|
2504
2633
|
{ 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 } : {}) } }] : []),
|
|
2634
|
+
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth && !OAUTH_SESSION_SCHEMES.length ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2506
2635
|
]);
|
|
2507
|
-
if (stored?.oauth
|
|
2636
|
+
if (stored?.oauth) {
|
|
2508
2637
|
const sessionId = stored.oauth.sessionId;
|
|
2509
|
-
|
|
2638
|
+
const token = forIdentity ? stored.oauth.accessToken : sessionCredential((rejected) => oauthSessionToken(credentialStore(), {
|
|
2510
2639
|
...sessionConfiguration(baseUrl, config),
|
|
2511
|
-
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken) => verifyClientIdentity((values) => new TypeshipClient(values),
|
|
2512
|
-
}, sessionId, OAUTH_TOKEN_PARAMS);
|
|
2640
|
+
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken) => verifyClientIdentity((values) => new TypeshipClient(values), withOAuthSession(options, accessToken), WHOAMI, IDENTITY_POLICY) } : {}),
|
|
2641
|
+
}, sessionId, OAUTH_TOKEN_PARAMS, rejected));
|
|
2642
|
+
applyOAuthSession(options, token, forIdentity);
|
|
2513
2643
|
}
|
|
2514
2644
|
if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
|
|
2515
2645
|
options.debug = (event) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
|
|
2516
2646
|
}
|
|
2517
2647
|
if (flags.get("validate") === true)
|
|
2518
2648
|
options.validate = true;
|
|
2649
|
+
const timeout = requestTimeoutMs(flags);
|
|
2650
|
+
if (timeout !== undefined)
|
|
2651
|
+
options.timeoutMs = timeout;
|
|
2519
2652
|
for (const g of GLOBALS) {
|
|
2520
2653
|
const flagValue = flags.get(g.flag);
|
|
2521
2654
|
const value = typeof flagValue === "string" ? flagValue : process.env["TYPESHIP_" + g.envSuffix];
|
|
@@ -2523,6 +2656,7 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
|
2523
2656
|
options[g.option] = value;
|
|
2524
2657
|
}
|
|
2525
2658
|
LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
|
|
2659
|
+
LAST_CLIENT_BASIC = { basicAuth: options.basicAuth, credentials: options.credentials };
|
|
2526
2660
|
requireOperationCredentials(op, options);
|
|
2527
2661
|
// Identify the package and version. Optional harness and caller details
|
|
2528
2662
|
// let the API distinguish agent traffic from other non-interactive use.
|
|
@@ -2540,6 +2674,9 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
|
2540
2674
|
options.maxRetries = 0;
|
|
2541
2675
|
options.timeoutMs = 10_000;
|
|
2542
2676
|
}
|
|
2677
|
+
const extraHeaders = extraRequestHeaders();
|
|
2678
|
+
if (Object.keys(extraHeaders).length)
|
|
2679
|
+
options.onRequest = (context) => { applyExtraHeaders(context.headers, extraHeaders); };
|
|
2543
2680
|
return new TypeshipClient(options);
|
|
2544
2681
|
}
|
|
2545
2682
|
function editDistance(a, b) {
|
|
@@ -2576,15 +2713,19 @@ function failOmitted(op) {
|
|
|
2576
2713
|
return failWith({
|
|
2577
2714
|
status: "action_required",
|
|
2578
2715
|
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: ["
|
|
2716
|
+
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.",
|
|
2717
|
+
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 },
|
|
2718
|
+
nextSteps: ["The package's publisher can regenerate it with every operation.", "Do not invent or retry an omitted command against this package."],
|
|
2582
2719
|
});
|
|
2583
2720
|
}
|
|
2584
2721
|
const BUILTIN_COMMANDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
|
|
2585
2722
|
async function main() {
|
|
2586
2723
|
const argv = process.argv.slice(2);
|
|
2587
2724
|
const parsed = parseArgv(argv);
|
|
2725
|
+
const headerFlag = parsed.flags.get("header");
|
|
2726
|
+
HEADER_FLAGS = parsed.repeated.get("header") ?? (typeof headerFlag === "string" ? [headerFlag] : []);
|
|
2727
|
+
if (headerFlag === true)
|
|
2728
|
+
fail(2, "--header expects \"Name: value\".");
|
|
2588
2729
|
if (!parsed.help)
|
|
2589
2730
|
validateCredentialsInput(parsed.flags);
|
|
2590
2731
|
const profileFlag = parsed.flags.get("profile");
|
|
@@ -2609,7 +2750,7 @@ async function main() {
|
|
|
2609
2750
|
// "acme 1.0.0 (acme 1.0.0)" would say the name twice; when the API's
|
|
2610
2751
|
// title is the bin, name the API version as such.
|
|
2611
2752
|
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
|
|
2753
|
+
process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by Typeship)\n");
|
|
2613
2754
|
await flushExit(0);
|
|
2614
2755
|
}
|
|
2615
2756
|
const [resourceCmd, methodCmd] = parsed.positionals;
|
|
@@ -2698,11 +2839,17 @@ async function main() {
|
|
|
2698
2839
|
USAGE_HINT = BIN + " " + op.command[0] + " " + op.command[1] + " --help";
|
|
2699
2840
|
const pathSpecs = op.params.filter((p) => p.kind === "path");
|
|
2700
2841
|
const pathValues = parsed.positionals.slice(2);
|
|
2701
|
-
|
|
2702
|
-
|
|
2842
|
+
// A path argument that is the Basic-auth username (Twilio's AccountSid)
|
|
2843
|
+
// may be left out; it defaults to the configured credential below.
|
|
2844
|
+
const pathGiven = pathValues.length === pathSpecs.length ? pathSpecs
|
|
2845
|
+
: pathValues.length === pathSpecs.filter((p) => !p.credential).length ? pathSpecs.filter((p) => !p.credential)
|
|
2846
|
+
: null;
|
|
2847
|
+
if (!pathGiven) {
|
|
2848
|
+
const optional = pathSpecs.filter((p) => p.credential).length;
|
|
2849
|
+
fail(2, "Expected " + (optional ? (pathSpecs.length - optional) + " or " : "") + pathSpecs.length + " argument(s): " + usageLine(op));
|
|
2703
2850
|
}
|
|
2704
2851
|
const values = {};
|
|
2705
|
-
|
|
2852
|
+
pathGiven.forEach((spec, i) => { values[spec.name] = pathValues[i]; });
|
|
2706
2853
|
let dataBody;
|
|
2707
2854
|
const dataRaw = parsed.flags.get("data");
|
|
2708
2855
|
if (dataRaw === true)
|
|
@@ -2727,7 +2874,7 @@ async function main() {
|
|
|
2727
2874
|
// Mirrors opReservedFlags() in the generator: API parameters never use these
|
|
2728
2875
|
// names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
|
|
2729
2876
|
// 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)]);
|
|
2877
|
+
const RESERVED_FLAGS = new Set(["data", "credentials", "header", "timeout", "all", "select", "base-url", "profile", "debug", "validate", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
|
|
2731
2878
|
for (const spec of op.params) {
|
|
2732
2879
|
if (spec.kind === "path")
|
|
2733
2880
|
continue;
|
|
@@ -2738,7 +2885,7 @@ async function main() {
|
|
|
2738
2885
|
// every value (each coerced to the element type); loosely typed (json)
|
|
2739
2886
|
// params become an array of the parsed values.
|
|
2740
2887
|
const all = parsed.repeated.get(spec.flag);
|
|
2741
|
-
values[spec.name] = spec.type === "array"
|
|
2888
|
+
values[spec.name] = spec.type === "array" || (spec.type === "file" && spec.multiple)
|
|
2742
2889
|
? coerce(spec, raw, all)
|
|
2743
2890
|
: all !== undefined && spec.type === "json"
|
|
2744
2891
|
? all.map((v) => coerce(spec, v))
|
|
@@ -2749,11 +2896,39 @@ async function main() {
|
|
|
2749
2896
|
continue;
|
|
2750
2897
|
if (key === "file" && op.bodyKind === "binary")
|
|
2751
2898
|
continue;
|
|
2899
|
+
if (key === "output" && op.rawResponse)
|
|
2900
|
+
continue;
|
|
2752
2901
|
if (!op.params.some((p) => p.flag === key)) {
|
|
2753
2902
|
const suggestion = didYouMean(key, [...op.params.filter((p) => p.kind !== "path").map((p) => p.flag), ...RESERVED_FLAGS]);
|
|
2754
2903
|
fail(2, "Unknown flag --" + key + "." + (suggestion ? " Did you mean --" + suggestion + "?" : ""));
|
|
2755
2904
|
}
|
|
2756
2905
|
}
|
|
2906
|
+
// Object and array values (a GraphQL input, a JSON flag, --data fields)
|
|
2907
|
+
// get the checks the MCP server applies: nested types, enums, required
|
|
2908
|
+
// properties, patterns and a closed object's unknown keys, all at once.
|
|
2909
|
+
const argumentSchemas = (op.inputSchema.properties ?? {});
|
|
2910
|
+
const nestedIssues = [];
|
|
2911
|
+
const checkNested = (name, value) => value !== null && typeof value === "object" && !(value instanceof Blob) && argumentSchemas[name]
|
|
2912
|
+
? checkValue(value, argumentSchemas[name], name, nestedIssues) : value;
|
|
2913
|
+
for (const spec of op.params) {
|
|
2914
|
+
if (spec.kind !== "path" && spec.type !== "file" && values[spec.name] !== undefined)
|
|
2915
|
+
values[spec.name] = checkNested(spec.name, values[spec.name]);
|
|
2916
|
+
}
|
|
2917
|
+
if (op.bodyStyle === "fields" && dataBody !== null && typeof dataBody === "object" && !Array.isArray(dataBody) && !(dataBody instanceof Blob)) {
|
|
2918
|
+
const body = dataBody;
|
|
2919
|
+
for (const name of Object.keys(body)) {
|
|
2920
|
+
if (op.params.some((p) => p.kind === "body" && p.name === name && p.type !== "file"))
|
|
2921
|
+
body[name] = checkNested(name, body[name]);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
if (nestedIssues.length > 0) {
|
|
2925
|
+
failWith({
|
|
2926
|
+
code: nestedIssues.every((issue) => issue.code === "MISSING_ARGUMENT") ? "MISSING_ARGUMENT" : "INVALID_USAGE",
|
|
2927
|
+
message: nestedIssues.length + (nestedIssues.length === 1 ? " problem" : " problems") + " in the arguments; nothing was sent: " + nestedIssues.map((issue) => issue.message).join("; "),
|
|
2928
|
+
detail: { issues: nestedIssues },
|
|
2929
|
+
nextSteps: ["Fix the values listed in detail.issues and run again.", "Run '" + USAGE_HINT + "' for each argument's type."],
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2757
2932
|
const missing = missingRequired(op, values).filter((name) => !(op.bodyStyle === "fields" && dataBody !== undefined && typeof dataBody === "object" && dataBody !== null && name in dataBody));
|
|
2758
2933
|
if (missing.length > 0)
|
|
2759
2934
|
fail(2, "Missing required: " + missing.map((m) => "--" + (op.params.find((p) => p.name === m)?.flag ?? m)).join(", "));
|
|
@@ -2773,6 +2948,7 @@ async function main() {
|
|
|
2773
2948
|
FIELDS = fieldsRaw.split(",").map((f) => f.trim()).filter((f) => f !== "").map((f) => f.split("."));
|
|
2774
2949
|
if (FIELDS.length === 0)
|
|
2775
2950
|
fail(2, "--fields expects at least one field path");
|
|
2951
|
+
FIELDS_AFTER_WRITE = op.safety !== "read";
|
|
2776
2952
|
}
|
|
2777
2953
|
// --out <dir> materializes a file-shaped response (see cli-agent.ts bundleProperty).
|
|
2778
2954
|
const bundleField = op.fileBundleProperty ?? bundleProperty(op.outputSchema);
|
|
@@ -2782,12 +2958,44 @@ async function main() {
|
|
|
2782
2958
|
fail(2, "--out applies to commands whose response carries files ({path, content}); " + op.command.join(" ") + " does not.");
|
|
2783
2959
|
}
|
|
2784
2960
|
validateParameters(op, values, parsed.flags);
|
|
2961
|
+
// Raw bytes on a terminal are unreadable and can garble it: ask for a
|
|
2962
|
+
// destination before the request runs (it may be billed, like speech).
|
|
2963
|
+
const outputFlag = op.rawResponse ? parsed.flags.get("output") : undefined;
|
|
2964
|
+
if (outputFlag === true)
|
|
2965
|
+
fail(2, "--output expects a file path, or - for stdout.");
|
|
2966
|
+
if (op.rawResponse === "binary" && outputFlag === undefined && process.stdout.isTTY) {
|
|
2967
|
+
fail(2, op.command.join(" ") + " returns binary data. Pass --output <file>, or redirect stdout to a file.");
|
|
2968
|
+
}
|
|
2969
|
+
// --all on a list the generator could not page would print one page and
|
|
2970
|
+
// exit 0, which reads as "that is everything".
|
|
2971
|
+
if (!op.paginated && parsed.flags.get("all") === true) {
|
|
2972
|
+
fail(2, op.command.join(" ") + " does not paginate, so --all has nothing to walk. Run it without --all; it returns the whole response.");
|
|
2973
|
+
}
|
|
2785
2974
|
const client = await makeClient(parsed.flags, op);
|
|
2975
|
+
for (const spec of pathSpecs) {
|
|
2976
|
+
if (!spec.credential || values[spec.name] !== undefined)
|
|
2977
|
+
continue;
|
|
2978
|
+
const username = credentialUsername(spec.credential.scheme);
|
|
2979
|
+
if (username === undefined) {
|
|
2980
|
+
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'."]);
|
|
2981
|
+
}
|
|
2982
|
+
values[spec.name] = username;
|
|
2983
|
+
}
|
|
2786
2984
|
// Destructive commands need --force. A person gets asked; an agent gets
|
|
2787
2985
|
// an action_required envelope with the exact command to run, so nothing
|
|
2788
2986
|
// is deleted on a guess.
|
|
2789
2987
|
if (op.safety === "destructive" && !assumeYes(parsed)) {
|
|
2790
|
-
|
|
2988
|
+
// Credential flags are replaced by placeholders: the rerun is shown to
|
|
2989
|
+
// agents and logged, so it must never repeat a key.
|
|
2990
|
+
const secretFlags = new Set([...AUTH_SCALARS.map((a) => "--" + a.flag), "--header"]);
|
|
2991
|
+
const rerun = BIN + " " + process.argv.slice(2).map((a, i, all) => {
|
|
2992
|
+
const eq = a.indexOf("=");
|
|
2993
|
+
if (a.startsWith("--") && eq > 0 && secretFlags.has(a.slice(0, eq)))
|
|
2994
|
+
return a.slice(0, eq) + "=<" + a.slice(2, eq) + ">";
|
|
2995
|
+
if (i > 0 && secretFlags.has(all[i - 1]))
|
|
2996
|
+
return "<" + all[i - 1].slice(2) + ">";
|
|
2997
|
+
return /\s/.test(a) ? JSON.stringify(a) : a;
|
|
2998
|
+
}).join(" ") + " --force";
|
|
2791
2999
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
2792
3000
|
failWith({
|
|
2793
3001
|
status: "action_required",
|
|
@@ -2804,19 +3012,30 @@ async function main() {
|
|
|
2804
3012
|
const selectValue = typeof parsed.flags.get("select") === "string" ? parsed.flags.get("select") : undefined;
|
|
2805
3013
|
const args = buildArgs(op, values, dataBody, selectValue);
|
|
2806
3014
|
const target = client[op.resource];
|
|
2807
|
-
|
|
3015
|
+
// --stream true on an operation with a streaming twin prints its events
|
|
3016
|
+
// as NDJSON as they arrive, instead of failing on the event stream.
|
|
3017
|
+
const streaming = op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value;
|
|
3018
|
+
const callResult = target[streaming ? op.streamMethod.method : op.method](...args);
|
|
2808
3019
|
if (op.paginated && parsed.flags.get("all") === true) {
|
|
3020
|
+
const fieldsCheck = streamFieldsCheck();
|
|
2809
3021
|
try {
|
|
2810
3022
|
for await (const item of callResult) {
|
|
2811
|
-
process.stdout.write(JSON.stringify(
|
|
3023
|
+
process.stdout.write(JSON.stringify(fieldsCheck.item(item)) + "\n");
|
|
2812
3024
|
}
|
|
3025
|
+
fieldsCheck.finish();
|
|
2813
3026
|
await flushExit(0);
|
|
2814
3027
|
}
|
|
2815
3028
|
catch (e) {
|
|
3029
|
+
if (isNotModified(e))
|
|
3030
|
+
await printNotModified(e);
|
|
2816
3031
|
failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
|
|
2817
3032
|
}
|
|
2818
3033
|
}
|
|
2819
3034
|
let result = await asApiResult(callResult);
|
|
3035
|
+
// 304 Not Modified: the conditional request matched, so there is no
|
|
3036
|
+
// body. Say so, with the ETag to send next time.
|
|
3037
|
+
if (!result.ok && isNotModified(result.error))
|
|
3038
|
+
await printNotModified(result.error);
|
|
2820
3039
|
if (result.ok && op.httpMethod === "POST" && op.path === "/projects/{project_id}/generate") {
|
|
2821
3040
|
const batch = result.data;
|
|
2822
3041
|
const generations = client.generations;
|
|
@@ -2830,16 +3049,40 @@ async function main() {
|
|
|
2830
3049
|
result = { ...result, data: { ...batch, data: completed } };
|
|
2831
3050
|
}
|
|
2832
3051
|
if (result.ok) {
|
|
2833
|
-
if (op.sse) {
|
|
3052
|
+
if (op.sse || streaming) {
|
|
2834
3053
|
// Server-sent events as NDJSON, one line per event, until the stream ends.
|
|
3054
|
+
const fieldsCheck = streamFieldsCheck();
|
|
2835
3055
|
try {
|
|
2836
3056
|
for await (const event of result.data) {
|
|
2837
|
-
process.stdout.write(JSON.stringify(
|
|
3057
|
+
process.stdout.write(JSON.stringify(fieldsCheck.item(event)) + "\n");
|
|
2838
3058
|
}
|
|
2839
3059
|
}
|
|
2840
3060
|
catch (e) {
|
|
2841
3061
|
failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
|
|
2842
3062
|
}
|
|
3063
|
+
fieldsCheck.finish();
|
|
3064
|
+
await flushExit(0);
|
|
3065
|
+
}
|
|
3066
|
+
// A non-JSON body (audio, a file, CSV): raw bytes to stdout, or to the
|
|
3067
|
+
// --output file with a JSON note of what was written. Never "{}".
|
|
3068
|
+
if (op.rawResponse || result.data instanceof Blob) {
|
|
3069
|
+
const data = result.data;
|
|
3070
|
+
const bytes = data instanceof Blob
|
|
3071
|
+
? new Uint8Array(await data.arrayBuffer())
|
|
3072
|
+
: new TextEncoder().encode(typeof data === "string" ? data : JSON.stringify(data ?? ""));
|
|
3073
|
+
if (typeof outputFlag === "string" && outputFlag !== "-") {
|
|
3074
|
+
try {
|
|
3075
|
+
writeFileSync(outputFlag, bytes);
|
|
3076
|
+
}
|
|
3077
|
+
catch (e) {
|
|
3078
|
+
fail(1, "--output: cannot write " + outputFlag + " (" + e.message + ")");
|
|
3079
|
+
}
|
|
3080
|
+
const mediaType = data instanceof Blob && data.type ? data.type : op.rawResponse === "text" ? "text/plain" : "application/octet-stream";
|
|
3081
|
+
out({ saved_to: resolvePath(outputFlag), bytes: bytes.length, media_type: mediaType });
|
|
3082
|
+
}
|
|
3083
|
+
else {
|
|
3084
|
+
process.stdout.write(bytes);
|
|
3085
|
+
}
|
|
2843
3086
|
await flushExit(0);
|
|
2844
3087
|
}
|
|
2845
3088
|
// A claim-shaped response (claim.url): say where to claim it, and leave
|
|
@@ -2858,7 +3101,7 @@ async function main() {
|
|
|
2858
3101
|
const page = result.data;
|
|
2859
3102
|
const next = page.nextPageParams();
|
|
2860
3103
|
out({
|
|
2861
|
-
items: project(page.items),
|
|
3104
|
+
items: project(page.items, true),
|
|
2862
3105
|
hasMore: next !== null,
|
|
2863
3106
|
...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
|
|
2864
3107
|
...(page.response.requestId ? { request_id: page.response.requestId } : {}),
|
|
@@ -2868,7 +3111,7 @@ async function main() {
|
|
|
2868
3111
|
// Batch-style collection envelopes ({data: [...]}) use item-relative
|
|
2869
3112
|
// fields, matching paginated results, while retaining envelope metadata.
|
|
2870
3113
|
const data = result.data;
|
|
2871
|
-
out({ ...data, [collectionField]: project(data[collectionField]) });
|
|
3114
|
+
out({ ...data, [collectionField]: project(data[collectionField], true) });
|
|
2872
3115
|
}
|
|
2873
3116
|
else if (outDir !== undefined && bundleField !== null) {
|
|
2874
3117
|
// --out: a file-shaped response (an array of {path, content}) lands
|
|
@@ -2877,10 +3120,10 @@ async function main() {
|
|
|
2877
3120
|
const files = data[bundleField] ?? [];
|
|
2878
3121
|
const written = writeBundle(outDir, files);
|
|
2879
3122
|
const { [bundleField]: _omitted, ...rest } = data;
|
|
2880
|
-
out({ ...project(rest), out: written });
|
|
3123
|
+
out({ ...project(rest, false), out: written });
|
|
2881
3124
|
}
|
|
2882
3125
|
else {
|
|
2883
|
-
out(
|
|
3126
|
+
out(result.data === undefined || result.data === null ? { ok: true } : project(result.data, Array.isArray(result.data)));
|
|
2884
3127
|
}
|
|
2885
3128
|
await flushExit(0);
|
|
2886
3129
|
}
|
|
@@ -2897,6 +3140,9 @@ main().catch((e) => {
|
|
|
2897
3140
|
if (e instanceof ExitPending)
|
|
2898
3141
|
return;
|
|
2899
3142
|
try {
|
|
3143
|
+
const auth = classifyAuthFailure(e, authFailureContext());
|
|
3144
|
+
if (auth)
|
|
3145
|
+
failWith(auth);
|
|
2900
3146
|
fail(1, errorMessage(e));
|
|
2901
3147
|
}
|
|
2902
3148
|
catch {
|