@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/src/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Typeship — command-line client. Generated by Typeship — https://typeship.dev
|
|
3
3
|
// Flags-only for API commands (CI-safe); `login` is the one interactive
|
|
4
4
|
// exception (hidden prompt on a TTY, --with-token/--token for scripts).
|
|
5
5
|
// Prints raw JSON to stdout; errors as JSON on stderr.
|
|
@@ -9,40 +9,52 @@ import { spawnSync } from "node:child_process";
|
|
|
9
9
|
import { oauthBrowserLogin, type OAuthLoginSession } from "./oauth-login.js";
|
|
10
10
|
import { oauthDeviceLogin, customBrowserApproval, loginEndpoint, type ApprovedCredential } from "./polling-login.js";
|
|
11
11
|
import { oauthStatusRequest } from "./oauth-request.js";
|
|
12
|
-
import {
|
|
13
|
-
import { type FileCredentialStore, assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, type CredentialDestination, oauthSessionToken, sessionBinding, type SessionConfiguration, type StoredCredentials as StoredCreds } from "./oauth-session.js";
|
|
12
|
+
import { type FileCredentialStore, sessionCredential, assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, type CredentialDestination, oauthSessionToken, sessionBinding, type SessionConfiguration, type StoredCredentials as StoredCreds } from "./oauth-session.js";
|
|
14
13
|
import { createCredentialStore } from "./credential-storage.js";
|
|
15
14
|
import { identityPolicyOf, identityFetch, identityResult, verifyApiIdentity, verifyClientIdentity, readApiIdentity, assertApiIdentity, type ApiIdentity, type IdentityConfiguration, type IdentityPolicy, type VerifiedIdentity } from "./api-identity.js";
|
|
16
|
-
import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability, type NamedCredentials, type CredentialSchemes } from "./named-credentials.js";
|
|
15
|
+
import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability, missingCredentials, oauthSessionSchemes, parseExtraHeaders, applyExtraHeaders, type NamedCredentials, type CredentialSchemes } from "./named-credentials.js";
|
|
17
16
|
import { resolveProfile, listProfiles, selectProfile, removeProfile, readProfileConfig, updateProfileConfig, type ProfileContext } from "./auth-profiles.js";
|
|
18
17
|
import { randomBytes } from "node:crypto";
|
|
19
18
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
20
19
|
import { homedir, hostname } from "node:os";
|
|
21
|
-
import { basename, dirname, join } from "node:path";
|
|
20
|
+
import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
22
21
|
import { fileURLToPath } from "node:url";
|
|
23
22
|
import { TypeshipClient, formatDebugEvent, type ClientOptions, type DebugEvent } from "./index.js";
|
|
24
|
-
import { asApiResult, validateAgainstSchema, ValidationError, type Violation } from "./core/http.js";
|
|
23
|
+
import { asApiResult, mediaTypeForPath, validateAgainstSchema, ValidationError, type Violation } from "./core/http.js";
|
|
25
24
|
import { SCHEMAS, DEFS } from "./schemas.js";
|
|
26
25
|
import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired, type OmittedOpSpec, type OpSpec, type ParamSpec } from "./ops.js";
|
|
27
26
|
import {
|
|
28
|
-
MCP_CLIENTS, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, collectionProperty, detectHarness, envelope,
|
|
27
|
+
MCP_CLIENTS, requiredScopes, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, classifyAuthFailure, collectionProperty, detectHarness, envelope,
|
|
29
28
|
exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig,
|
|
30
29
|
type AgentContext, type CommandSummary, type DoctorCheck, type EnvelopeInput, type IssueCode, type McpEntry, type McpWriteResult,
|
|
31
30
|
} from "./cli-agent.js";
|
|
32
31
|
import { relativeDate } from "./dates.js";
|
|
32
|
+
import { checkValue, type ArgumentIssue } from "./arguments.js";
|
|
33
33
|
import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
|
|
34
|
+
import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
|
|
35
|
+
import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
const BIN = "typeship";
|
|
39
|
+
/** The MCP server's key in client configs: the API's name, even when this
|
|
40
|
+
* command was renamed away from a vendor's. It reads this command's login. */
|
|
41
|
+
const MCP_SERVER_KEY = "typeship";
|
|
37
42
|
const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
|
|
38
43
|
const NAMED_SCHEMES: CredentialSchemes = {"apiKey":{"kind":"bearer","options":["bearerToken"]}};
|
|
39
44
|
const AUTH_SCALARS: { option: string; flag: string; env: string }[] = [{"option":"bearerToken","flag":"token","env":"TYPESHIP_TOKEN"}];
|
|
45
|
+
/** Hosted MCP request headers as name → env reference, never a literal. */
|
|
46
|
+
const HOSTED_MCP_HEADERS: Record<string, string> = {"Authorization":"Bearer ${TYPESHIP_TOKEN}"};
|
|
47
|
+
const HOSTED_MCP_NOTE: string | null = null;
|
|
40
48
|
const BASIC: { envUser: string; envPass: string } | null = null;
|
|
49
|
+
/** The spec declares no security, so the token is offered, never required. */
|
|
50
|
+
const AUTH_UNDECLARED = false;
|
|
51
|
+
/** Repeated --header "Name: value" flags for this invocation. */
|
|
52
|
+
let HEADER_FLAGS: string[] = [];
|
|
41
53
|
/** Operations omitted from the generated package by its plan cap. */
|
|
42
54
|
const EXCLUDED_OPS = 0;
|
|
43
55
|
/** Generated CLI operations that are intentionally unavailable to MCP. */
|
|
44
56
|
const MCP_EXCLUDED_OPS = 0;
|
|
45
|
-
const VERSION = "0.
|
|
57
|
+
const VERSION = "0.23.1";
|
|
46
58
|
const API_VERSION = "1.0.0";
|
|
47
59
|
const SPEC_FORMAT = "openapi";
|
|
48
60
|
const IDENTITY_POLICY: IdentityPolicy = {};
|
|
@@ -51,10 +63,13 @@ const WHOAMI: { resource: string; method: string } | null = null;
|
|
|
51
63
|
const ENVIRONMENTS: Record<string, string> = {};
|
|
52
64
|
const HAS_MCP = false;
|
|
53
65
|
const PKG_NAME = "@typeship-ax/cli";
|
|
66
|
+
/** False when the package name was derived rather than chosen: the npm
|
|
67
|
+
* package of that name may be someone else's, so upgrade never installs it. */
|
|
68
|
+
const PKG_CONFIRMED = true;
|
|
54
69
|
const UPDATE_NOTICE = false;
|
|
55
70
|
const API_DESCRIPTION: string | null = "Resolve an OpenAPI or GraphQL Spec, diagnose it, and keep every\nselected CLI, MCP, and SDK Target current.\n\nEvery operation but one requires a bearer credential: an organization\nAPI key from the console, or an OAuth access token carrying the operation's\nread, generate, or write capability and the organization selected during\nconsent. OAuth grants cannot switch organizations after consent. A browser\nsession is not a credential for this API. The exception is POST /generate,\nwhich works anonymously with the free plan's limits.\n\nExamples use Parcel, a fictional delivery service. Replace its domains,\nrepository names, and resource identifiers with your own. The hosted\npetstore Spec is a runnable sample.\n";
|
|
56
|
-
const DOCS_URL_DEFAULT: string | null = "https://typeship.dev";
|
|
57
|
-
const DOCS_INDEX_URL_DEFAULT: string | null =
|
|
71
|
+
const DOCS_URL_DEFAULT: string | null = "https://typeship.dev/docs";
|
|
72
|
+
const DOCS_INDEX_URL_DEFAULT: string | null = "https://typeship.dev/llms.txt";
|
|
58
73
|
const RELAY: { mintUrl: string; project: string } | null = null;
|
|
59
74
|
const SUPPORT_URL: string | null = null;
|
|
60
75
|
const OAUTH_TOKEN_URL: string | null = null;
|
|
@@ -64,6 +79,9 @@ const OAUTH_DISCOVERY_URLS: string[] = [];
|
|
|
64
79
|
const OAUTH_DEVICE_URL: string | null = null;
|
|
65
80
|
const HAS_OAUTH_LOGIN = OAUTH_TOKEN_URL !== null || OAUTH_DISCOVERY_URLS.length > 0;
|
|
66
81
|
const OAUTH_LOGIN_METHOD: string = "device";
|
|
82
|
+
/** The loopback port browser login listens on unless the redirect URI names
|
|
83
|
+
* one: fixed per CLI, so it can be registered with the provider. */
|
|
84
|
+
const OAUTH_DEFAULT_REDIRECT_PORT = 49910;
|
|
67
85
|
const OAUTH_REDIRECT_URI: string | undefined = undefined;
|
|
68
86
|
const OAUTH_ORGANIZATION_PARAMETER: "organization" | "organization_id" | undefined = undefined;
|
|
69
87
|
const OAUTH_AUTHORIZATION_URL: string | undefined = undefined;
|
|
@@ -74,7 +92,7 @@ const MCP_URL: string | null = "https://typeship.dev/mcp";
|
|
|
74
92
|
const SKILLS_REPO: string | null = "typeship-ax/skills";
|
|
75
93
|
const CLI_AUTH_URL: string | null = "https://typeship.dev/api/auth/cli";
|
|
76
94
|
const ENV_PREFIX = "TYPESHIP";
|
|
77
|
-
const API_TITLE = "
|
|
95
|
+
const API_TITLE = "Typeship";
|
|
78
96
|
|
|
79
97
|
interface Parsed {
|
|
80
98
|
positionals: string[];
|
|
@@ -99,7 +117,7 @@ const BUILTIN_BOOLEAN_FLAGS: Record<string, string[]> = {
|
|
|
99
117
|
mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
|
|
100
118
|
docs: ["web", "schema"],
|
|
101
119
|
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
|
|
102
|
-
auth: ["live"],
|
|
120
|
+
auth: ["live", "offline"],
|
|
103
121
|
doctor: [],
|
|
104
122
|
};
|
|
105
123
|
|
|
@@ -222,25 +240,46 @@ function out(value: unknown): void {
|
|
|
222
240
|
/** --fields a,b.c: the dotted paths to keep in API results (null = everything). Set in main(). */
|
|
223
241
|
let FIELDS: string[][] | null = null;
|
|
224
242
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return
|
|
243
|
+
/** Whether the command being run writes: an --fields mistake on a write
|
|
244
|
+
* must not tempt anyone into running it again. Set in main(). */
|
|
245
|
+
let FIELDS_AFTER_WRITE = false;
|
|
246
|
+
|
|
247
|
+
/** Keep only FIELDS of a result: arrays item by item, objects by dotted path;
|
|
248
|
+
* scalars untouched. A path that matches nothing is an error naming the keys
|
|
249
|
+
* that exist, never a silent {}. */
|
|
250
|
+
function project(value: unknown, perItem: boolean): unknown {
|
|
251
|
+
if (FIELDS === null) return value;
|
|
252
|
+
const unmatched = unmatchedFields(value, FIELDS);
|
|
253
|
+
if (unmatched.length > 0) failUnmatchedFields(unmatched, perItem, value);
|
|
254
|
+
return projectFields(value, FIELDS);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** --fields over a stream (--all, events): paths no item has matched yet.
|
|
258
|
+
* The stream is printed as it arrives, so the check fails at its end. */
|
|
259
|
+
function streamFieldsCheck(): { item(value: unknown): unknown; finish(): void } {
|
|
260
|
+
let pending: ReturnType<typeof unmatchedFields> | null = null;
|
|
261
|
+
return {
|
|
262
|
+
item(value: unknown): unknown {
|
|
263
|
+
if (FIELDS === null) return value;
|
|
264
|
+
const unmatched = unmatchedFields(value, FIELDS);
|
|
265
|
+
pending = pending === null ? unmatched : pending.filter((p) => unmatched.some((u) => u.path === p.path));
|
|
266
|
+
return projectFields(value, FIELDS);
|
|
267
|
+
},
|
|
268
|
+
finish(): void {
|
|
269
|
+
if (pending !== null && pending.length > 0) failUnmatchedFields(pending, true, undefined);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function failUnmatchedFields(unmatched: ReturnType<typeof unmatchedFields>, perItem: boolean, result: unknown): never {
|
|
275
|
+
return failWith({
|
|
276
|
+
code: "FIELDS_UNMATCHED",
|
|
277
|
+
message: unmatchedFieldsMessage(unmatched, perItem),
|
|
278
|
+
nextSteps: FIELDS_AFTER_WRITE
|
|
279
|
+
? ["This command has already run; do not run it again to change --fields." + (result !== undefined ? " Its full result is in detail.result." : "")]
|
|
280
|
+
: ["Run the command again with --fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or without --fields for the whole result."],
|
|
281
|
+
detail: { unmatched, ...(FIELDS_AFTER_WRITE && result !== undefined ? { result } : {}) },
|
|
282
|
+
});
|
|
244
283
|
}
|
|
245
284
|
|
|
246
285
|
/** Thrown after scheduling exit so sync callers stop; main() swallows it. */
|
|
@@ -323,7 +362,7 @@ let USAGE_HINT = BIN + " --help";
|
|
|
323
362
|
function fail(code: number, message: string, extra?: unknown, nextSteps?: string[]): never {
|
|
324
363
|
const usageCode: IssueCode = /^Unknown command/.test(message) ? "UNKNOWN_COMMAND"
|
|
325
364
|
: /^Unknown flag/.test(message) ? "UNKNOWN_FLAG"
|
|
326
|
-
: /^(Missing required|Expected \d+ argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
365
|
+
: /^(Missing required|Expected \d+ (or \d+ )?argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
327
366
|
: "INVALID_USAGE";
|
|
328
367
|
return failWith({
|
|
329
368
|
code: code === 2 ? usageCode : "CALL_FAILED",
|
|
@@ -334,8 +373,26 @@ function fail(code: number, message: string, extra?: unknown, nextSteps?: string
|
|
|
334
373
|
}
|
|
335
374
|
|
|
336
375
|
/** An SDK error result as an envelope: status-derived code, the API's body as detail, concrete next steps. */
|
|
376
|
+
/** OAuth scopes of the operation being run, so a 403 can name them. */
|
|
377
|
+
let CURRENT_SCOPES: string[] = [];
|
|
337
378
|
function failApi(error: unknown, hadCredential: boolean): never {
|
|
338
|
-
return failWith(classifyApiError(error, { bin: BIN, hadCredential, docsUrl: DOCS_URL_DEFAULT }));
|
|
379
|
+
return failWith(classifyAuthFailure(error, authFailureContext()) ?? classifyApiError(error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential, docsUrl: DOCS_URL_DEFAULT, requiredScopes: CURRENT_SCOPES, canLogin: HAS_OAUTH_LOGIN }));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** The SDK raises NotModifiedError for a 304: a conditional request
|
|
383
|
+
* matched. For the CLI that is a result, not a failure. */
|
|
384
|
+
function isNotModified(error: unknown): error is { etag?: string } {
|
|
385
|
+
return (error as { name?: string } | null)?.name === "NotModifiedError";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function printNotModified(error: { etag?: string }): Promise<never> {
|
|
389
|
+
out({ ok: true, not_modified: true, ...(error.etag ? { etag: error.etag } : {}) });
|
|
390
|
+
return flushExit(0);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** What a login, saved-session or credential-store failure names as alternatives. */
|
|
394
|
+
function authFailureContext(): { bin: string; envVars: string[]; storeVariable: string } {
|
|
395
|
+
return { bin: BIN, storeVariable: ENV_PREFIX + "_CREDENTIAL_STORE", envVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? [ENV_PREFIX + "_CREDENTIALS"] : [])] };
|
|
339
396
|
}
|
|
340
397
|
|
|
341
398
|
// ---------------------------------------------------------------------------
|
|
@@ -353,7 +410,7 @@ function credsPath(): string {
|
|
|
353
410
|
return credentialStore().path;
|
|
354
411
|
}
|
|
355
412
|
|
|
356
|
-
function credentialStore(): FileCredentialStore { return createCredentialStore(configDir(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
413
|
+
function credentialStore(): FileCredentialStore { return createCredentialStore(configDir(), BIN, process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
357
414
|
function readCreds(): StoredCreds | null { return credentialStore().read(); }
|
|
358
415
|
function sessionConfiguration(baseUrl: string, config = readConfig()): SessionConfiguration {
|
|
359
416
|
return {
|
|
@@ -492,9 +549,10 @@ async function deviceLogin(clientId: string, parsed: Parsed): Promise<void> {
|
|
|
492
549
|
if (!apiBaseUrl) fail(2, "Set the API base URL before logging in.");
|
|
493
550
|
const loginConfiguration = sessionConfiguration(apiBaseUrl!);
|
|
494
551
|
await credentialStore().prepare();
|
|
552
|
+
if (!OAUTH_DEVICE_URL && !OAUTH_ISSUER && !OAUTH_DISCOVERY_URL) fail(2, "This API does not declare device authorization. Run '" + BIN + " login' without --device to sign in through the browser.");
|
|
495
553
|
const session = await withLoginCancellation((signal) => oauthDeviceLogin({
|
|
496
554
|
clientId, issuer: OAUTH_ISSUER, discoveryUrls: OAUTH_DISCOVERY_URLS,
|
|
497
|
-
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes:
|
|
555
|
+
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: loginScopes(parsed.flags),
|
|
498
556
|
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
499
557
|
}, { signal, authorize({ verificationUri, userCode, expiresIn }) {
|
|
500
558
|
process.stderr.write("Open " + paintErr("cyan", verificationUri) + " and enter code: " + paintErr("bold", userCode) + "\n");
|
|
@@ -509,11 +567,40 @@ async function deviceLogin(clientId: string, parsed: Parsed): Promise<void> {
|
|
|
509
567
|
await flushExit(0);
|
|
510
568
|
}
|
|
511
569
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
570
|
+
type LoginValue = string | { username: string; password: string };
|
|
571
|
+
/** What --with-token, the prompt and browser approval store. */
|
|
572
|
+
interface LoginTarget { label: string; basic: boolean; storedAs: string; save(value: LoginValue): StoredCreds }
|
|
573
|
+
|
|
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: Map<string, string | boolean>): LoginTarget | null {
|
|
577
|
+
const requested = flags.get("scheme");
|
|
578
|
+
if (requested !== undefined) {
|
|
579
|
+
if (typeof requested !== "string" || !Object.hasOwn(NAMED_SCHEMES, requested)) fail(2, "--scheme expects one of this API's security schemes: " + (Object.keys(NAMED_SCHEMES).join(", ") || "none") + ".");
|
|
580
|
+
const name = requested as string;
|
|
581
|
+
return { label: name, basic: NAMED_SCHEMES[name]!.kind === "basic", storedAs: name, save(value) {
|
|
582
|
+
try { return { named: parseNamedCredentials({ [name]: value }, NAMED_SCHEMES) }; } catch (error) { fail(2, (error as Error).message); }
|
|
583
|
+
} };
|
|
584
|
+
}
|
|
585
|
+
const candidates = [...AUTH_SCALARS.map((a) => a.option), ...(BASIC ? ["basicAuth"] : [])];
|
|
586
|
+
if (!candidates.length) return null;
|
|
587
|
+
const uses = (option: string) => OPS.filter((op) => op.credentialOptions?.some((alternative) => alternative.length === 1 && alternative[0] === option)).length;
|
|
588
|
+
const option = candidates.reduce((best, candidate) => uses(candidate) > uses(best) ? candidate : best);
|
|
589
|
+
if (option === "basicAuth") return { label: "username and password", basic: true, storedAs: "basic", save: (value) => ({ basic: value as { username: string; password: string } }) };
|
|
590
|
+
const scalar = AUTH_SCALARS.find((a) => a.option === option)!;
|
|
591
|
+
return { label: scalar.flag.replace(/-/g, " "), basic: false, storedAs: scalar.flag, save: (value) => ({ scalars: { [scalar.option]: value as string } }) };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Basic credentials on stdin are one line: username:password. */
|
|
595
|
+
function basicFromText(text: string): { username: string; password: string } {
|
|
596
|
+
const separator = text.indexOf(":");
|
|
597
|
+
if (separator <= 0 || separator === text.length - 1) fail(2, "--with-token expects username:password on stdin for Basic auth.");
|
|
598
|
+
return { username: text.slice(0, separator), password: text.slice(separator + 1) };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function storePastedToken(value: LoginValue, target: LoginTarget, flags: Map<string, string | boolean>): Promise<void> {
|
|
602
|
+
await saveLoginCredentials(target.save(value), flags);
|
|
603
|
+
out({ ok: true, method: "paste", stored_as: target.storedAs, credentials: credsPath(), ...loginIdentityReport() });
|
|
517
604
|
await flushExit(0);
|
|
518
605
|
}
|
|
519
606
|
|
|
@@ -547,10 +634,9 @@ async function browserApprove(headless: boolean, flags: Map<string, string | boo
|
|
|
547
634
|
}
|
|
548
635
|
|
|
549
636
|
/** Store what the browser approval minted, marked as this CLI's own. */
|
|
550
|
-
async function storeMinted(minted: ApprovedCredential, flags: Map<string, string | boolean>): Promise<void> {
|
|
551
|
-
const first = AUTH_SCALARS[0]!;
|
|
637
|
+
async function storeMinted(minted: ApprovedCredential, target: LoginTarget, flags: Map<string, string | boolean>): Promise<void> {
|
|
552
638
|
await saveLoginCredentials({
|
|
553
|
-
|
|
639
|
+
...target.save(minted.api_key),
|
|
554
640
|
minted: { via: "browser", key_name: minted.key_name, revocationUrl: minted.revocationUrl, ...(minted.org_id ? { org_id: minted.org_id } : {}) },
|
|
555
641
|
}, flags, undefined, async () => {
|
|
556
642
|
try {
|
|
@@ -567,16 +653,37 @@ async function storeMinted(minted: ApprovedCredential, flags: Map<string, string
|
|
|
567
653
|
});
|
|
568
654
|
}
|
|
569
655
|
|
|
570
|
-
async function browserLogin(headless: boolean, flags: Map<string, string | boolean>): Promise<void> {
|
|
656
|
+
async function browserLogin(headless: boolean, target: LoginTarget, flags: Map<string, string | boolean>): Promise<void> {
|
|
571
657
|
await credentialStore().prepare();
|
|
572
658
|
const minted = await browserApprove(headless, flags);
|
|
573
|
-
await storeMinted(minted, flags);
|
|
574
|
-
out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
|
|
659
|
+
await storeMinted(minted, target, flags);
|
|
660
|
+
out({ ok: true, method: "browser", stored_as: target.storedAs, key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
|
|
575
661
|
await flushExit(0);
|
|
576
662
|
}
|
|
577
663
|
|
|
664
|
+
/** The callback URL browser login uses: the configured redirect URI, else
|
|
665
|
+
* http://127.0.0.1:<port>/callback. --redirect-port or
|
|
666
|
+
* TYPESHIP_OAUTH_REDIRECT_PORT picks another port. */
|
|
667
|
+
function oauthRedirectUri(flags: Map<string, string | boolean>): string {
|
|
668
|
+
const requested = typeof flags.get("redirect-port") === "string" ? flags.get("redirect-port") as string : process.env["TYPESHIP_OAUTH_REDIRECT_PORT"];
|
|
669
|
+
const redirect = new URL(OAUTH_REDIRECT_URI ?? "http://127.0.0.1:" + OAUTH_DEFAULT_REDIRECT_PORT + "/callback");
|
|
670
|
+
if (requested !== undefined) {
|
|
671
|
+
if (!/^[1-9][0-9]{0,4}$/.test(requested) || Number(requested) > 65535) fail(2, "--redirect-port expects a port number from 1 to 65535.");
|
|
672
|
+
redirect.port = requested;
|
|
673
|
+
}
|
|
674
|
+
return redirect.href;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** --scopes a,b narrows (or widens) what this login asks for. */
|
|
678
|
+
function loginScopes(flags: Map<string, string | boolean>): string[] {
|
|
679
|
+
const requested = flags.get("scopes");
|
|
680
|
+
if (requested === undefined) return OAUTH_SCOPES;
|
|
681
|
+
if (typeof requested !== "string" || !requested.trim()) fail(2, "--scopes expects a comma- or space-separated list of scopes.");
|
|
682
|
+
return [...new Set((requested as string).split(/[\s,]+/).filter(Boolean))];
|
|
683
|
+
}
|
|
684
|
+
|
|
578
685
|
async function acquireOAuthBrowserSession(parsed: Parsed, clientId: string): Promise<void> {
|
|
579
|
-
if (!OAUTH_ISSUER) fail(2, "Browser OAuth
|
|
686
|
+
if (!OAUTH_ISSUER && !OAUTH_AUTHORIZATION_URL) fail(2, "Browser OAuth needs an authorization URL: the API Spec's authorizationCode flow, or auth.oauth_server.issuer configured by the API owner.");
|
|
580
687
|
const apiBaseUrl = resolveBaseUrl(parsed.flags);
|
|
581
688
|
if (!apiBaseUrl) fail(2, "Set the API base URL before logging in.");
|
|
582
689
|
const loginConfiguration = sessionConfiguration(apiBaseUrl!);
|
|
@@ -588,7 +695,7 @@ async function acquireOAuthBrowserSession(parsed: Parsed, clientId: string): Pro
|
|
|
588
695
|
} }, parsed.flags, loginConfiguration);
|
|
589
696
|
}
|
|
590
697
|
|
|
591
|
-
/**
|
|
698
|
+
/** The native browser exchange. */
|
|
592
699
|
async function startOAuthBrowserSession(parsed: Parsed, clientId: string, timeoutMs?: number): Promise<OAuthLoginSession> {
|
|
593
700
|
const controller = new AbortController();
|
|
594
701
|
const cancel = () => controller.abort();
|
|
@@ -596,9 +703,9 @@ async function startOAuthBrowserSession(parsed: Parsed, clientId: string, timeou
|
|
|
596
703
|
process.once("SIGTERM", cancel);
|
|
597
704
|
try {
|
|
598
705
|
return await oauthBrowserLogin({
|
|
599
|
-
issuer: OAUTH_ISSUER
|
|
706
|
+
issuer: OAUTH_ISSUER, clientId, discoveryUrl: OAUTH_DISCOVERY_URL,
|
|
600
707
|
authorizationUrl: OAUTH_AUTHORIZATION_URL, tokenUrl: OAUTH_TOKEN_URL ?? undefined,
|
|
601
|
-
redirectUri:
|
|
708
|
+
redirectUri: oauthRedirectUri(parsed.flags), scopes: loginScopes(parsed.flags),
|
|
602
709
|
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
603
710
|
organization: requestedLoginOrganization(parsed.flags),
|
|
604
711
|
}, { signal: controller.signal, timeoutMs, authorize(url) {
|
|
@@ -609,56 +716,23 @@ async function startOAuthBrowserSession(parsed: Parsed, clientId: string, timeou
|
|
|
609
716
|
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
|
610
717
|
}
|
|
611
718
|
|
|
612
|
-
async function cmdConsoleLoginCheck(parsed: Parsed): Promise<void> {
|
|
613
|
-
const file = parsed.flags.get("console-check");
|
|
614
|
-
if (typeof file !== "string" || !file || parsed.flags.get("device") === true || parsed.flags.get("with-token") === true || explicitNonInteractive(parsed)) fail(2, "--console-check requires a downloaded JSON file and browser login. Use --no-browser to print the sign-in link.");
|
|
615
|
-
const baseUrl = resolveBaseUrl(parsed.flags);
|
|
616
|
-
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") as string : undefined) ?? process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
|
|
617
|
-
const op = WHOAMI && OPS.find((value) => value.resource === WHOAMI.resource && value.method === WHOAMI.method);
|
|
618
|
-
if (!baseUrl || !OAUTH_ISSUER || !clientId || OAUTH_LOGIN_METHOD !== "browser" || !op || op.auth !== "required" || !op.security?.length) fail(2, "Configure browser OAuth and a required-authentication identity read, then regenerate this CLI.");
|
|
619
|
-
const envOptions: Record<string, unknown> = {}, flagOptions: Record<string, unknown> = {};
|
|
620
|
-
for (const scalar of AUTH_SCALARS) {
|
|
621
|
-
if (process.env[scalar.env] !== undefined) envOptions[scalar.option] = process.env[scalar.env];
|
|
622
|
-
if (typeof parsed.flags.get(scalar.flag) === "string") flagOptions[scalar.option] = parsed.flags.get(scalar.flag);
|
|
623
|
-
}
|
|
624
|
-
if (BASIC && process.env[BASIC.envUser] && process.env[BASIC.envPass]) envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
|
|
625
|
-
if (BASIC && typeof parsed.flags.get("username") === "string" && typeof parsed.flags.get("password") === "string") flagOptions.basicAuth = { username: parsed.flags.get("username"), password: parsed.flags.get("password") };
|
|
626
|
-
const environment = (typeof parsed.flags.get("environment") === "string" ? parsed.flags.get("environment") as string : Object.entries(ENVIRONMENTS).find(([, url]) => new URL(url).href === new URL(baseUrl!).href)?.[0]) ?? null;
|
|
627
|
-
const result = await checkConsoleBrowserLogin({
|
|
628
|
-
file: file as string, configuration: {
|
|
629
|
-
baseUrl: baseUrl!, environment, operation: op!.resource + "." + op!.method, requirements: op!.security!,
|
|
630
|
-
request: { method: op!.graphql ? "POST" : op!.httpMethod, path: op!.graphql ? "" : op!.path, graphqlField: op!.graphql?.field ?? null, graphqlQuery: op!.graphql ? op!.graphql.docPrefix + op!.graphql.defaultSelection + " }" : null },
|
|
631
|
-
issuer: OAUTH_ISSUER!, clientId: clientId!, discoveryUrl: OAUTH_DISCOVERY_URL ?? null,
|
|
632
|
-
authorizationUrl: OAUTH_AUTHORIZATION_URL ?? null, tokenUrl: OAUTH_TOKEN_URL,
|
|
633
|
-
redirectUri: OAUTH_REDIRECT_URI ?? "http://127.0.0.1/callback", scopes: OAUTH_SCOPES,
|
|
634
|
-
audience: OAUTH_TOKEN_PARAMS.audience ?? null, resource: OAUTH_TOKEN_PARAMS.resource ?? null,
|
|
635
|
-
}, schemes: NAMED_SCHEMES,
|
|
636
|
-
credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ options: envOptions, named: environmentCredentials() }, { options: flagOptions, named: flagCredentials(parsed.flags) }]),
|
|
637
|
-
login: (timeoutMs) => startOAuthBrowserSession(parsed, clientId!, timeoutMs),
|
|
638
|
-
async verify(credentials, expectations) {
|
|
639
|
-
const kind = (value: "user" | "account" | "organization") => value === "user" ? "subject" : value;
|
|
640
|
-
const policy = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), entry.pointer])) as IdentityPolicy;
|
|
641
|
-
const expected = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), String(entry.expected)])) as ApiIdentity;
|
|
642
|
-
await verifyClientIdentity((options) => new TypeshipClient(options as unknown as ClientOptions), { baseUrl: baseUrl!, credentials }, op!, policy, expected);
|
|
643
|
-
},
|
|
644
|
-
progress: (message) => process.stderr.write(message + "\n"),
|
|
645
|
-
});
|
|
646
|
-
out(result);
|
|
647
|
-
await flushExit(0);
|
|
648
|
-
}
|
|
649
719
|
|
|
650
720
|
async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
651
721
|
if (parsed.help) {
|
|
722
|
+
const target = loginTarget(new Map());
|
|
652
723
|
const lines = [
|
|
653
724
|
BIN + " login — store credentials at " + credsPath(),
|
|
654
725
|
"",
|
|
655
726
|
...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
|
|
656
|
-
...(CLI_AUTH_URL ? [" " + BIN + " login approve in the browser: a key is minted for you (add --no-browser to print the link instead of opening it)"] : []),
|
|
657
|
-
" " + BIN + " login --with-token read the
|
|
658
|
-
...(
|
|
659
|
-
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --
|
|
727
|
+
...(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)"] : []),
|
|
728
|
+
...(target ? [" " + BIN + " login --with-token read the " + target.label + " from stdin" + (target.basic ? " as one username:password line" : "") + " (CI)"] : []),
|
|
729
|
+
...(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"] : []),
|
|
730
|
+
...(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")] : []),
|
|
731
|
+
...(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)"] : []),
|
|
732
|
+
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --scopes <a,b> request these scopes instead of " + (OAUTH_SCOPES.length ? OAUTH_SCOPES.join(" ") : "the provider's defaults")] : []),
|
|
733
|
+
...(HAS_OAUTH_LOGIN && (OAUTH_DEVICE_URL || OAUTH_ISSUER || OAUTH_DISCOVERY_URL) ? [" " + BIN + " login --device use device authorization when your provider supports it"] : []),
|
|
660
734
|
...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
|
|
661
|
-
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
735
|
+
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN || !target ? [] : [" " + BIN + " login interactive prompt" + (target.basic ? " for username and hidden password" : "") + " (TTY only)"]),
|
|
662
736
|
"",
|
|
663
737
|
"Precedence per scheme: flags > env vars > stored credentials. Named inputs beat convenience flags within the same source.",
|
|
664
738
|
" " + BIN + " login --credentials @<JSON-file> store named credentials (use - for stdin)",
|
|
@@ -674,9 +748,8 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
674
748
|
}
|
|
675
749
|
if (parsed.flags.has("login-organization")) {
|
|
676
750
|
expectedLoginIdentity(parsed.flags);
|
|
677
|
-
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("
|
|
751
|
+
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("with-token") || explicitNonInteractive(parsed)) fail(2, "--login-organization is available only for interactive OAuth browser login, including --no-browser.");
|
|
678
752
|
}
|
|
679
|
-
if (parsed.flags.has("console-check")) { await cmdConsoleLoginCheck(parsed); return; }
|
|
680
753
|
const named = flagCredentials(parsed.flags);
|
|
681
754
|
const scalarValues: Record<string, string> = {};
|
|
682
755
|
for (const a of AUTH_SCALARS) {
|
|
@@ -698,9 +771,11 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
698
771
|
}
|
|
699
772
|
|
|
700
773
|
if (parsed.flags.get("with-token") === true) {
|
|
774
|
+
const target = loginTarget(parsed.flags);
|
|
775
|
+
if (!target) fail(2, "This API declares no credential the CLI can store. See '" + BIN + " login --help'.");
|
|
701
776
|
const token = (await readStdin()).trim();
|
|
702
777
|
if (!token) fail(2, "--with-token expects the credential on stdin.");
|
|
703
|
-
await storePastedToken(token, parsed.flags);
|
|
778
|
+
await storePastedToken(target!.basic ? basicFromText(token) : token, target!, parsed.flags);
|
|
704
779
|
}
|
|
705
780
|
|
|
706
781
|
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") as string : undefined)
|
|
@@ -717,8 +792,9 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
717
792
|
// Browser approval: the API mints a key for this CLI once a person
|
|
718
793
|
// approves in the browser. Works under an agent too (it prints the URL and
|
|
719
794
|
// polls); only the explicit non-interactive switch turns it off.
|
|
720
|
-
|
|
721
|
-
|
|
795
|
+
const target = loginTarget(parsed.flags);
|
|
796
|
+
if (CLI_AUTH_URL && target && !target.basic && !explicitNonInteractive(parsed)) {
|
|
797
|
+
await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, target, parsed.flags);
|
|
722
798
|
}
|
|
723
799
|
|
|
724
800
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
@@ -728,17 +804,24 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
728
804
|
message: "login needs a terminal to prompt, and there is none.",
|
|
729
805
|
nextSteps: [
|
|
730
806
|
...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
|
|
731
|
-
"
|
|
807
|
+
...(BASIC ? ["Pass Basic credentials: '" + BIN + " login --username <u> --password <p>', or set " + BASIC.envUser + " and " + BASIC.envPass + " in the environment."] : []),
|
|
808
|
+
...(target ? ["Pipe it: echo \"" + (target.basic ? "$USERNAME:$PASSWORD" : "$TOKEN") + "\" | " + BIN + " login --with-token" + (parsed.flags.has("scheme") ? " --scheme " + target.storedAs : "")] : []),
|
|
732
809
|
...(HAS_OAUTH_LOGIN ? ["OAuth login (or --device when supported by your provider): '" + BIN + " login --client-id <id>' (opens or prints the provider sign-in URL)."] : []),
|
|
733
810
|
...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
|
|
734
811
|
],
|
|
735
812
|
});
|
|
736
813
|
}
|
|
737
|
-
|
|
738
|
-
if (
|
|
739
|
-
|
|
814
|
+
if (!target) fail(2, "This API declares no credential the CLI can prompt for. See '" + BIN + " login --help'.");
|
|
815
|
+
if (target!.basic) {
|
|
816
|
+
process.stderr.write("Username: ");
|
|
817
|
+
const username = (await readLine()).trim();
|
|
818
|
+
const password = await promptHidden("Password (input hidden): ");
|
|
819
|
+
if (!username || !password) fail(2, "Enter both a username and a password.");
|
|
820
|
+
await storePastedToken({ username, password }, target!, parsed.flags);
|
|
821
|
+
}
|
|
822
|
+
const token = (await promptHidden("Paste " + target!.label + " (input hidden): ")).trim();
|
|
740
823
|
if (!token) fail(2, "Nothing entered.");
|
|
741
|
-
await storePastedToken(token, parsed.flags);
|
|
824
|
+
await storePastedToken(token, target!, parsed.flags);
|
|
742
825
|
}
|
|
743
826
|
|
|
744
827
|
async function cmdLogout(parsed: Parsed): Promise<void> {
|
|
@@ -753,8 +836,8 @@ async function cmdLogout(parsed: Parsed): Promise<void> {
|
|
|
753
836
|
// way out, so logging out ends the credential and not just the file. A
|
|
754
837
|
// pasted or CI key is someone else's to revoke, and is left alone.
|
|
755
838
|
let revoked: boolean | null = null;
|
|
756
|
-
|
|
757
|
-
const ownKey =
|
|
839
|
+
// The approval stored exactly one credential, as a scalar or a named scheme.
|
|
840
|
+
const ownKey = stored?.minted?.via === "browser" ? [...Object.values(stored.scalars ?? {}), ...Object.values(stored.named ?? {})].find((value): value is string => typeof value === "string") : undefined;
|
|
758
841
|
if (ownKey) {
|
|
759
842
|
try {
|
|
760
843
|
const response = await oauthStatusRequest(loginEndpoint(stored!.minted!.revocationUrl!), { method: "POST", headers: { Authorization: "Bearer " + ownKey } }, 15_000);
|
|
@@ -915,12 +998,12 @@ function mcpEntryFor(url: string | undefined, readOnly = false): { entry: McpEnt
|
|
|
915
998
|
const warnings: string[] = [];
|
|
916
999
|
const hosted = url ?? MCP_URL ?? undefined;
|
|
917
1000
|
if (hosted) {
|
|
918
|
-
const envVar = AUTH_SCALARS[0]?.env;
|
|
919
1001
|
// The hosted endpoint serves its read-only twin at <url>/readonly; a
|
|
920
1002
|
// remote server of someone else's may not, so say so.
|
|
921
1003
|
const target = readOnly ? hosted.replace(/\/+$/, "") + "/readonly" : hosted;
|
|
922
|
-
if (readOnly && url !== undefined && !MCP_URL) warnings.push("--read-only appended /readonly to the URL
|
|
923
|
-
|
|
1004
|
+
if (readOnly && url !== undefined && !MCP_URL) warnings.push("--read-only appended /readonly to the URL; check that this server serves a read-only endpoint there.");
|
|
1005
|
+
if (HOSTED_MCP_NOTE) warnings.push(HOSTED_MCP_NOTE);
|
|
1006
|
+
return { entry: { url: target, ...(Object.keys(HOSTED_MCP_HEADERS).length ? { headers: { ...HOSTED_MCP_HEADERS } } : {}) }, warnings };
|
|
924
1007
|
}
|
|
925
1008
|
if (!HAS_MCP) {
|
|
926
1009
|
fail(2, "This package was generated without the MCP server target. Regenerate with it, or pass --url for a remote endpoint.");
|
|
@@ -992,7 +1075,7 @@ async function cmdMcp(parsed: Parsed): Promise<void> {
|
|
|
992
1075
|
|
|
993
1076
|
if (wanted.size === 0) {
|
|
994
1077
|
out({
|
|
995
|
-
server:
|
|
1078
|
+
server: MCP_SERVER_KEY,
|
|
996
1079
|
entry,
|
|
997
1080
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
998
1081
|
detected: MCP_CLIENTS.filter((c) => c.detect(cwd)).map((c) => c.id),
|
|
@@ -1003,11 +1086,11 @@ async function cmdMcp(parsed: Parsed): Promise<void> {
|
|
|
1003
1086
|
const results: McpWriteResult[] = [];
|
|
1004
1087
|
for (const id of wanted) {
|
|
1005
1088
|
const client = findMcpClient(id)!;
|
|
1006
|
-
results.push(writeMcpConfig(client, cwd,
|
|
1089
|
+
results.push(writeMcpConfig(client, cwd, MCP_SERVER_KEY, entry));
|
|
1007
1090
|
}
|
|
1008
1091
|
out({
|
|
1009
1092
|
ok: true,
|
|
1010
|
-
server:
|
|
1093
|
+
server: MCP_SERVER_KEY,
|
|
1011
1094
|
entry,
|
|
1012
1095
|
written: results.filter((r) => r.written).map((r) => r.file),
|
|
1013
1096
|
clients: results,
|
|
@@ -1029,10 +1112,11 @@ function agentContext(): AgentContext {
|
|
|
1029
1112
|
version: VERSION,
|
|
1030
1113
|
envPrefix: ENV_PREFIX,
|
|
1031
1114
|
authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? ["TYPESHIP_CREDENTIALS"] : [])],
|
|
1115
|
+
...(AUTH_UNDECLARED ? { authNotDeclared: true } : {}),
|
|
1032
1116
|
docsUrl: docsSiteUrl(),
|
|
1033
1117
|
docsIndexUrl: docsIndexUrl(),
|
|
1034
1118
|
generatedOperationCount: OPS.length,
|
|
1035
|
-
|
|
1119
|
+
omittedOperationCount: OMITTED_OPS.length,
|
|
1036
1120
|
mcpUrl: MCP_URL,
|
|
1037
1121
|
skillsRepo: SKILLS_REPO,
|
|
1038
1122
|
hasMcp: HAS_MCP,
|
|
@@ -1097,8 +1181,6 @@ function helpJson(): Record<string, unknown> {
|
|
|
1097
1181
|
coverage: {
|
|
1098
1182
|
generated_operations: OPS.length,
|
|
1099
1183
|
total_operations: OPS.length + EXCLUDED_OPS,
|
|
1100
|
-
omitted_operations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
|
|
1101
|
-
reason: "plan_limit",
|
|
1102
1184
|
},
|
|
1103
1185
|
} : {}),
|
|
1104
1186
|
discovery: {
|
|
@@ -1107,7 +1189,7 @@ function helpJson(): Record<string, unknown> {
|
|
|
1107
1189
|
note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
|
|
1108
1190
|
},
|
|
1109
1191
|
builtins: BUILTIN_COMMANDS,
|
|
1110
|
-
global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
|
|
1192
|
+
global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--header 'Name: value'", "--timeout <seconds>", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
|
|
1111
1193
|
auth_env_vars: agentContext().authEnvVars,
|
|
1112
1194
|
};
|
|
1113
1195
|
}
|
|
@@ -1139,24 +1221,26 @@ async function cmdAuth(parsed: Parsed): Promise<void> {
|
|
|
1139
1221
|
}
|
|
1140
1222
|
if (parsed.help || sub !== "check") {
|
|
1141
1223
|
process.stdout.write([
|
|
1142
|
-
BIN + " auth check [--
|
|
1224
|
+
BIN + " auth check [--offline] — report the credential the CLI would use and verify it with the API's identity read, as JSON: {status: ok|action_required, authenticated, verification: verified|unverified|rejected, source, ...}",
|
|
1143
1225
|
" " + BIN + " auth profiles list profiles without unlocking credentials",
|
|
1144
1226
|
" " + BIN + " auth use <name> select the default profile",
|
|
1145
1227
|
" " + BIN + " auth remove <name> remove a profile after logout",
|
|
1146
1228
|
" --profile <name> overrides TYPESHIP_PROFILE, then the saved selection, then default.",
|
|
1147
|
-
" --
|
|
1229
|
+
" --offline skip the identity read; the credential is then reported as unverified" + (WHOAMI ? "" : " (this API has no identity read, so credentials are always unverified)"),
|
|
1148
1230
|
"",
|
|
1149
1231
|
"Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
|
|
1150
1232
|
].join("\n") + "\n");
|
|
1151
1233
|
await flushExit(parsed.help ? 0 : 2);
|
|
1152
1234
|
}
|
|
1153
1235
|
const source = credentialSource(parsed.flags) ?? "none";
|
|
1154
|
-
const
|
|
1236
|
+
const present = source !== "none";
|
|
1155
1237
|
const savedIdentity = source === "login" ? readCreds() : null;
|
|
1156
1238
|
if (savedIdentity) assertStoredIdentity(savedIdentity, identityConfiguration());
|
|
1239
|
+
// A credential counts as authenticated only once the API accepted it.
|
|
1157
1240
|
const report: Record<string, unknown> = {
|
|
1158
|
-
status:
|
|
1159
|
-
authenticated,
|
|
1241
|
+
status: present ? "ok" : "action_required",
|
|
1242
|
+
authenticated: false,
|
|
1243
|
+
verification: present ? "unverified" : "none",
|
|
1160
1244
|
source,
|
|
1161
1245
|
credentials_path: existsSync(credsPath()) ? credsPath() : null,
|
|
1162
1246
|
credential_storage: credentialStore().backend,
|
|
@@ -1164,12 +1248,13 @@ async function cmdAuth(parsed: Parsed): Promise<void> {
|
|
|
1164
1248
|
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
1165
1249
|
auth_env_vars: agentContext().authEnvVars,
|
|
1166
1250
|
base_url: resolveBaseUrl(parsed.flags) ?? null,
|
|
1167
|
-
next_steps:
|
|
1251
|
+
next_steps: present ? [WHOAMI ? "Run '" + BIN + " auth check' without --offline to verify the credential." : "This API has no identity read, so the credential was not checked. Run a read command to confirm the API accepts it."] : [
|
|
1168
1252
|
...AUTH_SCALARS.map((a) => "Set " + a.env + " in the environment, or run '" + BIN + " login --" + a.flag + " <value>'."),
|
|
1169
|
-
|
|
1253
|
+
...(AUTH_UNDECLARED ? ["The API Spec does not declare authentication. Add another header with --header \"Name: value\" or TYPESHIP_HEADERS."] : []),
|
|
1254
|
+
...(WHOAMI ? ["Then run '" + BIN + " auth check'."] : []),
|
|
1170
1255
|
],
|
|
1171
1256
|
};
|
|
1172
|
-
if (
|
|
1257
|
+
if (present && parsed.flags.get("offline") !== true && WHOAMI) {
|
|
1173
1258
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1174
1259
|
if (op) {
|
|
1175
1260
|
const client = await makeClient(parsed.flags, op);
|
|
@@ -1178,9 +1263,12 @@ async function cmdAuth(parsed: Parsed): Promise<void> {
|
|
|
1178
1263
|
if (result.ok) {
|
|
1179
1264
|
if (identityConfiguration() && savedIdentity?.identity) assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
|
|
1180
1265
|
report.identity = result.data;
|
|
1266
|
+
// An identity read that also answers anonymous callers proves nothing.
|
|
1267
|
+
if (op.auth !== "none") { report.authenticated = true; report.verification = "verified"; report.next_steps = []; }
|
|
1181
1268
|
}
|
|
1182
1269
|
else {
|
|
1183
|
-
const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1270
|
+
const why = classifyApiError(result.error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1271
|
+
report.verification = "rejected";
|
|
1184
1272
|
report.status = "action_required";
|
|
1185
1273
|
report.live = { ok: false, code: why.code, message: why.message };
|
|
1186
1274
|
report.next_steps = why.nextSteps ?? [];
|
|
@@ -1209,7 +1297,13 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
|
|
|
1209
1297
|
if (baseUrl) {
|
|
1210
1298
|
try {
|
|
1211
1299
|
const response = await fetch(baseUrl, { method: "GET", signal: AbortSignal.timeout(8_000) });
|
|
1212
|
-
|
|
1300
|
+
void response.body?.cancel().catch(() => {});
|
|
1301
|
+
const reachable = response.status >= 200 && response.status < 300;
|
|
1302
|
+
checks.push({ name: "base_url", ok: reachable, detail: baseUrl + " → HTTP " + response.status, ...(reachable ? {} : { fix: response.status === 404 || response.status === 405
|
|
1303
|
+
? "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."
|
|
1304
|
+
: response.status === 401 || response.status === 403
|
|
1305
|
+
? "The API answered without a credential with HTTP " + response.status + "; the identity check below tests the credential."
|
|
1306
|
+
: "The API answered HTTP " + response.status + ". Check the base URL and the API's status." }) });
|
|
1213
1307
|
} catch (e) {
|
|
1214
1308
|
checks.push({ name: "base_url", ok: false, detail: baseUrl + ": " + (e as Error).message, fix: "Check the network, or set --base-url / " + ENV_PREFIX + "_BASE_URL." });
|
|
1215
1309
|
}
|
|
@@ -1223,7 +1317,13 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
|
|
|
1223
1317
|
const client = await makeClient(parsed.flags, op);
|
|
1224
1318
|
const target = (client as unknown as Record<string, Record<string, () => Promise<{ ok: boolean; error?: unknown }>>>)[op.resource]!;
|
|
1225
1319
|
const result = await asApiResult(target[op.method]!());
|
|
1226
|
-
|
|
1320
|
+
const status = (result.error as { status?: unknown } | undefined)?.status;
|
|
1321
|
+
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,
|
|
1322
|
+
// Only 401 and 403 are about the credential. A 404 or 405 means the
|
|
1323
|
+
// API no longer has this endpoint where this CLI version expects it.
|
|
1324
|
+
fix: status === 401 || status === 403 ? "The credential was rejected; run '" + BIN + " login' with a current one."
|
|
1325
|
+
: status === 404 || status === 405 ? "The API does not know " + wireOf(op) + ", which this CLI version calls. Run '" + BIN + " upgrade', and check the base URL."
|
|
1326
|
+
: "The identity read failed; the detail says why." });
|
|
1227
1327
|
} catch (e) {
|
|
1228
1328
|
checks.push({ name: "identity", ok: false, detail: (e as Error).message });
|
|
1229
1329
|
}
|
|
@@ -1236,7 +1336,7 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
|
|
|
1236
1336
|
}
|
|
1237
1337
|
if (HAS_MCP || MCP_URL) {
|
|
1238
1338
|
const cwd = process.cwd();
|
|
1239
|
-
const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd,
|
|
1339
|
+
const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd, MCP_SERVER_KEY)).map((c) => c.id);
|
|
1240
1340
|
const detected = MCP_CLIENTS.filter((c) => c.detect(cwd) && !c.incompatible).map((c) => c.id);
|
|
1241
1341
|
checks.push({ name: "mcp_clients", ok: detected.length === 0 || configured.length > 0, detail: "detected: " + (detected.join(", ") || "none") + "; configured: " + (configured.join(", ") || "none"), ...(detected.length > 0 && configured.length === 0 ? { fix: "Run '" + BIN + " mcp install --all'." } : {}) });
|
|
1242
1342
|
}
|
|
@@ -1276,13 +1376,18 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1276
1376
|
const first = AUTH_SCALARS[0];
|
|
1277
1377
|
const named = flagCredentials(parsed.flags), envNamed = environmentCredentials();
|
|
1278
1378
|
const stored: StoredCreds = Object.keys(named).length || Object.keys(envNamed).length || first && process.env[first.env] ? {} : readCreds() ?? {};
|
|
1279
|
-
|
|
1280
|
-
|
|
1379
|
+
// -k stores the credential login would store; --<flag> stores that scalar.
|
|
1380
|
+
const target = loginTarget(parsed.flags);
|
|
1381
|
+
const keyValue = typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") as string : undefined;
|
|
1382
|
+
const flagValue = first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) as string : undefined;
|
|
1281
1383
|
if (Object.keys(named).length) {
|
|
1282
1384
|
await saveLoginCredentials({ named }, parsed.flags);
|
|
1283
1385
|
report.credential = { status: "stored", path: credsPath() };
|
|
1284
|
-
} else if (
|
|
1285
|
-
await saveLoginCredentials(
|
|
1386
|
+
} else if (keyValue && target && !target.basic) {
|
|
1387
|
+
await saveLoginCredentials(target.save(keyValue), parsed.flags);
|
|
1388
|
+
report.credential = { status: "stored", path: credsPath() };
|
|
1389
|
+
} else if ((keyValue ?? flagValue) && first) {
|
|
1390
|
+
await saveLoginCredentials({ scalars: { [first.option]: (keyValue ?? flagValue)! } }, parsed.flags);
|
|
1286
1391
|
report.credential = { status: "stored", path: credsPath() };
|
|
1287
1392
|
} else if (first && process.env[first.env]) {
|
|
1288
1393
|
report.credential = { status: "env", variable: first.env };
|
|
@@ -1293,13 +1398,13 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1293
1398
|
} else if (first && HAS_OAUTH_LOGIN && OAUTH_LOGIN_METHOD === "browser" && (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID) && !explicitNonInteractive(parsed)) {
|
|
1294
1399
|
await acquireOAuthBrowserSession(parsed, (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID)!);
|
|
1295
1400
|
report.credential = { status: "stored", method: "oauth_browser", path: credsPath() };
|
|
1296
|
-
} else if (
|
|
1401
|
+
} else if (target && !target.basic && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
|
|
1297
1402
|
// Nothing anywhere: approve a credential in the browser, as `login`
|
|
1298
1403
|
// would, then carry on. Under an agent the URL is printed for the person
|
|
1299
1404
|
// and polled; only the explicit non-interactive switch skips this.
|
|
1300
1405
|
await credentialStore().prepare();
|
|
1301
1406
|
const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
1302
|
-
await storeMinted(minted, parsed.flags);
|
|
1407
|
+
await storeMinted(minted, target, parsed.flags);
|
|
1303
1408
|
report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
|
|
1304
1409
|
} else {
|
|
1305
1410
|
report.credential = { status: "none" };
|
|
@@ -1317,7 +1422,7 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1317
1422
|
} else {
|
|
1318
1423
|
const { entry, warnings } = mcpEntryFor(undefined);
|
|
1319
1424
|
const clients = MCP_CLIENTS.filter((c) => !c.incompatible && c.detect(cwd) && !(c.id === "claude-desktop" && entry.url));
|
|
1320
|
-
const results = clients.map((c) => writeMcpConfig(c, cwd,
|
|
1425
|
+
const results = clients.map((c) => writeMcpConfig(c, cwd, MCP_SERVER_KEY, entry));
|
|
1321
1426
|
report.mcp = { status: results.length > 0 ? "written" : "no-clients", entry, clients: results, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
1322
1427
|
if (results.length === 0) nextSteps.push("No MCP client was found on this machine; run '" + BIN + " mcp' to print the entry.");
|
|
1323
1428
|
}
|
|
@@ -1331,7 +1436,7 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1331
1436
|
report.agents_md = { status: result.updated ? "updated" : "written", file: result.file };
|
|
1332
1437
|
}
|
|
1333
1438
|
|
|
1334
|
-
nextSteps.push("Run '" + BIN + " auth check
|
|
1439
|
+
nextSteps.push("Run '" + BIN + " auth check'" + (WHOAMI ? "" : " (or any read command)") + " to confirm the connection.");
|
|
1335
1440
|
nextSteps.push("Run '" + BIN + " agent-guide' for the conventions, or '" + BIN + " --help' for commands.");
|
|
1336
1441
|
out({ ...report, next_steps: nextSteps });
|
|
1337
1442
|
await flushExit(0);
|
|
@@ -1357,6 +1462,7 @@ function registryBase(): string {
|
|
|
1357
1462
|
}
|
|
1358
1463
|
|
|
1359
1464
|
async function latestVersion(timeoutMs: number): Promise<string | null> {
|
|
1465
|
+
if (!PKG_CONFIRMED) return null;
|
|
1360
1466
|
try {
|
|
1361
1467
|
const response = await fetch(registryBase() + "/" + PKG_NAME, {
|
|
1362
1468
|
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
@@ -1383,6 +1489,9 @@ async function cmdUpgrade(parsed: Parsed): Promise<void> {
|
|
|
1383
1489
|
process.stdout.write(lines.join("\n") + "\n");
|
|
1384
1490
|
await flushExit(0);
|
|
1385
1491
|
}
|
|
1492
|
+
if (!PKG_CONFIRMED) {
|
|
1493
|
+
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.");
|
|
1494
|
+
}
|
|
1386
1495
|
const latest = await latestVersion(5000);
|
|
1387
1496
|
if (latest === null) {
|
|
1388
1497
|
fail(1, PKG_NAME + " is not on the registry (" + registryBase() + "). This package updates by regeneration from its API spec; get the latest from the API provider.");
|
|
@@ -1434,7 +1543,7 @@ function completionFlagsFor(op: OpSpec): { flags: string[]; values: Record<strin
|
|
|
1434
1543
|
return { flags, values };
|
|
1435
1544
|
}
|
|
1436
1545
|
|
|
1437
|
-
const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
|
|
1546
|
+
const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--header", "--timeout", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
|
|
1438
1547
|
const BUILTIN_WORDS: Record<string, string[]> = {
|
|
1439
1548
|
config: ["list", "get", "set", "unset", "path"],
|
|
1440
1549
|
completion: ["bash", "zsh", "fish"],
|
|
@@ -1574,34 +1683,6 @@ async function fetchDocs(pathOrFile: string): Promise<string | null> {
|
|
|
1574
1683
|
}
|
|
1575
1684
|
}
|
|
1576
1685
|
|
|
1577
|
-
function searchTerms(query: string): string[] {
|
|
1578
|
-
return [...new Set(query.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 2))];
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
/** Token search across names, prose, paths, and arguments. A phrase such as
|
|
1582
|
-
* "create project" should find the projects create command, even though that exact
|
|
1583
|
-
* substring never occurs in the generated command. */
|
|
1584
|
-
function referenceSearchScore(op: OpSpec, query: string): number {
|
|
1585
|
-
const terms = searchTerms(query);
|
|
1586
|
-
if (terms.length === 0) return 0;
|
|
1587
|
-
const names = [...op.command, op.tool].join("_").toLowerCase().split(/[^a-z0-9]+/);
|
|
1588
|
-
const summary = (op.summary ?? "").toLowerCase();
|
|
1589
|
-
const description = (op.description ?? "").toLowerCase();
|
|
1590
|
-
const path = op.path.toLowerCase();
|
|
1591
|
-
const params = op.params.flatMap((p) => [p.name.toLowerCase(), p.flag.toLowerCase()]);
|
|
1592
|
-
let score = 0;
|
|
1593
|
-
for (const term of terms) {
|
|
1594
|
-
if (names.includes(term)) score += 10;
|
|
1595
|
-
if (summary.split(/[^a-z0-9]+/).includes(term)) score += 5;
|
|
1596
|
-
else if (summary.includes(term)) score += 3;
|
|
1597
|
-
if (path.includes(term)) score += 3;
|
|
1598
|
-
if (params.includes(term)) score += 3;
|
|
1599
|
-
else if (params.some((param) => param.includes(term))) score += 1;
|
|
1600
|
-
if (description.includes(term)) score += 1;
|
|
1601
|
-
}
|
|
1602
|
-
return score;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
1686
|
function referenceFor(op: OpSpec, includeSchemas = false): string {
|
|
1606
1687
|
const lines: string[] = [];
|
|
1607
1688
|
lines.push(paintOut("bold", usageLine(op)));
|
|
@@ -1690,23 +1771,22 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1690
1771
|
const term = parsed.positionals.slice(2).join(" ");
|
|
1691
1772
|
if (!term) fail(2, "docs search expects a term");
|
|
1692
1773
|
const jsonOutput = parsed.flags.get("json") === true || parsed.flags.get("format") === "json";
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
.sort((a, b) => b.score - a.score || a.op.command.join(" ").localeCompare(b.op.command.join(" ")))
|
|
1696
|
-
.map((match) => match.op);
|
|
1774
|
+
// The MCP server's search_docs ranking, so both surfaces agree.
|
|
1775
|
+
const refMatches = rankOperations(OPS, term).map((match) => match.op);
|
|
1697
1776
|
const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
|
|
1698
1777
|
if (jsonOutput) {
|
|
1699
1778
|
out({
|
|
1700
1779
|
schema_version: "1",
|
|
1701
1780
|
query: term,
|
|
1702
|
-
reference: refMatches.slice(0,
|
|
1781
|
+
reference: refMatches.slice(0, SEARCH_PAGE_SIZE).map((op) => ({
|
|
1703
1782
|
command: op.command.join(" "),
|
|
1704
1783
|
method: op.httpMethod,
|
|
1705
1784
|
path: op.path,
|
|
1706
1785
|
...(op.summary ? { summary: op.summary } : {}),
|
|
1786
|
+
...(op.deprecated ? { deprecated: true } : {}),
|
|
1707
1787
|
details_command: BIN + " docs " + op.command.join(" ") + " --json",
|
|
1708
1788
|
})),
|
|
1709
|
-
guides: proseMatches.slice(0,
|
|
1789
|
+
guides: proseMatches.slice(0, SEARCH_PAGE_SIZE).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
|
|
1710
1790
|
totals: { reference: refMatches.length, guides: proseMatches.length },
|
|
1711
1791
|
guides_status: docsStatus,
|
|
1712
1792
|
...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
|
|
@@ -1717,11 +1797,11 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1717
1797
|
const lines: string[] = [];
|
|
1718
1798
|
if (refMatches.length > 0) {
|
|
1719
1799
|
lines.push(paintOut("bold", "Reference:"));
|
|
1720
|
-
for (const op of refMatches.slice(0,
|
|
1800
|
+
for (const op of refMatches.slice(0, SEARCH_PAGE_SIZE)) lines.push(" " + padPaint("cyan", op.command.join(" "), 34) + (op.deprecated ? "(deprecated) " : "") + (op.summary ?? wireOf(op)));
|
|
1721
1801
|
}
|
|
1722
1802
|
if (proseMatches.length > 0) {
|
|
1723
1803
|
lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
|
|
1724
|
-
for (const match of proseMatches.slice(0,
|
|
1804
|
+
for (const match of proseMatches.slice(0, SEARCH_PAGE_SIZE)) lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
|
|
1725
1805
|
} else if (docsStatus === "not_configured") {
|
|
1726
1806
|
lines.push(...(lines.length > 0 ? [""] : []), "(no docs site configured for guide search: '" + BIN + " config set docs-url <url>')");
|
|
1727
1807
|
} else if (docsStatus === "unavailable") {
|
|
@@ -1778,7 +1858,7 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1778
1858
|
}
|
|
1779
1859
|
|
|
1780
1860
|
const lines: string[] = [];
|
|
1781
|
-
lines.push(paintOut("bold", "
|
|
1861
|
+
lines.push(paintOut("bold", "Typeship") + " (v" + API_VERSION + ")");
|
|
1782
1862
|
if (API_DESCRIPTION) lines.push("", API_DESCRIPTION.trim());
|
|
1783
1863
|
lines.push("", paintOut("bold", "Reference:") + " " + BIN + " docs <resource> <command>");
|
|
1784
1864
|
const byResource = new Map<string, number>();
|
|
@@ -1834,7 +1914,7 @@ function shellQuote(value: string): string {
|
|
|
1834
1914
|
}
|
|
1835
1915
|
|
|
1836
1916
|
function usageLine(op: OpSpec): string {
|
|
1837
|
-
const paths = op.params.filter((p) => p.kind === "path").map((p) => "<" + p.name + ">").join(" ");
|
|
1917
|
+
const paths = op.params.filter((p) => p.kind === "path").map((p) => p.credential ? "[<" + p.name + ">]" : "<" + p.name + ">").join(" ");
|
|
1838
1918
|
return BIN + " " + op.command[0] + " " + op.command[1] + (paths ? " " + paths : "");
|
|
1839
1919
|
}
|
|
1840
1920
|
|
|
@@ -1882,7 +1962,7 @@ function helpSentence(description: string | undefined): string {
|
|
|
1882
1962
|
/** Type column text for a param: string, number, string[], a|b|c, enum, object, json, path. */
|
|
1883
1963
|
function typeLabel(p: ParamSpec): string {
|
|
1884
1964
|
if (p.nullable) return typeLabel({ ...p, nullable: false }) + "|null";
|
|
1885
|
-
if (p.type === "file") return "path (uploaded)";
|
|
1965
|
+
if (p.type === "file") return p.multiple ? "paths (uploaded, repeatable)" : "path (uploaded)";
|
|
1886
1966
|
if (p.format && p.type === "string") return p.format;
|
|
1887
1967
|
const inlineEnum = (values: string[] | undefined) => values && values.join("|").length <= 24 ? values.join("|") : undefined;
|
|
1888
1968
|
if (p.type === "array") {
|
|
@@ -1957,7 +2037,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
|
|
|
1957
2037
|
}
|
|
1958
2038
|
const width = termWidth();
|
|
1959
2039
|
const lines: string[] = [];
|
|
1960
|
-
lines.push(paintOut("bold", BIN) + ": " + "
|
|
2040
|
+
lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.23.1");
|
|
1961
2041
|
lines.push("");
|
|
1962
2042
|
lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
|
|
1963
2043
|
lines.push("");
|
|
@@ -1976,18 +2056,18 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
|
|
|
1976
2056
|
}
|
|
1977
2057
|
if (EXCLUDED_OPS > 0) {
|
|
1978
2058
|
lines.push("");
|
|
1979
|
-
lines.push(...labeled(paintOut("yellow", "
|
|
1980
|
-
lines.push(...labeled("Omitted: ", OMITTED_OPS.map((op) => op.command.join(" ") + " (" + op.httpMethod + " " + op.path + ")").join(", "), width, 14));
|
|
1981
|
-
lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
|
|
2059
|
+
lines.push(...labeled(paintOut("yellow", "Coverage:") + " ", "this build includes " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations; api.json lists the rest", width, 14));
|
|
1982
2060
|
}
|
|
1983
2061
|
lines.push("");
|
|
1984
|
-
const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check parameters and JSON bodies)" +
|
|
2062
|
+
const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --header \"Name: value\", --timeout <seconds>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check parameters and JSON bodies)" +
|
|
1985
2063
|
(AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
|
|
1986
2064
|
lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
|
|
1987
2065
|
lines.push(...labeled("Credential env vars: ", [
|
|
1988
2066
|
"TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
|
|
1989
2067
|
...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
|
|
1990
2068
|
].join(", ") || "none", width, 21));
|
|
2069
|
+
if (AUTH_UNDECLARED) lines.push(...labeled("Auth: ", "not declared by the API Spec; " + AUTH_SCALARS[0]!.env + " is sent as Authorization: Bearer when set", width, 6));
|
|
2070
|
+
lines.push(...labeled("Extra headers: ", "--header \"Name: value\" (repeatable) or TYPESHIP_HEADERS", width, 15));
|
|
1991
2071
|
lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
|
|
1992
2072
|
lines.push(...labeled("Sign-in: ", BIN + " login | logout | whoami | auth check (stored at " + credsPath() + ")", width, 9));
|
|
1993
2073
|
lines.push(...labeled("Setup: ", BIN + " init (connect this machine)" + " | " + BIN + " config (defaults)" + (HAS_MCP || MCP_URL ? " | " + BIN + " mcp install --all (agent clients)" : "") + " | " + BIN + " doctor | " + BIN + " upgrade | " + BIN + " completion <shell>", width, 7));
|
|
@@ -2030,6 +2110,7 @@ function commandExtras(op: OpSpec): [string, string][] {
|
|
|
2030
2110
|
const extras: [string, string][] = [];
|
|
2031
2111
|
if (op.auth !== "none" && Object.keys(NAMED_SCHEMES).length) extras.push(["--credentials @<file>|-", "named credentials as JSON; use - for stdin"]);
|
|
2032
2112
|
if (op.hasBody && op.bodyKind === "binary") extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
|
|
2113
|
+
if (op.rawResponse) extras.push(["--output <file>", "write the " + (op.rawResponse === "binary" ? "binary " : "") + "response body to a file (- for stdout) and print what was written"]);
|
|
2033
2114
|
else if (op.hasBody) extras.push(["--data '<json>'", "raw JSON body" + (op.bodyStyle === "fields" ? " (merged under field flags)" : "") + "; @<file> reads a file, - reads stdin"]);
|
|
2034
2115
|
if (op.select) extras.push(["--select '<selection>'", "GraphQL selection set replacing the default, e.g. '{ id name }'"]);
|
|
2035
2116
|
if (op.paginated) extras.push(["--all", "stream every item from every page (NDJSON)"]);
|
|
@@ -2044,6 +2125,7 @@ function commandExtras(op: OpSpec): [string, string][] {
|
|
|
2044
2125
|
function exampleLine(op: OpSpec): string {
|
|
2045
2126
|
const parts = [BIN, op.command[0], op.command[1]];
|
|
2046
2127
|
for (const p of op.params) {
|
|
2128
|
+
if (p.credential) continue; // defaulted from the configured credential
|
|
2047
2129
|
const hasExample = p.type !== "file" && Object.hasOwn(op.exampleArguments, p.name);
|
|
2048
2130
|
if (!p.required && !hasExample) continue;
|
|
2049
2131
|
const generated = p.type === "file" ? undefined : op.exampleArguments[p.name];
|
|
@@ -2074,6 +2156,7 @@ function exampleLine(op: OpSpec): string {
|
|
|
2074
2156
|
|
|
2075
2157
|
/** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
|
|
2076
2158
|
function authNote(op: OpSpec): string {
|
|
2159
|
+
if (AUTH_UNDECLARED) return op.auth === "none" ? " (no auth needed)" : " (auth not declared)";
|
|
2077
2160
|
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
|
|
2078
2161
|
return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
|
|
2079
2162
|
}
|
|
@@ -2083,6 +2166,8 @@ function printOp(op: OpSpec): void {
|
|
|
2083
2166
|
lines.push(paintOut("bold", usageLine(op)));
|
|
2084
2167
|
if (op.summary) lines.push(helpSentence(op.summary));
|
|
2085
2168
|
lines.push(wireOf(op) + authNote(op));
|
|
2169
|
+
const scopes = requiredScopes(op.security);
|
|
2170
|
+
if (scopes.length) lines.push("Requires OAuth scopes: " + scopes.join(", ") + (HAS_OAUTH_LOGIN ? " (login --scopes " + scopes.join(",") + ")" : ""));
|
|
2086
2171
|
lines.push("");
|
|
2087
2172
|
const rows = op.params.filter((p) => p.kind !== "path");
|
|
2088
2173
|
const extras = commandExtras(op);
|
|
@@ -2135,7 +2220,7 @@ function readStdinBytes(): Promise<Uint8Array<ArrayBuffer>> {
|
|
|
2135
2220
|
/** A local file as an upload part; the SDK's multipart encoder takes Blobs. */
|
|
2136
2221
|
function fileFromPath(flag: string, path: string): File {
|
|
2137
2222
|
try {
|
|
2138
|
-
return new File([readFileSync(path)], basename(path));
|
|
2223
|
+
return new File([readFileSync(path)], basename(path), { type: mediaTypeForPath(path) });
|
|
2139
2224
|
} catch (e) {
|
|
2140
2225
|
return fail(2, "--" + flag + ": cannot read " + path + " (" + (e as Error).message + ")");
|
|
2141
2226
|
}
|
|
@@ -2182,6 +2267,7 @@ function coerce(spec: ParamSpec, raw: string | boolean, repeated?: string[]): un
|
|
|
2182
2267
|
if (spec.nullable && raw === "null" && repeated === undefined) return null;
|
|
2183
2268
|
if (spec.type === "file") {
|
|
2184
2269
|
if (raw === true) fail(2, "--" + spec.flag + " expects a file path");
|
|
2270
|
+
if (spec.multiple) return (repeated ?? [String(raw)]).map((path) => fileFromPath(spec.flag, path));
|
|
2185
2271
|
return fileFromPath(spec.flag, String(raw));
|
|
2186
2272
|
}
|
|
2187
2273
|
if (spec.type === "boolean") {
|
|
@@ -2252,18 +2338,24 @@ function validateParameters(op: OpSpec, values: Record<string, unknown>, flags:
|
|
|
2252
2338
|
|
|
2253
2339
|
/** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
|
|
2254
2340
|
let LAST_CLIENT_HAD_CREDENTIAL = false;
|
|
2341
|
+
/** The last client's Basic credentials, for path arguments that default to the username. */
|
|
2342
|
+
let LAST_CLIENT_BASIC: { basicAuth?: unknown; credentials?: unknown } = {};
|
|
2343
|
+
|
|
2344
|
+
/** The configured Basic-auth username: the named scheme's, else basicAuth's. */
|
|
2345
|
+
function credentialUsername(scheme: string): string | undefined {
|
|
2346
|
+
const named = (LAST_CLIENT_BASIC.credentials as Record<string, unknown> | undefined)?.[scheme];
|
|
2347
|
+
const username = named && typeof named === "object" ? (named as { username?: unknown }).username
|
|
2348
|
+
: (LAST_CLIENT_BASIC.basicAuth as { username?: unknown } | undefined)?.username;
|
|
2349
|
+
return typeof username === "string" && username !== "" ? username : undefined;
|
|
2350
|
+
}
|
|
2255
2351
|
|
|
2256
2352
|
/** Check the same complete alternatives the request runtime can select, after
|
|
2257
2353
|
* per-scheme flag, environment, profile, and OAuth resolution. */
|
|
2258
2354
|
function requireOperationCredentials(op: OpSpec, options: ClientOptions & Record<string, unknown>): void {
|
|
2259
2355
|
if (op.auth !== "required") return;
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
const
|
|
2263
|
-
const available = namedCredentialAvailability(NAMED_SCHEMES, new Set(Object.keys(options).filter((key) => supplied(options[key]))), named);
|
|
2264
|
-
if (op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => available.has(option)))) return;
|
|
2265
|
-
const alternatives = (op.credentialOptions ?? []).filter((alternative) => alternative.length > 0 && alternative.every((option) => option.startsWith("credentials.")));
|
|
2266
|
-
const missing = alternatives.map((alternative) => alternative.filter((option) => !available.has(option)).map((option) => option.slice("credentials.".length)));
|
|
2356
|
+
const gap = missingCredentials(NAMED_SCHEMES, op.credentialOptions, options);
|
|
2357
|
+
if (!gap) return;
|
|
2358
|
+
const { alternatives, missing } = gap;
|
|
2267
2359
|
const names = new Set(missing.flat());
|
|
2268
2360
|
const relevant = AUTH_SCALARS.filter((scalar) => [...names].some((name) => NAMED_SCHEMES[name]?.options.includes(scalar.option)));
|
|
2269
2361
|
const needsBasic = [...names].some((name) => NAMED_SCHEMES[name]?.options.includes("basicAuth"));
|
|
@@ -2276,7 +2368,8 @@ function requireOperationCredentials(op: OpSpec, options: ClientOptions & Record
|
|
|
2276
2368
|
nextSteps: alternatives.length ? [
|
|
2277
2369
|
...relevant.map((a) => "Set " + a.env + " in the environment, pass --" + a.flag + " <value>, or run '" + BIN + " login'."),
|
|
2278
2370
|
...(needsBasic && BASIC ? ["Set " + BASIC.envUser + " and " + BASIC.envPass + ", or pass --username and --password."] : []),
|
|
2279
|
-
|
|
2371
|
+
...(HAS_OAUTH_LOGIN && [...names].some((name) => OAUTH_SESSION_SCHEMES.includes(name)) ? ["Sign in with OAuth: '" + BIN + " login'."] : []),
|
|
2372
|
+
"Supply all schemes in one alternative through " + "TYPESHIP_CREDENTIALS" + " or --credentials @<JSON-file>: " + alternatives.map((alternative) => alternative.join(" + ")).join(" OR ") + ".",
|
|
2280
2373
|
] : ["Check the operation's security schemes in the API Spec and regenerate with a supported, compatible alternative."],
|
|
2281
2374
|
});
|
|
2282
2375
|
}
|
|
@@ -2307,6 +2400,23 @@ function environmentCredentials(): NamedCredentials {
|
|
|
2307
2400
|
return value === undefined ? {} : parseNamedCredentials(value, NAMED_SCHEMES);
|
|
2308
2401
|
}
|
|
2309
2402
|
|
|
2403
|
+
/** --timeout <seconds> or TYPESHIP_TIMEOUT: the per-attempt deadline
|
|
2404
|
+
* (default 60 seconds) for slow operations. */
|
|
2405
|
+
function requestTimeoutMs(flags: Map<string, string | boolean>): number | undefined {
|
|
2406
|
+
const raw = typeof flags.get("timeout") === "string" ? flags.get("timeout") as string : process.env["TYPESHIP_TIMEOUT"];
|
|
2407
|
+
if (raw === undefined) return undefined;
|
|
2408
|
+
const seconds = Number(raw);
|
|
2409
|
+
if (!/^\d+(\.\d+)?$/.test(raw.trim()) || !Number.isFinite(seconds) || seconds <= 0 || seconds > 3600) fail(2, "--timeout expects seconds between 0 and 3600, such as --timeout 120.");
|
|
2410
|
+
return Math.round(seconds * 1000);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/** --header flags and TYPESHIP_HEADERS: sent on every API request, after
|
|
2414
|
+
* (and in place of) any generated header of the same name. */
|
|
2415
|
+
function extraRequestHeaders(): Record<string, string> {
|
|
2416
|
+
try { return parseExtraHeaders(process.env["TYPESHIP_HEADERS"], HEADER_FLAGS, "TYPESHIP_HEADERS"); }
|
|
2417
|
+
catch (error) { fail(2, (error as Error).message); }
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2310
2420
|
/** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
|
|
2311
2421
|
function credentialSource(flags: Map<string, string | boolean>): string | null {
|
|
2312
2422
|
if (Object.keys(flagCredentials(flags)).length) return "flags";
|
|
@@ -2330,7 +2440,29 @@ function resolveBaseUrl(flags: Map<string, string | boolean>, config = readConfi
|
|
|
2330
2440
|
?? DEFAULT_BASE_URL ?? undefined;
|
|
2331
2441
|
}
|
|
2332
2442
|
|
|
2443
|
+
/** OAuth schemes the login session authenticates by name; empty when the
|
|
2444
|
+
* session is the convenience bearer token (see oauthSessionSchemes). */
|
|
2445
|
+
const OAUTH_SESSION_SCHEMES = oauthSessionSchemes(NAMED_SCHEMES);
|
|
2446
|
+
|
|
2447
|
+
/** Send a login session to the OAuth scheme, never to a separate http bearer
|
|
2448
|
+
* scheme. Explicit credentials for the same scheme keep precedence. */
|
|
2449
|
+
function applyOAuthSession(options: ClientOptions & Record<string, unknown>, token: string | (() => Promise<string>), replace = false): void {
|
|
2450
|
+
if (!OAUTH_SESSION_SCHEMES.length) {
|
|
2451
|
+
if (replace || options.bearerToken === undefined) options.bearerToken = token;
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
const credentials = (options.credentials ??= {}) as Record<string, unknown>;
|
|
2455
|
+
for (const name of OAUTH_SESSION_SCHEMES) if (replace || !Object.hasOwn(credentials, name)) credentials[name] = token;
|
|
2456
|
+
}
|
|
2457
|
+
function withOAuthSession(options: ClientOptions & Record<string, unknown>, accessToken: string): ClientOptions & Record<string, unknown> {
|
|
2458
|
+
const next: ClientOptions & Record<string, unknown> = { ...options, credentials: { ...(options.credentials as Record<string, unknown>) } as ClientOptions["credentials"] };
|
|
2459
|
+
applyOAuthSession(next, accessToken, true);
|
|
2460
|
+
if (!OAUTH_SESSION_SCHEMES.length) next.credentials = { ...(next.credentials as Record<string, unknown>), ...resolveNamedCredentials(NAMED_SCHEMES, [{ options: { bearerToken: accessToken } }]) } as ClientOptions["credentials"];
|
|
2461
|
+
return next;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2333
2464
|
async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, candidate?: StoredCreds, forIdentity = false): Promise<TypeshipClient> {
|
|
2465
|
+
CURRENT_SCOPES = requiredScopes(op.security);
|
|
2334
2466
|
const flagNamed = flagCredentials(flags), envNamed = environmentCredentials();
|
|
2335
2467
|
const explicitOptions = new Set(AUTH_SCALARS.filter((a) => typeof flags.get(a.flag) === "string" || process.env[a.env] !== undefined).map((a) => a.option));
|
|
2336
2468
|
if (BASIC && (typeof flags.get("username") === "string" || process.env[BASIC.envUser] !== undefined) && (typeof flags.get("password") === "string" || process.env[BASIC.envPass] !== undefined)) explicitOptions.add("basicAuth");
|
|
@@ -2369,25 +2501,29 @@ async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, cand
|
|
|
2369
2501
|
options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
|
|
2370
2502
|
{ named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
|
|
2371
2503
|
{ named: envNamed, options: envOptions }, { named: flagNamed, options: flagOptions },
|
|
2372
|
-
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2504
|
+
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth && !OAUTH_SESSION_SCHEMES.length ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2373
2505
|
]);
|
|
2374
|
-
if (stored?.oauth
|
|
2506
|
+
if (stored?.oauth) {
|
|
2375
2507
|
const sessionId = stored.oauth.sessionId;
|
|
2376
|
-
|
|
2508
|
+
const token = forIdentity ? stored.oauth.accessToken : sessionCredential((rejected) => oauthSessionToken(credentialStore(), {
|
|
2377
2509
|
...sessionConfiguration(baseUrl, config),
|
|
2378
|
-
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken: string) => verifyClientIdentity((values) => new TypeshipClient(values as unknown as ClientOptions),
|
|
2379
|
-
}, sessionId, OAUTH_TOKEN_PARAMS);
|
|
2510
|
+
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken: string) => verifyClientIdentity((values) => new TypeshipClient(values as unknown as ClientOptions), withOAuthSession(options, accessToken), WHOAMI!, IDENTITY_POLICY) } : {}),
|
|
2511
|
+
}, sessionId, OAUTH_TOKEN_PARAMS, rejected));
|
|
2512
|
+
applyOAuthSession(options, token, forIdentity);
|
|
2380
2513
|
}
|
|
2381
2514
|
if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
|
|
2382
2515
|
options.debug = (event: DebugEvent) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
|
|
2383
2516
|
}
|
|
2384
2517
|
if (flags.get("validate") === true) options.validate = true;
|
|
2518
|
+
const timeout = requestTimeoutMs(flags);
|
|
2519
|
+
if (timeout !== undefined) options.timeoutMs = timeout;
|
|
2385
2520
|
for (const g of GLOBALS) {
|
|
2386
2521
|
const flagValue = flags.get(g.flag);
|
|
2387
2522
|
const value = typeof flagValue === "string" ? flagValue : process.env["TYPESHIP_" + g.envSuffix];
|
|
2388
2523
|
if (value !== undefined) options[g.option] = value;
|
|
2389
2524
|
}
|
|
2390
2525
|
LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
|
|
2526
|
+
LAST_CLIENT_BASIC = { basicAuth: options.basicAuth, credentials: options.credentials };
|
|
2391
2527
|
requireOperationCredentials(op, options);
|
|
2392
2528
|
// Identify the package and version. Optional harness and caller details
|
|
2393
2529
|
// let the API distinguish agent traffic from other non-interactive use.
|
|
@@ -2401,6 +2537,8 @@ async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, cand
|
|
|
2401
2537
|
"User-Agent": PKG_NAME + "-cli/" + VERSION + (details ? " (" + details + ")" : ""),
|
|
2402
2538
|
};
|
|
2403
2539
|
if (forIdentity) { options.fetch = identityFetch(baseUrl); options.maxRetries = 0; options.timeoutMs = 10_000; }
|
|
2540
|
+
const extraHeaders = extraRequestHeaders();
|
|
2541
|
+
if (Object.keys(extraHeaders).length) options.onRequest = (context) => { applyExtraHeaders(context.headers, extraHeaders); };
|
|
2404
2542
|
return new TypeshipClient(options);
|
|
2405
2543
|
}
|
|
2406
2544
|
|
|
@@ -2438,9 +2576,9 @@ function failOmitted(op: OmittedOpSpec): never {
|
|
|
2438
2576
|
return failWith({
|
|
2439
2577
|
status: "action_required",
|
|
2440
2578
|
code: "PLAN_LIMIT",
|
|
2441
|
-
message: "The command '" + BIN + " " + op.command.join(" ") + "'
|
|
2442
|
-
detail: { operation: op.tool, method: op.httpMethod, path: op.path, generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
|
|
2443
|
-
nextSteps: ["
|
|
2579
|
+
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.",
|
|
2580
|
+
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 },
|
|
2581
|
+
nextSteps: ["The package's publisher can regenerate it with every operation.", "Do not invent or retry an omitted command against this package."],
|
|
2444
2582
|
});
|
|
2445
2583
|
}
|
|
2446
2584
|
|
|
@@ -2449,6 +2587,9 @@ const BUILTIN_COMMANDS = ["login","logout","whoami","config","mcp","docs","upgra
|
|
|
2449
2587
|
async function main(): Promise<void> {
|
|
2450
2588
|
const argv = process.argv.slice(2);
|
|
2451
2589
|
const parsed = parseArgv(argv);
|
|
2590
|
+
const headerFlag = parsed.flags.get("header");
|
|
2591
|
+
HEADER_FLAGS = parsed.repeated.get("header") ?? (typeof headerFlag === "string" ? [headerFlag] : []);
|
|
2592
|
+
if (headerFlag === true) fail(2, "--header expects \"Name: value\".");
|
|
2452
2593
|
if (!parsed.help) validateCredentialsInput(parsed.flags);
|
|
2453
2594
|
const profileFlag = parsed.flags.get("profile");
|
|
2454
2595
|
if (profileFlag !== undefined && typeof profileFlag !== "string") fail(2, "--profile requires a profile name.");
|
|
@@ -2466,7 +2607,7 @@ async function main(): Promise<void> {
|
|
|
2466
2607
|
// "acme 1.0.0 (acme 1.0.0)" would say the name twice; when the API's
|
|
2467
2608
|
// title is the bin, name the API version as such.
|
|
2468
2609
|
const apiLabel = API_TITLE.toLowerCase().replace(/[^a-z0-9]/g, "") === BIN.toLowerCase().replace(/[^a-z0-9]/g, "") ? "API " + API_VERSION : API_TITLE + " " + API_VERSION;
|
|
2469
|
-
process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by
|
|
2610
|
+
process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by Typeship)\n");
|
|
2470
2611
|
await flushExit(0);
|
|
2471
2612
|
}
|
|
2472
2613
|
const [resourceCmd, methodCmd] = parsed.positionals;
|
|
@@ -2531,12 +2672,18 @@ async function main(): Promise<void> {
|
|
|
2531
2672
|
|
|
2532
2673
|
const pathSpecs = op.params.filter((p) => p.kind === "path");
|
|
2533
2674
|
const pathValues = parsed.positionals.slice(2);
|
|
2534
|
-
|
|
2535
|
-
|
|
2675
|
+
// A path argument that is the Basic-auth username (Twilio's AccountSid)
|
|
2676
|
+
// may be left out; it defaults to the configured credential below.
|
|
2677
|
+
const pathGiven = pathValues.length === pathSpecs.length ? pathSpecs
|
|
2678
|
+
: pathValues.length === pathSpecs.filter((p) => !p.credential).length ? pathSpecs.filter((p) => !p.credential)
|
|
2679
|
+
: null;
|
|
2680
|
+
if (!pathGiven) {
|
|
2681
|
+
const optional = pathSpecs.filter((p) => p.credential).length;
|
|
2682
|
+
fail(2, "Expected " + (optional ? (pathSpecs.length - optional) + " or " : "") + pathSpecs.length + " argument(s): " + usageLine(op));
|
|
2536
2683
|
}
|
|
2537
2684
|
|
|
2538
2685
|
const values: Record<string, unknown> = {};
|
|
2539
|
-
|
|
2686
|
+
pathGiven!.forEach((spec, i) => { values[spec.name] = pathValues[i]; });
|
|
2540
2687
|
|
|
2541
2688
|
let dataBody: unknown;
|
|
2542
2689
|
const dataRaw = parsed.flags.get("data");
|
|
@@ -2557,7 +2704,7 @@ async function main(): Promise<void> {
|
|
|
2557
2704
|
// Mirrors opReservedFlags() in the generator: API parameters never use these
|
|
2558
2705
|
// names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
|
|
2559
2706
|
// check can be exact.
|
|
2560
|
-
const RESERVED_FLAGS = new Set(["data", "credentials", "all", "select", "base-url", "profile", "debug", "validate", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
|
|
2707
|
+
const RESERVED_FLAGS = new Set(["data", "credentials", "header", "timeout", "all", "select", "base-url", "profile", "debug", "validate", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
|
|
2561
2708
|
for (const spec of op.params) {
|
|
2562
2709
|
if (spec.kind === "path") continue;
|
|
2563
2710
|
const raw = parsed.flags.get(spec.flag);
|
|
@@ -2566,7 +2713,7 @@ async function main(): Promise<void> {
|
|
|
2566
2713
|
// every value (each coerced to the element type); loosely typed (json)
|
|
2567
2714
|
// params become an array of the parsed values.
|
|
2568
2715
|
const all = parsed.repeated.get(spec.flag);
|
|
2569
|
-
values[spec.name] = spec.type === "array"
|
|
2716
|
+
values[spec.name] = spec.type === "array" || (spec.type === "file" && spec.multiple)
|
|
2570
2717
|
? coerce(spec, raw, all)
|
|
2571
2718
|
: all !== undefined && spec.type === "json"
|
|
2572
2719
|
? all.map((v) => coerce(spec, v))
|
|
@@ -2575,12 +2722,39 @@ async function main(): Promise<void> {
|
|
|
2575
2722
|
for (const key of parsed.flags.keys()) {
|
|
2576
2723
|
if (RESERVED_FLAGS.has(key)) continue;
|
|
2577
2724
|
if (key === "file" && op.bodyKind === "binary") continue;
|
|
2725
|
+
if (key === "output" && op.rawResponse) continue;
|
|
2578
2726
|
if (!op.params.some((p) => p.flag === key)) {
|
|
2579
2727
|
const suggestion = didYouMean(key, [...op.params.filter((p) => p.kind !== "path").map((p) => p.flag), ...RESERVED_FLAGS]);
|
|
2580
2728
|
fail(2, "Unknown flag --" + key + "." + (suggestion ? " Did you mean --" + suggestion + "?" : ""));
|
|
2581
2729
|
}
|
|
2582
2730
|
}
|
|
2583
2731
|
|
|
2732
|
+
// Object and array values (a GraphQL input, a JSON flag, --data fields)
|
|
2733
|
+
// get the checks the MCP server applies: nested types, enums, required
|
|
2734
|
+
// properties, patterns and a closed object's unknown keys, all at once.
|
|
2735
|
+
const argumentSchemas = (op.inputSchema.properties ?? {}) as Record<string, Record<string, unknown>>;
|
|
2736
|
+
const nestedIssues: ArgumentIssue[] = [];
|
|
2737
|
+
const checkNested = (name: string, value: unknown): unknown =>
|
|
2738
|
+
value !== null && typeof value === "object" && !(value instanceof Blob) && argumentSchemas[name]
|
|
2739
|
+
? checkValue(value, argumentSchemas[name]!, name, nestedIssues) : value;
|
|
2740
|
+
for (const spec of op.params) {
|
|
2741
|
+
if (spec.kind !== "path" && spec.type !== "file" && values[spec.name] !== undefined) values[spec.name] = checkNested(spec.name, values[spec.name]);
|
|
2742
|
+
}
|
|
2743
|
+
if (op.bodyStyle === "fields" && dataBody !== null && typeof dataBody === "object" && !Array.isArray(dataBody) && !(dataBody instanceof Blob)) {
|
|
2744
|
+
const body = dataBody as Record<string, unknown>;
|
|
2745
|
+
for (const name of Object.keys(body)) {
|
|
2746
|
+
if (op.params.some((p) => p.kind === "body" && p.name === name && p.type !== "file")) body[name] = checkNested(name, body[name]);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
if (nestedIssues.length > 0) {
|
|
2750
|
+
failWith({
|
|
2751
|
+
code: nestedIssues.every((issue) => issue.code === "MISSING_ARGUMENT") ? "MISSING_ARGUMENT" : "INVALID_USAGE",
|
|
2752
|
+
message: nestedIssues.length + (nestedIssues.length === 1 ? " problem" : " problems") + " in the arguments; nothing was sent: " + nestedIssues.map((issue) => issue.message).join("; "),
|
|
2753
|
+
detail: { issues: nestedIssues },
|
|
2754
|
+
nextSteps: ["Fix the values listed in detail.issues and run again.", "Run '" + USAGE_HINT + "' for each argument's type."],
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2584
2758
|
const missing = missingRequired(op, values).filter((name) =>
|
|
2585
2759
|
!(op.bodyStyle === "fields" && dataBody !== undefined && typeof dataBody === "object" && dataBody !== null && name in (dataBody as object)),
|
|
2586
2760
|
);
|
|
@@ -2600,6 +2774,7 @@ async function main(): Promise<void> {
|
|
|
2600
2774
|
if (typeof fieldsRaw === "string") {
|
|
2601
2775
|
FIELDS = fieldsRaw.split(",").map((f) => f.trim()).filter((f) => f !== "").map((f) => f.split("."));
|
|
2602
2776
|
if (FIELDS.length === 0) fail(2, "--fields expects at least one field path");
|
|
2777
|
+
FIELDS_AFTER_WRITE = op.safety !== "read";
|
|
2603
2778
|
}
|
|
2604
2779
|
|
|
2605
2780
|
// --out <dir> materializes a file-shaped response (see cli-agent.ts bundleProperty).
|
|
@@ -2611,13 +2786,42 @@ async function main(): Promise<void> {
|
|
|
2611
2786
|
}
|
|
2612
2787
|
|
|
2613
2788
|
validateParameters(op, values, parsed.flags);
|
|
2789
|
+
// Raw bytes on a terminal are unreadable and can garble it: ask for a
|
|
2790
|
+
// destination before the request runs (it may be billed, like speech).
|
|
2791
|
+
const outputFlag = op.rawResponse ? parsed.flags.get("output") : undefined;
|
|
2792
|
+
if (outputFlag === true) fail(2, "--output expects a file path, or - for stdout.");
|
|
2793
|
+
if (op.rawResponse === "binary" && outputFlag === undefined && process.stdout.isTTY) {
|
|
2794
|
+
fail(2, op.command.join(" ") + " returns binary data. Pass --output <file>, or redirect stdout to a file.");
|
|
2795
|
+
}
|
|
2796
|
+
// --all on a list the generator could not page would print one page and
|
|
2797
|
+
// exit 0, which reads as "that is everything".
|
|
2798
|
+
if (!op.paginated && parsed.flags.get("all") === true) {
|
|
2799
|
+
fail(2, op.command.join(" ") + " does not paginate, so --all has nothing to walk. Run it without --all; it returns the whole response.");
|
|
2800
|
+
}
|
|
2614
2801
|
const client = await makeClient(parsed.flags, op);
|
|
2802
|
+
for (const spec of pathSpecs) {
|
|
2803
|
+
if (!spec.credential || values[spec.name] !== undefined) continue;
|
|
2804
|
+
const username = credentialUsername(spec.credential.scheme);
|
|
2805
|
+
if (username === undefined) {
|
|
2806
|
+
fail(2, "Missing required: <" + spec.name + ">. It defaults to the Basic-auth username (" + spec.credential.env + "), and none is configured.", undefined,
|
|
2807
|
+
["Pass " + spec.name + " as an argument: " + usageLine(op) + ".", "Or set " + spec.credential.env + (BASIC ? " and " + BASIC.envPass : "") + ", or run '" + BIN + " login'."]);
|
|
2808
|
+
}
|
|
2809
|
+
values[spec.name] = username;
|
|
2810
|
+
}
|
|
2615
2811
|
|
|
2616
2812
|
// Destructive commands need --force. A person gets asked; an agent gets
|
|
2617
2813
|
// an action_required envelope with the exact command to run, so nothing
|
|
2618
2814
|
// is deleted on a guess.
|
|
2619
2815
|
if (op.safety === "destructive" && !assumeYes(parsed)) {
|
|
2620
|
-
|
|
2816
|
+
// Credential flags are replaced by placeholders: the rerun is shown to
|
|
2817
|
+
// agents and logged, so it must never repeat a key.
|
|
2818
|
+
const secretFlags = new Set([...AUTH_SCALARS.map((a) => "--" + a.flag), "--header"]);
|
|
2819
|
+
const rerun = BIN + " " + process.argv.slice(2).map((a, i, all) => {
|
|
2820
|
+
const eq = a.indexOf("=");
|
|
2821
|
+
if (a.startsWith("--") && eq > 0 && secretFlags.has(a.slice(0, eq))) return a.slice(0, eq) + "=<" + a.slice(2, eq) + ">";
|
|
2822
|
+
if (i > 0 && secretFlags.has(all[i - 1]!)) return "<" + all[i - 1]!.slice(2) + ">";
|
|
2823
|
+
return /\s/.test(a) ? JSON.stringify(a) : a;
|
|
2824
|
+
}).join(" ") + " --force";
|
|
2621
2825
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
2622
2826
|
failWith({
|
|
2623
2827
|
status: "action_required",
|
|
@@ -2634,20 +2838,29 @@ async function main(): Promise<void> {
|
|
|
2634
2838
|
const selectValue = typeof parsed.flags.get("select") === "string" ? (parsed.flags.get("select") as string) : undefined;
|
|
2635
2839
|
const args = buildArgs(op, values, dataBody, selectValue);
|
|
2636
2840
|
const target = (client as unknown as Record<string, Record<string, (...a: unknown[]) => unknown>>)[op.resource]!;
|
|
2637
|
-
|
|
2841
|
+
// --stream true on an operation with a streaming twin prints its events
|
|
2842
|
+
// as NDJSON as they arrive, instead of failing on the event stream.
|
|
2843
|
+
const streaming = op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value;
|
|
2844
|
+
const callResult = target[streaming ? op.streamMethod!.method : op.method]!(...args);
|
|
2638
2845
|
|
|
2639
2846
|
if (op.paginated && parsed.flags.get("all") === true) {
|
|
2847
|
+
const fieldsCheck = streamFieldsCheck();
|
|
2640
2848
|
try {
|
|
2641
2849
|
for await (const item of callResult as AsyncIterable<unknown>) {
|
|
2642
|
-
process.stdout.write(JSON.stringify(
|
|
2850
|
+
process.stdout.write(JSON.stringify(fieldsCheck.item(item)) + "\n");
|
|
2643
2851
|
}
|
|
2852
|
+
fieldsCheck.finish();
|
|
2644
2853
|
await flushExit(0);
|
|
2645
2854
|
} catch (e) {
|
|
2855
|
+
if (isNotModified(e)) await printNotModified(e);
|
|
2646
2856
|
failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
|
|
2647
2857
|
}
|
|
2648
2858
|
}
|
|
2649
2859
|
|
|
2650
2860
|
let result = await asApiResult(callResult as Promise<unknown>);
|
|
2861
|
+
// 304 Not Modified: the conditional request matched, so there is no
|
|
2862
|
+
// body. Say so, with the ETag to send next time.
|
|
2863
|
+
if (!result.ok && isNotModified(result.error)) await printNotModified(result.error);
|
|
2651
2864
|
if (result.ok && op.httpMethod === "POST" && op.path === "/projects/{project_id}/generate") {
|
|
2652
2865
|
const batch = result.data as { data: Array<{ id: string }> };
|
|
2653
2866
|
const generations = (client as unknown as { generations: { wait(id: string): Promise<unknown> } }).generations;
|
|
@@ -2660,15 +2873,33 @@ async function main(): Promise<void> {
|
|
|
2660
2873
|
result = { ...result, data: { ...batch, data: completed } };
|
|
2661
2874
|
}
|
|
2662
2875
|
if (result.ok) {
|
|
2663
|
-
if (op.sse) {
|
|
2876
|
+
if (op.sse || streaming) {
|
|
2664
2877
|
// Server-sent events as NDJSON, one line per event, until the stream ends.
|
|
2878
|
+
const fieldsCheck = streamFieldsCheck();
|
|
2665
2879
|
try {
|
|
2666
2880
|
for await (const event of result.data as AsyncIterable<{ event?: string; id?: string; data: string }>) {
|
|
2667
|
-
process.stdout.write(JSON.stringify(
|
|
2881
|
+
process.stdout.write(JSON.stringify(fieldsCheck.item(event)) + "\n");
|
|
2668
2882
|
}
|
|
2669
2883
|
} catch (e) {
|
|
2670
2884
|
failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
|
|
2671
2885
|
}
|
|
2886
|
+
fieldsCheck.finish();
|
|
2887
|
+
await flushExit(0);
|
|
2888
|
+
}
|
|
2889
|
+
// A non-JSON body (audio, a file, CSV): raw bytes to stdout, or to the
|
|
2890
|
+
// --output file with a JSON note of what was written. Never "{}".
|
|
2891
|
+
if (op.rawResponse || result.data instanceof Blob) {
|
|
2892
|
+
const data = result.data;
|
|
2893
|
+
const bytes = data instanceof Blob
|
|
2894
|
+
? new Uint8Array(await data.arrayBuffer())
|
|
2895
|
+
: new TextEncoder().encode(typeof data === "string" ? data : JSON.stringify(data ?? ""));
|
|
2896
|
+
if (typeof outputFlag === "string" && outputFlag !== "-") {
|
|
2897
|
+
try { writeFileSync(outputFlag, bytes); } catch (e) { fail(1, "--output: cannot write " + outputFlag + " (" + (e as Error).message + ")"); }
|
|
2898
|
+
const mediaType = data instanceof Blob && data.type ? data.type : op.rawResponse === "text" ? "text/plain" : "application/octet-stream";
|
|
2899
|
+
out({ saved_to: resolvePath(outputFlag), bytes: bytes.length, media_type: mediaType });
|
|
2900
|
+
} else {
|
|
2901
|
+
process.stdout.write(bytes);
|
|
2902
|
+
}
|
|
2672
2903
|
await flushExit(0);
|
|
2673
2904
|
}
|
|
2674
2905
|
// A claim-shaped response (claim.url): say where to claim it, and leave
|
|
@@ -2687,7 +2918,7 @@ async function main(): Promise<void> {
|
|
|
2687
2918
|
const page = result.data as { items: unknown[]; hasNextPage(): boolean; nextPageParams(): Record<string, unknown> | null; response: { requestId?: string } };
|
|
2688
2919
|
const next = page.nextPageParams();
|
|
2689
2920
|
out({
|
|
2690
|
-
items: project(page.items),
|
|
2921
|
+
items: project(page.items, true),
|
|
2691
2922
|
hasMore: next !== null,
|
|
2692
2923
|
...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
|
|
2693
2924
|
...(page.response.requestId ? { request_id: page.response.requestId } : {}),
|
|
@@ -2696,7 +2927,7 @@ async function main(): Promise<void> {
|
|
|
2696
2927
|
// Batch-style collection envelopes ({data: [...]}) use item-relative
|
|
2697
2928
|
// fields, matching paginated results, while retaining envelope metadata.
|
|
2698
2929
|
const data = result.data as Record<string, unknown>;
|
|
2699
|
-
out({ ...data, [collectionField]: project(data[collectionField]) });
|
|
2930
|
+
out({ ...data, [collectionField]: project(data[collectionField], true) });
|
|
2700
2931
|
} else if (outDir !== undefined && bundleField !== null) {
|
|
2701
2932
|
// --out: a file-shaped response (an array of {path, content}) lands
|
|
2702
2933
|
// on disk; stdout gets the rest of the response plus a summary.
|
|
@@ -2704,9 +2935,9 @@ async function main(): Promise<void> {
|
|
|
2704
2935
|
const files = (data[bundleField] as { path: string; content: string }[] | undefined) ?? [];
|
|
2705
2936
|
const written = writeBundle(outDir, files);
|
|
2706
2937
|
const { [bundleField]: _omitted, ...rest } = data;
|
|
2707
|
-
out({ ...(project(rest) as Record<string, unknown>), out: written });
|
|
2938
|
+
out({ ...(project(rest, false) as Record<string, unknown>), out: written });
|
|
2708
2939
|
} else {
|
|
2709
|
-
out(
|
|
2940
|
+
out(result.data === undefined || result.data === null ? { ok: true } : project(result.data, Array.isArray(result.data)));
|
|
2710
2941
|
}
|
|
2711
2942
|
await flushExit(0);
|
|
2712
2943
|
}
|
|
@@ -2724,6 +2955,8 @@ function errorMessage(error: unknown): string {
|
|
|
2724
2955
|
main().catch((e) => {
|
|
2725
2956
|
if (e instanceof ExitPending) return;
|
|
2726
2957
|
try {
|
|
2958
|
+
const auth = classifyAuthFailure(e, authFailureContext());
|
|
2959
|
+
if (auth) failWith(auth);
|
|
2727
2960
|
fail(1, errorMessage(e));
|
|
2728
2961
|
} catch {
|
|
2729
2962
|
// exit already scheduled
|