@typeship-ax/cli 0.6.0 → 0.9.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.
Files changed (121) hide show
  1. package/AGENTS.md +31 -0
  2. package/README.md +26 -9
  3. package/api.json +6861 -3083
  4. package/api.md +508 -277
  5. package/dist/api-identity.d.ts +40 -0
  6. package/dist/api-identity.d.ts.map +1 -0
  7. package/dist/api-identity.js +128 -0
  8. package/dist/auth-profiles.d.ts +30 -0
  9. package/dist/auth-profiles.d.ts.map +1 -0
  10. package/dist/auth-profiles.js +138 -0
  11. package/dist/cli-agent.d.ts +9 -1
  12. package/dist/cli-agent.d.ts.map +1 -1
  13. package/dist/cli-agent.js +26 -9
  14. package/dist/cli.js +600 -582
  15. package/dist/console-login-check.d.ts +21 -0
  16. package/dist/console-login-check.d.ts.map +1 -0
  17. package/dist/console-login-check.js +107 -0
  18. package/dist/console-login-contract.d.ts +45 -0
  19. package/dist/console-login-contract.d.ts.map +1 -0
  20. package/dist/console-login-contract.js +40 -0
  21. package/dist/core/http.d.ts +21 -92
  22. package/dist/core/http.d.ts.map +1 -1
  23. package/dist/core/http.js +143 -221
  24. package/dist/core/pagination.d.ts.map +1 -1
  25. package/dist/core/pagination.js +6 -34
  26. package/dist/credential-storage.d.ts +24 -0
  27. package/dist/credential-storage.d.ts.map +1 -0
  28. package/dist/credential-storage.js +207 -0
  29. package/dist/dates.d.ts +0 -2
  30. package/dist/dates.d.ts.map +1 -1
  31. package/dist/dates.js +0 -1
  32. package/dist/docs.d.ts +36 -0
  33. package/dist/docs.d.ts.map +1 -0
  34. package/dist/docs.js +258 -0
  35. package/dist/errors.d.ts +42 -34
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +30 -20
  38. package/dist/index.d.ts +27 -12
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +40 -14
  41. package/dist/named-credentials.d.ts +21 -0
  42. package/dist/named-credentials.d.ts.map +1 -0
  43. package/dist/named-credentials.js +86 -0
  44. package/dist/oauth-login.d.ts +39 -0
  45. package/dist/oauth-login.d.ts.map +1 -0
  46. package/dist/oauth-login.js +171 -0
  47. package/dist/oauth-request.d.ts +21 -0
  48. package/dist/oauth-request.d.ts.map +1 -0
  49. package/dist/oauth-request.js +119 -0
  50. package/dist/oauth-session.d.ts +106 -0
  51. package/dist/oauth-session.d.ts.map +1 -0
  52. package/dist/oauth-session.js +244 -0
  53. package/dist/ops.d.ts +18 -0
  54. package/dist/ops.d.ts.map +1 -1
  55. package/dist/ops.js +31 -17
  56. package/dist/polling-login.d.ts +57 -0
  57. package/dist/polling-login.d.ts.map +1 -0
  58. package/dist/polling-login.js +204 -0
  59. package/dist/resources/account.d.ts +4 -4
  60. package/dist/resources/account.d.ts.map +1 -1
  61. package/dist/resources/account.js +1 -0
  62. package/dist/resources/api-keys.d.ts +13 -8
  63. package/dist/resources/api-keys.d.ts.map +1 -1
  64. package/dist/resources/api-keys.js +5 -1
  65. package/dist/resources/definition-revisions.d.ts +58 -0
  66. package/dist/resources/definition-revisions.d.ts.map +1 -0
  67. package/dist/resources/definition-revisions.js +114 -0
  68. package/dist/resources/definitions.d.ts +35 -0
  69. package/dist/resources/definitions.d.ts.map +1 -0
  70. package/dist/resources/definitions.js +60 -0
  71. package/dist/resources/generate.d.ts +18 -7
  72. package/dist/resources/generate.d.ts.map +1 -1
  73. package/dist/resources/generate.js +13 -5
  74. package/dist/resources/generations.d.ts +6 -6
  75. package/dist/resources/generations.d.ts.map +1 -1
  76. package/dist/resources/generations.js +3 -1
  77. package/dist/resources/projects.d.ts +111 -35
  78. package/dist/resources/projects.d.ts.map +1 -1
  79. package/dist/resources/projects.js +125 -15
  80. package/dist/resources/targets.d.ts +97 -0
  81. package/dist/resources/targets.d.ts.map +1 -0
  82. package/dist/resources/targets.js +197 -0
  83. package/dist/schemas.d.ts.map +1 -1
  84. package/dist/schemas.js +135 -62
  85. package/dist/types.d.ts +2072 -267
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/types.js +20 -3
  88. package/package.json +2 -1
  89. package/src/api-identity.ts +98 -0
  90. package/src/auth-profiles.ts +114 -0
  91. package/src/cli-agent.ts +28 -11
  92. package/src/cli.ts +528 -560
  93. package/src/console-login-check.ts +88 -0
  94. package/src/console-login-contract.ts +65 -0
  95. package/src/core/http.ts +156 -305
  96. package/src/core/pagination.ts +6 -30
  97. package/src/credential-storage.ts +183 -0
  98. package/src/dates.ts +0 -1
  99. package/src/docs.ts +239 -0
  100. package/src/errors.ts +52 -41
  101. package/src/index.ts +49 -14
  102. package/src/named-credentials.ts +74 -0
  103. package/src/oauth-login.ts +184 -0
  104. package/src/oauth-request.ts +90 -0
  105. package/src/oauth-session.ts +258 -0
  106. package/src/ops.ts +56 -17
  107. package/src/polling-login.ts +165 -0
  108. package/src/resources/account.ts +6 -3
  109. package/src/resources/api-keys.ts +27 -7
  110. package/src/resources/definition-revisions.ts +207 -0
  111. package/src/resources/definitions.ts +122 -0
  112. package/src/resources/generate.ts +29 -6
  113. package/src/resources/generations.ts +9 -4
  114. package/src/resources/projects.ts +274 -41
  115. package/src/resources/targets.ts +378 -0
  116. package/src/schemas.ts +135 -62
  117. package/src/types.ts +2273 -322
  118. package/dist/resources/spec-revisions.d.ts +0 -47
  119. package/dist/resources/spec-revisions.d.ts.map +0 -1
  120. package/dist/resources/spec-revisions.js +0 -90
  121. package/src/resources/spec-revisions.ts +0 -150
package/dist/cli.js CHANGED
@@ -5,33 +5,59 @@
5
5
  // Prints raw JSON to stdout; errors as JSON on stderr.
6
6
  // Exit codes: 0 success, 1 API/transport error, 2 usage error.
7
7
  import { spawnSync } from "node:child_process";
8
- import { createHash, randomBytes } from "node:crypto";
9
- import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
8
+ import { oauthBrowserLogin } from "./oauth-login.js";
9
+ import { oauthDeviceLogin, customBrowserApproval, loginEndpoint } from "./polling-login.js";
10
+ import { oauthStatusRequest } from "./oauth-request.js";
11
+ import { checkConsoleBrowserLogin } from "./console-login-check.js";
12
+ import { assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, oauthSessionToken, sessionBinding } from "./oauth-session.js";
13
+ import { createCredentialStore } from "./credential-storage.js";
14
+ import { identityFetch, identityResult, verifyApiIdentity, verifyClientIdentity, readApiIdentity, assertApiIdentity } from "./api-identity.js";
15
+ import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability } from "./named-credentials.js";
16
+ import { resolveProfile, listProfiles, selectProfile, removeProfile, readProfileConfig, updateProfileConfig } from "./auth-profiles.js";
17
+ import { randomBytes } from "node:crypto";
18
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
10
19
  import { homedir, hostname } from "node:os";
11
20
  import { basename, join } from "node:path";
12
21
  import { fileURLToPath } from "node:url";
13
22
  import { TypeshipClient, formatDebugEvent } from "./index.js";
14
- import { GLOBALS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
23
+ import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
15
24
  import { MCP_CLIENTS, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
16
25
  import { relativeDate } from "./dates.js";
26
+ import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
17
27
  const BIN = "typeship";
18
28
  const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
29
+ const NAMED_SCHEMES = { "apiKey": { "kind": "bearer", "options": ["bearerToken"] } };
19
30
  const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESHIP_TOKEN" }];
20
31
  const BASIC = null;
32
+ /** Operations omitted from the generated package by its plan cap. */
21
33
  const EXCLUDED_OPS = 0;
22
- const VERSION = "0.6.0";
23
- const API_VERSION = "0.6.0";
34
+ /** Generated CLI operations that are intentionally unavailable to MCP. */
35
+ const MCP_EXCLUDED_OPS = 0;
36
+ const VERSION = "0.9.1";
37
+ const API_VERSION = "1.0.0";
24
38
  const SPEC_FORMAT = "openapi";
39
+ const IDENTITY_POLICY = {};
40
+ let LOGIN_IDENTITY;
25
41
  const WHOAMI = { "resource": "account", "method": "retrieve" };
26
42
  const ENVIRONMENTS = {};
27
43
  const HAS_MCP = false;
28
44
  const PKG_NAME = "@typeship-ax/cli";
29
45
  const UPDATE_NOTICE = false;
30
- const API_DESCRIPTION = "Generate production SDKs, CLIs, and MCP servers from an OpenAPI or\nGraphQL spec, and keep every selected output current.\n\nEvery operation but one requires an API key, created in the console and\nsent as `Authorization: Bearer ak_...`. A browser session is not a\ncredential for this API. The exception is POST /generate, which works\nanonymously with the free plan's limits.\n";
46
+ const API_DESCRIPTION = "Resolve an OpenAPI or GraphQL Definition, diagnose it, and keep every\nselected SDK, CLI, and MCP 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";
31
47
  const DOCS_URL_DEFAULT = "https://typeship.dev";
48
+ const DOCS_INDEX_URL_DEFAULT = null;
32
49
  const RELAY = null;
33
50
  const SUPPORT_URL = null;
34
51
  const OAUTH_TOKEN_URL = null;
52
+ const OAUTH_ISSUER = null;
53
+ const OAUTH_DISCOVERY_URL = undefined;
54
+ const OAUTH_DISCOVERY_URLS = [];
55
+ const OAUTH_DEVICE_URL = null;
56
+ const HAS_OAUTH_LOGIN = OAUTH_TOKEN_URL !== null || OAUTH_DISCOVERY_URLS.length > 0;
57
+ const OAUTH_LOGIN_METHOD = "device";
58
+ const OAUTH_REDIRECT_URI = undefined;
59
+ const OAUTH_ORGANIZATION_PARAMETER = undefined;
60
+ const OAUTH_AUTHORIZATION_URL = undefined;
35
61
  const OAUTH_CLIENT_ID = null;
36
62
  const OAUTH_SCOPES = [];
37
63
  const OAUTH_TOKEN_PARAMS = {};
@@ -48,11 +74,12 @@ const API_TITLE = "typeship";
48
74
  * is known. */
49
75
  const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json"]);
50
76
  const BUILTIN_BOOLEAN_FLAGS = {
51
- login: ["with-token", "no-browser"],
77
+ login: ["with-token", "no-browser", "device"],
78
+ logout: ["local"],
52
79
  upgrade: ["check"],
53
80
  mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
54
81
  docs: ["web", "schema"],
55
- init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md"],
82
+ init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
56
83
  auth: ["live"],
57
84
  doctor: [],
58
85
  };
@@ -271,7 +298,7 @@ function humanError(body, code) {
271
298
  const firstWords = (issue?.message ?? "").slice(0, 40);
272
299
  if (compact && compact !== "{}" && !(firstWords && compact.includes(firstWords)))
273
300
  lines.push(" " + paintErr("dim", "API said: " + (compact.length > 300 ? compact.slice(0, 297) + "…" : compact)));
274
- const requestId = detail.body?.request_id ?? detail.body?.requestId;
301
+ const requestId = detail.request_id ?? detail.body?.request_id ?? detail.body?.requestId;
275
302
  if (typeof requestId === "string")
276
303
  lines.push(" " + paintErr("dim", "request id: " + requestId));
277
304
  }
@@ -290,7 +317,7 @@ function fail(code, message, extra, nextSteps) {
290
317
  : /^(Missing required|Expected \d+ argument)/.test(message) ? "MISSING_ARGUMENT"
291
318
  : "INVALID_USAGE";
292
319
  return failWith({
293
- code: code === 2 ? usageCode : "COMMAND_FAILED",
320
+ code: code === 2 ? usageCode : "CALL_FAILED",
294
321
  message,
295
322
  ...(extra !== undefined ? { detail: extra } : {}),
296
323
  nextSteps: nextSteps ?? (code === 2 ? ["Run '" + USAGE_HINT + "' for the flags this command takes."] : []),
@@ -300,24 +327,92 @@ function fail(code, message, extra, nextSteps) {
300
327
  function failApi(error, hadCredential) {
301
328
  return failWith(classifyApiError(error, { bin: BIN, hadCredential, docsUrl: DOCS_URL_DEFAULT }));
302
329
  }
303
- function configDir() {
330
+ // ---------------------------------------------------------------------------
331
+ // credentials — written by `login`, cleared by `logout`
332
+ // ---------------------------------------------------------------------------
333
+ function configRoot() {
304
334
  const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
305
335
  return join(base, BIN);
306
336
  }
337
+ let PROFILE;
338
+ function configDir() { return PROFILE.directory; }
307
339
  function credsPath() {
308
- return join(configDir(), "credentials.json");
340
+ return credentialStore().path;
309
341
  }
310
- function readCreds() {
311
- try {
312
- return JSON.parse(readFileSync(credsPath(), "utf8"));
342
+ function credentialStore() { return createCredentialStore(configDir(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
343
+ function readCreds() { return credentialStore().read(); }
344
+ function sessionConfiguration(baseUrl, config = readConfig()) {
345
+ return {
346
+ apiBaseUrl: baseUrl, environment: config.environment, profile: PROFILE.name,
347
+ identity: identityConfiguration(),
348
+ clientId: process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID,
349
+ issuer: OAUTH_ISSUER, discoveryUrl: OAUTH_DISCOVERY_URL, tokenUrl: OAUTH_TOKEN_URL,
350
+ authorizationUrl: OAUTH_AUTHORIZATION_URL, scopes: OAUTH_SCOPES,
351
+ audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
352
+ organizationParameter: OAUTH_ORGANIZATION_PARAMETER,
353
+ };
354
+ }
355
+ function identityConfiguration() {
356
+ return Object.keys(IDENTITY_POLICY).length && WHOAMI ? { operation: WHOAMI.resource + "." + WHOAMI.method, fields: IDENTITY_POLICY } : undefined;
357
+ }
358
+ function expectedLoginIdentity(flags) {
359
+ const expected = {};
360
+ for (const kind of ["subject", "account", "organization"]) {
361
+ const value = flags.get(kind);
362
+ if (value === undefined)
363
+ continue;
364
+ if (typeof value !== "string" || !value || !IDENTITY_POLICY[kind])
365
+ fail(2, "--" + kind + " requires an expected ID and a configured identity field for that ID.");
366
+ expected[kind] = value;
313
367
  }
314
- catch {
315
- return null;
368
+ const selection = requestedLoginOrganization(flags);
369
+ if (selection) {
370
+ if (expected.organization !== undefined && expected.organization !== selection.id)
371
+ fail(2, "--organization must match --login-organization when both are provided.");
372
+ expected.organization = selection.id;
316
373
  }
374
+ return Object.keys(expected).length ? expected : undefined;
317
375
  }
318
- function writeCreds(creds) {
319
- mkdirSync(configDir(), { recursive: true, mode: 0o700 });
320
- writeFileSync(credsPath(), JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
376
+ function requestedLoginOrganization(flags) {
377
+ const id = flags.get("login-organization");
378
+ if (id === undefined)
379
+ return;
380
+ if (!OAUTH_ORGANIZATION_PARAMETER || !IDENTITY_POLICY.organization)
381
+ fail(2, "--login-organization requires auth.oauth_organization_parameter and identity_organization configured by the API owner.");
382
+ if (typeof id !== "string" || !id || id.length > 512 || /\s|[\u0000-\u001F\u007F]/.test(id))
383
+ fail(2, "--login-organization requires a nonempty provider organization ID without whitespace.");
384
+ return { parameter: OAUTH_ORGANIZATION_PARAMETER, id: id };
385
+ }
386
+ function loginIdentityReport() { return LOGIN_IDENTITY ? { verified_identity: LOGIN_IDENTITY.values, identity_checked_at: LOGIN_IDENTITY.checkedAt } : {}; }
387
+ /** Each login replaces this profile's credentials and retains its API destination. */
388
+ async function saveLoginCredentials(credentials, flags, destination, onIdentityFailure) {
389
+ const config = readConfig();
390
+ const apiBaseUrl = destination?.apiBaseUrl ?? resolveBaseUrl(flags, config);
391
+ if (!apiBaseUrl)
392
+ fail(2, "Set the API base URL before logging in.");
393
+ const url = new URL(apiBaseUrl);
394
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
395
+ fail(2, "The API base URL must be HTTP or HTTPS without credentials, query, or fragment.");
396
+ const bound = destination ? { apiBaseUrl: destination.apiBaseUrl, environment: destination.environment, profile: destination.profile } : { apiBaseUrl: url.href, environment: config.environment, profile: PROFILE.name };
397
+ const next = { ...credentials, destination: bound };
398
+ const identity = identityConfiguration();
399
+ if (identity && WHOAMI) {
400
+ const op = OPS.find((op) => op.resource === WHOAMI.resource && op.method === WHOAMI.method);
401
+ if (!op)
402
+ fail(2, "The identity operation is unavailable. Regenerate this product with the configured identity read.");
403
+ const transport = identityFetch(bound.apiBaseUrl);
404
+ const verified = await verifyApiIdentity(async (anonymous) => {
405
+ const client = anonymous ? new TypeshipClient({ baseUrl: bound.apiBaseUrl, fetch: transport, maxRetries: 0, timeoutMs: 10_000 }) : await makeClient(flags, op, next, true);
406
+ return identityResult(client, op);
407
+ }, identity.fields, expectedLoginIdentity(flags)).catch(async (error) => {
408
+ await onIdentityFailure?.();
409
+ throw error;
410
+ });
411
+ next.identity = { ...verified, binding: credentialIdentityBinding(next, identity) };
412
+ LOGIN_IDENTITY = verified;
413
+ }
414
+ await credentialStore().update(() => next);
415
+ await updateProfileConfig(configDir(), (current) => ({ ...current, baseUrl: bound.apiBaseUrl }));
321
416
  }
322
417
  function readStdin() {
323
418
  return new Promise((resolve) => {
@@ -379,130 +474,48 @@ function promptHidden(promptText) {
379
474
  stdin.on("data", onData);
380
475
  });
381
476
  }
382
- async function oauthForm(url, params) {
383
- const response = await fetch(url, {
384
- method: "POST",
385
- headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
386
- body: new URLSearchParams(params).toString(),
387
- });
388
- let body = null;
477
+ async function withLoginCancellation(work) {
478
+ const controller = new AbortController();
479
+ const cancel = () => controller.abort();
480
+ process.once("SIGINT", cancel);
481
+ process.once("SIGTERM", cancel);
389
482
  try {
390
- body = await response.json();
391
- }
392
- catch { /* non-JSON error body */ }
393
- return { status: response.status, body };
394
- }
395
- /** RFC 8628 device flow: discover the device endpoint from the token URL's
396
- * well-known metadata, show the code, poll until authorized. */
397
- async function deviceLogin(clientId, agent = false) {
398
- if (!OAUTH_TOKEN_URL)
399
- fail(2, "This API declares no OAuth token URL.");
400
- const origin = new URL(OAUTH_TOKEN_URL).origin;
401
- let deviceEndpoint;
402
- let tokenEndpoint = OAUTH_TOKEN_URL;
403
- for (const wellKnown of ["/.well-known/oauth-authorization-server", "/.well-known/openid-configuration"]) {
404
- try {
405
- const response = await fetch(origin + wellKnown, { headers: { Accept: "application/json" } });
406
- if (!response.ok)
407
- continue;
408
- const meta = await response.json();
409
- if (meta.device_authorization_endpoint) {
410
- deviceEndpoint = meta.device_authorization_endpoint;
411
- if (meta.token_endpoint)
412
- tokenEndpoint = meta.token_endpoint;
413
- break;
414
- }
415
- }
416
- catch { /* try the next well-known path */ }
417
- }
418
- if (!deviceEndpoint) {
419
- fail(1, "The authorization server does not advertise a device flow. Run '" + BIN + " login' with a pasted credential instead.");
420
- }
421
- const start = await oauthForm(deviceEndpoint, {
422
- client_id: clientId,
423
- ...(OAUTH_SCOPES.length > 0 ? { scope: OAUTH_SCOPES.join(" ") } : {}),
424
- ...OAUTH_TOKEN_PARAMS,
425
- });
426
- const startBody = start.body;
427
- if (start.status !== 200 || !startBody?.device_code) {
428
- fail(1, "Device authorization failed (HTTP " + start.status + ").", start.body);
429
- }
430
- const uri = startBody.verification_uri_complete ?? startBody.verification_uri;
431
- process.stderr.write("Open " + paintErr("cyan", String(uri)) + " and enter code: " + paintErr("bold", String(startBody.user_code)) + "\n");
432
- // Under an agent the same facts also go out as one JSON line (stderr,
433
- // so stdout stays the single result document), for the agent to hand
434
- // the URL and code to the user while this polls.
435
- if (agent)
436
- process.stderr.write(JSON.stringify({ event: "device_code", verification_uri: uri, user_code: startBody.user_code, expires_in: startBody.expires_in ?? 900 }) + "\n");
437
- let intervalMs = (startBody.interval ?? 5) * 1000;
438
- const deadline = Date.now() + (startBody.expires_in ?? 900) * 1000;
439
- while (Date.now() < deadline) {
440
- await new Promise((r) => setTimeout(r, intervalMs));
441
- const poll = await oauthForm(tokenEndpoint, {
442
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
443
- device_code: startBody.device_code,
444
- client_id: clientId,
445
- });
446
- const tokenBody = poll.body;
447
- if (poll.status === 200 && tokenBody?.access_token) {
448
- writeCreds({
449
- ...(readCreds() ?? {}),
450
- oauth: {
451
- accessToken: tokenBody.access_token,
452
- refreshToken: tokenBody.refresh_token,
453
- expiresAt: tokenBody.expires_in ? Date.now() + tokenBody.expires_in * 1000 : undefined,
454
- },
455
- });
456
- process.stderr.write(paintErr("green", "Logged in.") + "\n");
457
- out({ ok: true, method: "device", credentials: credsPath() });
458
- await flushExit(0);
459
- }
460
- const errorCode = tokenBody?.error;
461
- if (errorCode === "authorization_pending")
462
- continue;
463
- if (errorCode === "slow_down") {
464
- intervalMs += 5000;
465
- continue;
466
- }
467
- fail(1, "Device login failed: " + (errorCode ?? "HTTP " + poll.status), poll.body);
468
- }
469
- fail(1, "Device login timed out before the code was entered.");
470
- }
471
- /** Stored OAuth access token, refreshed through the token URL when expired.
472
- * A failed refresh returns the stale token; the API's 401 explains better
473
- * than a local guess. */
474
- async function refreshedOauthToken(stored) {
475
- const oauth = stored.oauth;
476
- if (!oauth)
477
- return undefined;
478
- const expired = oauth.expiresAt !== undefined && Date.now() > oauth.expiresAt - 60_000;
479
- if (!expired || !oauth.refreshToken || !OAUTH_TOKEN_URL)
480
- return oauth.accessToken;
481
- const clientId = process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
482
- const result = await oauthForm(OAUTH_TOKEN_URL, {
483
- grant_type: "refresh_token",
484
- refresh_token: oauth.refreshToken,
485
- ...(clientId ? { client_id: clientId } : {}),
486
- });
487
- const body = result.body;
488
- if (result.status === 200 && body?.access_token) {
489
- const next = {
490
- accessToken: body.access_token,
491
- refreshToken: body.refresh_token ?? oauth.refreshToken,
492
- expiresAt: body.expires_in ? Date.now() + body.expires_in * 1000 : undefined,
493
- };
494
- writeCreds({ ...stored, oauth: next });
495
- return next.accessToken;
496
- }
497
- return oauth.accessToken;
483
+ return await work(controller.signal);
484
+ }
485
+ finally {
486
+ process.off("SIGINT", cancel);
487
+ process.off("SIGTERM", cancel);
488
+ }
489
+ }
490
+ async function deviceLogin(clientId, parsed) {
491
+ const apiBaseUrl = resolveBaseUrl(parsed.flags);
492
+ if (!apiBaseUrl)
493
+ fail(2, "Set the API base URL before logging in.");
494
+ const loginConfiguration = sessionConfiguration(apiBaseUrl);
495
+ await credentialStore().prepare();
496
+ const session = await withLoginCancellation((signal) => oauthDeviceLogin({
497
+ clientId, issuer: OAUTH_ISSUER, discoveryUrls: OAUTH_DISCOVERY_URLS,
498
+ deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: OAUTH_SCOPES,
499
+ audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
500
+ }, { signal, authorize({ verificationUri, userCode, expiresIn }) {
501
+ process.stderr.write("Open " + paintErr("cyan", verificationUri) + " and enter code: " + paintErr("bold", userCode) + "\n");
502
+ if (isAgentMode(parsed))
503
+ process.stderr.write(JSON.stringify({ event: "device_code", verification_uri: verificationUri, user_code: userCode, expires_in: expiresIn }) + "\n");
504
+ } }));
505
+ await saveLoginCredentials({ oauth: { ...session,
506
+ sessionId: randomBytes(16).toString("hex"), binding: sessionBinding(loginConfiguration),
507
+ apiBaseUrl, configuredClientId: loginConfiguration.clientId ?? null,
508
+ } }, parsed.flags, loginConfiguration);
509
+ process.stderr.write(paintErr("green", "Logged in.") + "\n");
510
+ out({ ok: true, method: "device", credentials: credsPath(), ...loginIdentityReport() });
511
+ await flushExit(0);
498
512
  }
499
- async function storePastedToken(stored, token) {
513
+ async function storePastedToken(token, flags) {
500
514
  const first = AUTH_SCALARS[0];
501
515
  if (!first)
502
516
  fail(2, "This API declares no credential the CLI can store. Use --username/--password if it uses basic auth.");
503
- stored.scalars = { ...stored.scalars, [first.option]: token };
504
- writeCreds(stored);
505
- out({ ok: true, method: "paste", stored_as: first.flag, credentials: credsPath() });
517
+ await saveLoginCredentials({ scalars: { [first.option]: token } }, flags);
518
+ out({ ok: true, method: "paste", stored_as: first.flag, credentials: credsPath(), ...loginIdentityReport() });
506
519
  await flushExit(0);
507
520
  }
508
521
  /**
@@ -512,97 +525,139 @@ async function storePastedToken(stored, token) {
512
525
  * until the API hands back a key minted for this CLI, store it. The key
513
526
  * never crosses the chat: the agent relays a URL, the person clicks once.
514
527
  */
515
- /** The approval endpoint follows the base URL: a preview or local deployment
516
- * of the API approves its own logins. */
528
+ /** A same-origin API override uses that deployment's approval route. A
529
+ * separately hosted identity service retains its explicitly configured URL. */
517
530
  function cliAuthUrl(flags) {
531
+ const auth = loginEndpoint(CLI_AUTH_URL);
518
532
  const base = resolveBaseUrl(flags);
519
- try {
520
- if (CLI_AUTH_URL && DEFAULT_BASE_URL && base) {
521
- const defaultOrigin = new URL(DEFAULT_BASE_URL).origin;
522
- const origin = new URL(base).origin;
523
- if (origin !== defaultOrigin && CLI_AUTH_URL.startsWith(defaultOrigin))
524
- return origin + CLI_AUTH_URL.slice(defaultOrigin.length);
525
- }
533
+ if (DEFAULT_BASE_URL && base && auth.origin === new URL(DEFAULT_BASE_URL).origin) {
534
+ const selected = loginEndpoint(base);
535
+ auth.protocol = selected.protocol;
536
+ auth.host = selected.host;
526
537
  }
527
- catch { /* fall through to the configured URL */ }
528
- return CLI_AUTH_URL;
538
+ return auth.href;
529
539
  }
530
- /** Browser approval, start to key: opens (or prints) the approval URL and
531
- * polls until a person decides. Returns the minted credential; every
532
- * failure exits with the envelope. Shared by `login` and `init`. */
533
540
  async function browserApprove(headless, flags) {
541
+ return withLoginCancellation((signal) => customBrowserApproval({
542
+ authUrl: cliAuthUrl(flags), name: BIN + " CLI on " + hostname(), source: headless && !process.stdin.isTTY ? "agent" : "cli",
543
+ }, { signal, authorize({ verificationUrl, expiresIn }) {
544
+ process.stderr.write("Approve this CLI in your browser: " + paintErr("cyan", verificationUrl) + "\n");
545
+ if (headless)
546
+ process.stderr.write(JSON.stringify({ event: "browser_approval", verification_url: verificationUrl, expires_in: expiresIn, note: "Give this URL to the user; polling until they approve or it expires." }) + "\n");
547
+ else
548
+ openInBrowser(verificationUrl);
549
+ } }));
550
+ }
551
+ /** Store what the browser approval minted, marked as this CLI's own. */
552
+ async function storeMinted(minted, flags) {
534
553
  const first = AUTH_SCALARS[0];
535
- const authUrl = cliAuthUrl(flags);
536
- const verifier = randomBytes(32).toString("base64url");
537
- const challenge = createHash("sha256").update(verifier).digest("base64url");
538
- const name = BIN + " CLI on " + hostname();
539
- const source = headless && !process.stdin.isTTY ? "agent" : "cli";
540
- let start;
541
- try {
542
- const response = await fetch(authUrl + "/start", {
543
- method: "POST",
544
- headers: { "Content-Type": "application/json" },
545
- body: JSON.stringify({ code_challenge: challenge, name, source }),
546
- signal: AbortSignal.timeout(15_000),
547
- });
548
- start = await response.json();
549
- if (!response.ok || !start.session || !start.verification_url) {
550
- failWith({ code: "COMMAND_FAILED", message: "Could not start the browser login: " + (start.error ?? "HTTP " + response.status), nextSteps: ["Pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
551
- }
552
- }
553
- catch (e) {
554
- failWith({ code: "NETWORK_ERROR", message: "Could not reach " + authUrl + "/start: " + e.message, nextSteps: ["Check the network, or pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
555
- }
556
- const url = start.verification_url;
557
- const expiresIn = start.expires_in ?? 600;
558
- const intervalMs = Math.max(1, start.interval ?? 3) * 1000;
559
- process.stderr.write("Approve this CLI in your browser: " + paintErr("cyan", url) + "\n");
560
- if (headless) {
561
- process.stderr.write(JSON.stringify({ event: "browser_approval", verification_url: url, expires_in: expiresIn, note: "Give this URL to the user; polling until they approve or it expires." }) + "\n");
562
- }
563
- else {
564
- openInBrowser(url);
565
- }
566
- const deadline = Date.now() + expiresIn * 1000;
567
- while (Date.now() < deadline) {
568
- await new Promise((r) => setTimeout(r, intervalMs));
569
- let poll;
554
+ await saveLoginCredentials({
555
+ scalars: { [first.option]: minted.api_key },
556
+ minted: { via: "browser", key_name: minted.key_name, revocationUrl: minted.revocationUrl, ...(minted.org_id ? { org_id: minted.org_id } : {}) },
557
+ }, flags, undefined, async () => {
570
558
  try {
571
- const response = await fetch(authUrl + "/status", {
572
- method: "POST",
573
- headers: { "Content-Type": "application/json" },
574
- body: JSON.stringify({ session: start.session, code_verifier: verifier }),
575
- signal: AbortSignal.timeout(15_000),
576
- });
577
- poll = await response.json();
559
+ const saved = credentialStore().read();
560
+ if ([...Object.values(saved?.scalars ?? {}), ...Object.values(saved?.named ?? {}), saved?.oauth?.accessToken, saved?.oauth?.refreshToken].includes(minted.api_key)) {
561
+ process.stderr.write("The rejected approval returned an existing saved credential; it was not revoked.\n");
562
+ return;
563
+ }
564
+ const response = await oauthStatusRequest(loginEndpoint(minted.revocationUrl), { method: "POST", headers: { Authorization: "Bearer " + minted.api_key } }, 15_000);
565
+ if (response.status < 200 || response.status >= 300)
566
+ throw new Error("Revocation failed");
578
567
  }
579
568
  catch {
580
- continue; // a blip; the next tick tries again
581
- }
582
- if (poll.status === "pending")
583
- continue;
584
- if (poll.status === "complete" && poll.api_key) {
585
- return { api_key: poll.api_key, key_name: poll.key_name ?? name, ...(poll.org_id ? { org_id: poll.org_id } : {}) };
569
+ process.stderr.write("The rejected approval's new credential could not be revoked. Revoke it in your API account.\n");
586
570
  }
587
- if (poll.status === "denied")
588
- failWith({ status: "action_required", code: "AUTH_INVALID", message: "The request was denied in the browser.", nextSteps: ["Run '" + BIN + " login' again if that was a mistake, or pass a credential directly with --" + first.flag + "."] });
589
- if (poll.status === "expired")
590
- break;
591
- failWith({ code: "COMMAND_FAILED", message: "Browser login stopped: " + (poll.status ?? "unknown status"), nextSteps: ["Run '" + BIN + " login' again."] });
592
- }
593
- failWith({ status: "action_required", code: "TTY_REQUIRED", message: "The browser approval expired after " + expiresIn + "s without a decision.", nextSteps: ["Run '" + BIN + " login' again and approve the link within ten minutes.", "Or pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
594
- }
595
- /** Store what the browser approval minted, marked as this CLI's own. */
596
- function storeMinted(stored, minted) {
597
- const first = AUTH_SCALARS[0];
598
- stored.scalars = { ...stored.scalars, [first.option]: minted.api_key };
599
- stored.minted = { via: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}) };
600
- writeCreds(stored);
571
+ });
601
572
  }
602
- async function browserLogin(stored, headless, flags) {
573
+ async function browserLogin(headless, flags) {
574
+ await credentialStore().prepare();
603
575
  const minted = await browserApprove(headless, flags);
604
- storeMinted(stored, minted);
605
- out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath() });
576
+ await storeMinted(minted, flags);
577
+ out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
578
+ await flushExit(0);
579
+ }
580
+ async function acquireOAuthBrowserSession(parsed, clientId) {
581
+ if (!OAUTH_ISSUER)
582
+ fail(2, "Browser OAuth requires the exact auth.oauth_issuer configured by the API owner.");
583
+ const apiBaseUrl = resolveBaseUrl(parsed.flags);
584
+ if (!apiBaseUrl)
585
+ fail(2, "Set the API base URL before logging in.");
586
+ const loginConfiguration = sessionConfiguration(apiBaseUrl);
587
+ await credentialStore().prepare();
588
+ const session = await startOAuthBrowserSession(parsed, clientId);
589
+ await saveLoginCredentials({ oauth: { ...session, apiBaseUrl,
590
+ sessionId: randomBytes(16).toString("hex"), binding: sessionBinding(loginConfiguration),
591
+ configuredClientId: loginConfiguration.clientId ?? null,
592
+ } }, parsed.flags, loginConfiguration);
593
+ }
594
+ /** Normal login and Console verification use the same native exchange. */
595
+ async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
596
+ const controller = new AbortController();
597
+ const cancel = () => controller.abort();
598
+ process.once("SIGINT", cancel);
599
+ process.once("SIGTERM", cancel);
600
+ try {
601
+ return await oauthBrowserLogin({
602
+ issuer: OAUTH_ISSUER, clientId, discoveryUrl: OAUTH_DISCOVERY_URL,
603
+ authorizationUrl: OAUTH_AUTHORIZATION_URL, tokenUrl: OAUTH_TOKEN_URL ?? undefined,
604
+ redirectUri: OAUTH_REDIRECT_URI, scopes: OAUTH_SCOPES,
605
+ audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
606
+ organization: requestedLoginOrganization(parsed.flags),
607
+ }, { signal: controller.signal, timeoutMs, authorize(url) {
608
+ process.stderr.write("Sign in to your existing account: " + url + "\n");
609
+ if (isAgentMode(parsed) || parsed.flags.get("no-browser") === true)
610
+ process.stderr.write(JSON.stringify({ event: "oauth_browser", authorization_url: url, note: "Open this URL in a browser on the same computer as the CLI." }) + "\n");
611
+ else
612
+ openInBrowser(url);
613
+ } });
614
+ }
615
+ finally {
616
+ process.off("SIGINT", cancel);
617
+ process.off("SIGTERM", cancel);
618
+ }
619
+ }
620
+ async function cmdConsoleLoginCheck(parsed) {
621
+ const file = parsed.flags.get("console-check");
622
+ if (typeof file !== "string" || !file || parsed.flags.get("device") === true || parsed.flags.get("with-token") === true || explicitNonInteractive(parsed))
623
+ fail(2, "--console-check requires a downloaded JSON file and browser login. Use --no-browser to print the sign-in link.");
624
+ const baseUrl = resolveBaseUrl(parsed.flags);
625
+ const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined) ?? process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
626
+ const op = WHOAMI && OPS.find((value) => value.resource === WHOAMI.resource && value.method === WHOAMI.method);
627
+ if (!baseUrl || !OAUTH_ISSUER || !clientId || OAUTH_LOGIN_METHOD !== "browser" || !op || op.auth !== "required" || !op.security?.length)
628
+ fail(2, "Configure browser OAuth and a required-authentication identity read, then regenerate this CLI.");
629
+ const envOptions = {}, flagOptions = {};
630
+ for (const scalar of AUTH_SCALARS) {
631
+ if (process.env[scalar.env] !== undefined)
632
+ envOptions[scalar.option] = process.env[scalar.env];
633
+ if (typeof parsed.flags.get(scalar.flag) === "string")
634
+ flagOptions[scalar.option] = parsed.flags.get(scalar.flag);
635
+ }
636
+ if (BASIC && process.env[BASIC.envUser] && process.env[BASIC.envPass])
637
+ envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
638
+ if (BASIC && typeof parsed.flags.get("username") === "string" && typeof parsed.flags.get("password") === "string")
639
+ flagOptions.basicAuth = { username: parsed.flags.get("username"), password: parsed.flags.get("password") };
640
+ const environment = (typeof parsed.flags.get("environment") === "string" ? parsed.flags.get("environment") : Object.entries(ENVIRONMENTS).find(([, url]) => new URL(url).href === new URL(baseUrl).href)?.[0]) ?? null;
641
+ const result = await checkConsoleBrowserLogin({
642
+ file: file, configuration: {
643
+ baseUrl: baseUrl, environment, operation: op.resource + "." + op.method, requirements: op.security,
644
+ 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 },
645
+ issuer: OAUTH_ISSUER, clientId: clientId, discoveryUrl: OAUTH_DISCOVERY_URL ?? null,
646
+ authorizationUrl: OAUTH_AUTHORIZATION_URL ?? null, tokenUrl: OAUTH_TOKEN_URL,
647
+ redirectUri: OAUTH_REDIRECT_URI ?? "http://127.0.0.1/callback", scopes: OAUTH_SCOPES,
648
+ audience: OAUTH_TOKEN_PARAMS.audience ?? null, resource: OAUTH_TOKEN_PARAMS.resource ?? null,
649
+ }, schemes: NAMED_SCHEMES,
650
+ credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ options: envOptions, named: environmentCredentials() }, { options: flagOptions, named: flagCredentials(parsed.flags) }]),
651
+ login: (timeoutMs) => startOAuthBrowserSession(parsed, clientId, timeoutMs),
652
+ async verify(credentials, expectations) {
653
+ const kind = (value) => value === "user" ? "subject" : value;
654
+ const policy = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), entry.pointer]));
655
+ const expected = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), String(entry.expected)]));
656
+ await verifyClientIdentity((options) => new TypeshipClient(options), { baseUrl: baseUrl, credentials }, op, policy, expected);
657
+ },
658
+ progress: (message) => process.stderr.write(message + "\n"),
659
+ });
660
+ out(result);
606
661
  await flushExit(0);
607
662
  }
608
663
  async function cmdLogin(parsed) {
@@ -613,16 +668,34 @@ async function cmdLogin(parsed) {
613
668
  ...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
614
669
  ...(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)"] : []),
615
670
  " " + BIN + " login --with-token read the credential from stdin (CI)",
616
- ...(OAUTH_TOKEN_URL ? [" " + BIN + " login --client-id <id> OAuth device flow" + (OAUTH_CLIENT_ID ? " (a default id is built in)" : "")] : []),
671
+ " " + BIN + " login --console-check <file> test browser login for your Typeship Console without replacing saved logins",
672
+ ...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --client-id <id> OAuth " + OAUTH_LOGIN_METHOD + " login" + (OAUTH_CLIENT_ID ? " (a default id is built in)" : "")] : []),
673
+ ...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --no-browser print the approval URL instead of opening a browser", " " + BIN + " login --device use device authorization when your provider supports it"] : []),
617
674
  ...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
618
- ...(CLI_AUTH_URL ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
675
+ ...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
619
676
  "",
620
- "Precedence at request time: flags > env vars > stored credentials.",
677
+ "Precedence per scheme: flags > env vars > stored credentials. Named inputs beat convenience flags within the same source.",
678
+ " " + BIN + " login --credentials @<JSON-file> store named credentials (use - for stdin)",
679
+ "Named schemes: " + Object.entries(NAMED_SCHEMES).map(([name, scheme]) => name + " (" + scheme.kind + ")").join(", "),
680
+ "JSON values are tokens/API keys, or {username, password} for Basic auth. Runtime env: TYPESHIP_CREDENTIALS.",
681
+ "--profile <name> selects an isolated login. Use auth profiles to list profiles and auth use <name> to select a default.",
682
+ ...(Object.keys(IDENTITY_POLICY).length ? ["Login verifies your API identity before saving. Add --subject <id>, --account <id> or --organization <id> to require a particular mapped identity."] : []),
683
+ ...(OAUTH_ORGANIZATION_PARAMETER ? ["--login-organization <id> requests that provider organization during browser login and requires the API to confirm the same ID."] : []),
684
+ "Saved credentials use OS protection by default. TYPESHIP_CREDENTIAL_STORE=file explicitly opts into plaintext storage.",
621
685
  ];
622
686
  process.stdout.write(lines.join("\n") + "\n");
623
687
  await flushExit(0);
624
688
  }
625
- const stored = readCreds() ?? {};
689
+ if (parsed.flags.has("login-organization")) {
690
+ expectedLoginIdentity(parsed.flags);
691
+ if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("console-check") || parsed.flags.has("with-token") || explicitNonInteractive(parsed))
692
+ fail(2, "--login-organization is available only for interactive OAuth browser login, including --no-browser.");
693
+ }
694
+ if (parsed.flags.has("console-check")) {
695
+ await cmdConsoleLoginCheck(parsed);
696
+ return;
697
+ }
698
+ const named = flagCredentials(parsed.flags);
626
699
  const scalarValues = {};
627
700
  for (const a of AUTH_SCALARS) {
628
701
  const v = parsed.flags.get(a.flag);
@@ -632,31 +705,38 @@ async function cmdLogin(parsed) {
632
705
  const username = parsed.flags.get("username");
633
706
  const password = parsed.flags.get("password");
634
707
  const gotBasic = BASIC !== null && typeof username === "string" && typeof password === "string";
635
- if (Object.keys(scalarValues).length > 0 || gotBasic) {
636
- if (Object.keys(scalarValues).length > 0)
637
- stored.scalars = { ...stored.scalars, ...scalarValues };
638
- if (gotBasic)
639
- stored.basic = { username: username, password: password };
640
- writeCreds(stored);
641
- out({ ok: true, method: "flags", credentials: credsPath() });
708
+ if (Object.keys(scalarValues).length > 0 || gotBasic || Object.keys(named).length > 0) {
709
+ if (parsed.flags.has("login-organization"))
710
+ fail(2, "--login-organization cannot be combined with pasted credentials.");
711
+ await saveLoginCredentials({
712
+ ...(Object.keys(named).length ? { named } : {}),
713
+ ...(Object.keys(scalarValues).length > 0 ? { scalars: scalarValues } : {}),
714
+ ...(gotBasic ? { basic: { username: username, password: password } } : {}),
715
+ }, parsed.flags);
716
+ out({ ok: true, method: "flags", credentials: credsPath(), ...loginIdentityReport() });
642
717
  await flushExit(0);
643
718
  }
644
719
  if (parsed.flags.get("with-token") === true) {
645
720
  const token = (await readStdin()).trim();
646
721
  if (!token)
647
722
  fail(2, "--with-token expects the credential on stdin.");
648
- await storePastedToken(stored, token);
723
+ await storePastedToken(token, parsed.flags);
649
724
  }
650
725
  const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined)
651
726
  ?? process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID ?? undefined;
652
- if (OAUTH_TOKEN_URL && clientId !== undefined && !explicitNonInteractive(parsed)) {
653
- await deviceLogin(clientId, isAgentMode(parsed));
727
+ if (HAS_OAUTH_LOGIN && clientId !== undefined && !explicitNonInteractive(parsed)) {
728
+ if (OAUTH_LOGIN_METHOD === "browser" && parsed.flags.get("device") !== true) {
729
+ await acquireOAuthBrowserSession(parsed, clientId);
730
+ out({ ok: true, method: "oauth_browser", credentials: credsPath(), ...loginIdentityReport() });
731
+ await flushExit(0);
732
+ }
733
+ await deviceLogin(clientId, parsed);
654
734
  }
655
735
  // Browser approval: the API mints a key for this CLI once a person
656
736
  // approves in the browser. Works under an agent too (it prints the URL and
657
737
  // polls); only the explicit non-interactive switch turns it off.
658
738
  if (CLI_AUTH_URL && AUTH_SCALARS[0] && !explicitNonInteractive(parsed)) {
659
- await browserLogin(stored, isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
739
+ await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
660
740
  }
661
741
  if (nonInteractive(parsed) || !process.stdin.isTTY) {
662
742
  failWith({
@@ -666,7 +746,7 @@ async function cmdLogin(parsed) {
666
746
  nextSteps: [
667
747
  ...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
668
748
  "Pipe it: echo \"$TOKEN\" | " + BIN + " login --with-token",
669
- ...(OAUTH_TOKEN_URL ? ["Device flow: '" + BIN + " login --client-id <id>' (prints a URL and code for the user)."] : []),
749
+ ...(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)."] : []),
670
750
  ...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
671
751
  ],
672
752
  });
@@ -677,34 +757,49 @@ async function cmdLogin(parsed) {
677
757
  const token = (await promptHidden("Paste " + first.flag.replace(/-/g, " ") + " (input hidden): ")).trim();
678
758
  if (!token)
679
759
  fail(2, "Nothing entered.");
680
- await storePastedToken(stored, token);
760
+ await storePastedToken(token, parsed.flags);
681
761
  }
682
- async function cmdLogout() {
683
- const stored = readCreds();
684
- const existed = existsSync(credsPath());
762
+ async function cmdLogout(parsed) {
763
+ if (parsed.flags.get("local") === true) {
764
+ const removed = await credentialStore().clear();
765
+ out({ ok: true, removed: removed ? credsPath() : null, revoked: false, revocation_skipped: true });
766
+ await flushExit(0);
767
+ }
768
+ const stored = await credentialStore().take();
769
+ const existed = stored !== null;
685
770
  // A key this CLI minted for itself (browser approval) is revoked on the
686
771
  // way out, so logging out ends the credential and not just the file. A
687
772
  // pasted or CI key is someone else's to revoke, and is left alone.
688
773
  let revoked = null;
689
774
  const first = AUTH_SCALARS[0];
690
775
  const ownKey = first && stored?.minted?.via === "browser" ? stored.scalars?.[first.option] : undefined;
691
- if (ownKey && CLI_AUTH_URL) {
776
+ if (ownKey) {
692
777
  try {
693
- const response = await fetch(CLI_AUTH_URL + "/revoke", { method: "POST", headers: { Authorization: "Bearer " + ownKey }, signal: AbortSignal.timeout(15_000) });
694
- revoked = response.ok;
778
+ const response = await oauthStatusRequest(loginEndpoint(stored.minted.revocationUrl), { method: "POST", headers: { Authorization: "Bearer " + ownKey } }, 15_000);
779
+ revoked = response.status >= 200 && response.status < 300;
780
+ }
781
+ catch {
782
+ revoked = false;
783
+ }
784
+ }
785
+ if (stored?.oauth?.revocationUrl) {
786
+ try {
787
+ const oauth = stored.oauth;
788
+ const token = oauth.refreshToken ?? oauth.accessToken;
789
+ const result = await oauthStatusRequest(loginEndpoint(oauth.revocationUrl), { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token, token_type_hint: oauth.refreshToken ? "refresh_token" : "access_token", ...(oauth.clientId ? { client_id: oauth.clientId } : {}) }) });
790
+ revoked = result.status >= 200 && result.status < 300;
695
791
  }
696
792
  catch {
697
793
  revoked = false;
698
794
  }
699
795
  }
700
- rmSync(credsPath(), { force: true });
701
796
  out({ ok: true, removed: existed ? credsPath() : null, ...(revoked === null ? {} : { revoked, key_name: stored?.minted?.key_name }) });
702
797
  await flushExit(0);
703
798
  }
704
799
  async function cmdWhoami(parsed) {
705
800
  const op = WHOAMI ? OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method) : undefined;
706
801
  if (op) {
707
- const client = await makeClient(parsed.flags);
802
+ const client = await makeClient(parsed.flags, op);
708
803
  const target = client[op.resource];
709
804
  const result = await target[op.method]();
710
805
  if (result.ok) {
@@ -713,36 +808,14 @@ async function cmdWhoami(parsed) {
713
808
  }
714
809
  failApi(result.error, LAST_CLIENT_HAD_CREDENTIAL);
715
810
  }
716
- const stored = readCreds();
717
- let source = "none";
718
- if (AUTH_SCALARS.some((a) => typeof parsed.flags.get(a.flag) === "string")) {
719
- source = "flags";
720
- }
721
- else {
722
- const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
723
- if (envScalar)
724
- source = "env:" + envScalar.env;
725
- else if (stored && (stored.scalars || stored.basic || stored.oauth))
726
- source = "login";
727
- }
811
+ const source = credentialSource(parsed.flags) ?? "none";
728
812
  out({ authenticated: source !== "none", source, credentials: existsSync(credsPath()) ? credsPath() : null });
729
813
  await flushExit(source === "none" ? 1 : 0);
730
814
  }
731
815
  function configFilePath() {
732
816
  return join(configDir(), "config.json");
733
817
  }
734
- function readConfig() {
735
- try {
736
- return JSON.parse(readFileSync(configFilePath(), "utf8"));
737
- }
738
- catch {
739
- return {};
740
- }
741
- }
742
- function writeConfig(config) {
743
- mkdirSync(configDir(), { recursive: true, mode: 0o700 });
744
- writeFileSync(configFilePath(), JSON.stringify(config, null, 2) + "\n");
745
- }
818
+ function readConfig() { return readProfileConfig(configDir()); }
746
819
  const CONFIG_KEYS = ["base-url", "environment", "docs-url"];
747
820
  function configGet(config, key) {
748
821
  return key === "base-url" ? config.baseUrl : key === "environment" ? config.environment : config.docsUrl;
@@ -773,6 +846,7 @@ async function cmdConfig(parsed) {
773
846
  if (sub === undefined || sub === "list") {
774
847
  const config = readConfig();
775
848
  out({
849
+ profile: PROFILE.name, profile_source: PROFILE.source,
776
850
  base_url: config.baseUrl ?? null,
777
851
  environment: config.environment ?? null,
778
852
  docs_url: config.docsUrl ?? DOCS_URL_DEFAULT,
@@ -795,13 +869,16 @@ async function cmdConfig(parsed) {
795
869
  await flushExit(0);
796
870
  }
797
871
  if (sub === "unset") {
798
- if (key === "base-url")
799
- delete config.baseUrl;
800
- else if (key === "environment")
801
- delete config.environment;
802
- else
803
- delete config.docsUrl;
804
- writeConfig(config);
872
+ await updateProfileConfig(configDir(), (current) => {
873
+ const next = { ...current };
874
+ if (key === "base-url")
875
+ delete next.baseUrl;
876
+ else if (key === "environment")
877
+ delete next.environment;
878
+ else
879
+ delete next.docsUrl;
880
+ return next;
881
+ });
805
882
  out({ ok: true });
806
883
  await flushExit(0);
807
884
  }
@@ -814,10 +891,6 @@ async function cmdConfig(parsed) {
814
891
  catch {
815
892
  fail(2, key + " must be a valid URL");
816
893
  }
817
- if (key === "base-url")
818
- config.baseUrl = value;
819
- else
820
- config.docsUrl = value;
821
894
  }
822
895
  else {
823
896
  if (!(value in ENVIRONMENTS)) {
@@ -825,9 +898,8 @@ async function cmdConfig(parsed) {
825
898
  ? "environment must be one of: " + Object.keys(ENVIRONMENTS).join(", ")
826
899
  : "The spec declares no named environments; use 'config set base-url' instead.");
827
900
  }
828
- config.environment = value;
829
901
  }
830
- writeConfig(config);
902
+ await updateProfileConfig(configDir(), (current) => key === "base-url" ? { ...current, baseUrl: value } : key === "docs-url" ? { ...current, docsUrl: value } : { ...current, environment: value, baseUrl: undefined });
831
903
  out({ ok: true, [key.replace(/-/g, "_")]: value });
832
904
  await flushExit(0);
833
905
  }
@@ -848,15 +920,6 @@ function mcpServerPath() {
848
920
  }
849
921
  return { path, warning };
850
922
  }
851
- function claudeDesktopConfigPath() {
852
- if (process.platform === "darwin") {
853
- return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
854
- }
855
- if (process.platform === "win32") {
856
- return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
857
- }
858
- return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "Claude", "claude_desktop_config.json");
859
- }
860
923
  /** The mcp entry for a client: the hosted endpoint when the API has one
861
924
  * (auth env var as a reference, never a literal), else the local stdio
862
925
  * server. --url overrides. */
@@ -878,7 +941,12 @@ function mcpEntryFor(url, readOnly = false) {
878
941
  const server = mcpServerPath();
879
942
  if (server.warning)
880
943
  warnings.push(server.warning);
881
- return { entry: { command: "node", args: [server.path, ...(readOnly ? ["--read-only"] : [])] }, warnings };
944
+ const environment = {
945
+ ...(process.env.XDG_CONFIG_HOME !== undefined ? { XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME } : {}),
946
+ ...(process.env["TYPESHIP_CREDENTIAL_STORE"] !== undefined ? { "TYPESHIP_CREDENTIAL_STORE": process.env["TYPESHIP_CREDENTIAL_STORE"] } : {}),
947
+ };
948
+ const pinned = PROFILE.source === "flag" || PROFILE.source === "environment";
949
+ return { entry: { command: "node", args: [server.path, ...(pinned ? ["--profile", PROFILE.name] : []), ...(readOnly ? ["--read-only"] : [])], ...(Object.keys(environment).length ? { env: environment } : {}) }, warnings };
882
950
  }
883
951
  const MCP_CLIENT_FLAGS = {
884
952
  claude: "claude-code", cursor: "cursor", "claude-desktop": "claude-desktop", codex: "codex", vscode: "vscode",
@@ -900,11 +968,20 @@ async function cmdMcp(parsed) {
900
968
  " " + BIN + " mcp install --claude --read-only register a read-only server (writes are not callable)",
901
969
  "",
902
970
  (MCP_URL ? "Default entry: the hosted endpoint " + MCP_URL + " with the auth env var as a reference (never a literal key)." : "Default entry: this package's local stdio server, which reads credentials saved by '" + BIN + " login' or the CLI's auth env vars."),
903
- "The old spelling '" + BIN + " mcp --claude' still works. --all skips Cursor until it speaks MCP 2026-07-28.",
971
+ "--all skips Cursor until it speaks MCP 2026-07-28.",
904
972
  ];
905
973
  process.stdout.write(lines.join("\n") + "\n");
906
974
  await flushExit(0);
907
975
  }
976
+ const sub = parsed.positionals[1];
977
+ if (sub !== undefined && sub !== "install") {
978
+ fail(2, "Unknown mcp command: " + sub + ". Try '" + BIN + " mcp install --all'.");
979
+ }
980
+ const requestedInstall = parsed.flags.get("all") === true
981
+ || Object.keys(MCP_CLIENT_FLAGS).some((flag) => parsed.flags.get(flag) === true);
982
+ if (requestedInstall && sub !== "install") {
983
+ fail(2, "Installing an MCP entry requires '" + BIN + " mcp install'.");
984
+ }
908
985
  const url = typeof parsed.flags.get("url") === "string" ? parsed.flags.get("url") : undefined;
909
986
  if (url) {
910
987
  try {
@@ -968,8 +1045,11 @@ function agentContext() {
968
1045
  apiTitle: API_TITLE,
969
1046
  version: VERSION,
970
1047
  envPrefix: ENV_PREFIX,
971
- authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : [])],
1048
+ authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? ["TYPESHIP_CREDENTIALS"] : [])],
972
1049
  docsUrl: docsSiteUrl(),
1050
+ docsIndexUrl: docsIndexUrl(),
1051
+ generatedOperationCount: OPS.length,
1052
+ omittedOperations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
973
1053
  mcpUrl: MCP_URL,
974
1054
  skillsRepo: SKILLS_REPO,
975
1055
  hasMcp: HAS_MCP,
@@ -1028,13 +1108,21 @@ function helpJson() {
1028
1108
  };
1029
1109
  }),
1030
1110
  })),
1111
+ ...(EXCLUDED_OPS > 0 ? {
1112
+ coverage: {
1113
+ generated_operations: OPS.length,
1114
+ total_operations: OPS.length + EXCLUDED_OPS,
1115
+ omitted_operations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
1116
+ reason: "plan_limit",
1117
+ },
1118
+ } : {}),
1031
1119
  discovery: {
1032
1120
  search: BIN + " docs search <term> --json",
1033
1121
  operation: BIN + " docs <resource> <command> --json",
1034
1122
  note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
1035
1123
  },
1036
1124
  builtins: BUILTIN_COMMANDS,
1037
- global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--base-url <url>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
1125
+ 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>")],
1038
1126
  auth_env_vars: agentContext().authEnvVars,
1039
1127
  };
1040
1128
  }
@@ -1049,32 +1137,48 @@ async function cmdAgentGuide(parsed) {
1049
1137
  /** Which credential the CLI would send, without sending it. --live calls the identity endpoint too. */
1050
1138
  async function cmdAuth(parsed) {
1051
1139
  const sub = parsed.positionals[1];
1140
+ if (!parsed.help && sub === "profiles") {
1141
+ out({ profile: PROFILE.name, source: PROFILE.source, profiles: listProfiles(configRoot()).map((entry) => ({ name: entry.name, selected: entry.name === PROFILE.name, has_saved_credentials: entry.hasSavedCredentials, base_url: entry.config.baseUrl ?? (entry.config.environment ? ENVIRONMENTS[entry.config.environment] : undefined) ?? DEFAULT_BASE_URL, environment: entry.config.environment ?? null })) });
1142
+ await flushExit(0);
1143
+ }
1144
+ if (!parsed.help && (sub === "use" || sub === "remove")) {
1145
+ const name = parsed.positionals[2];
1146
+ if (!name)
1147
+ fail(2, "auth " + sub + " requires a profile name.");
1148
+ if (sub === "use")
1149
+ selectProfile(configRoot(), name);
1150
+ else
1151
+ await removeProfile(configRoot(), name);
1152
+ const effective = resolveProfile(configRoot(), { flag: typeof parsed.flags.get("profile") === "string" ? parsed.flags.get("profile") : undefined, environment: process.env["TYPESHIP_PROFILE"], allowMissing: true });
1153
+ out({ ok: true, ...(sub === "use" ? { selected: name } : { removed: name }), profile: effective.name, source: effective.source });
1154
+ await flushExit(0);
1155
+ }
1052
1156
  if (parsed.help || sub !== "check") {
1053
1157
  process.stdout.write([
1054
1158
  BIN + " auth check [--live] — report the credential the CLI would use, as JSON: {status: ok|action_required, authenticated, source, ...}",
1159
+ " " + BIN + " auth profiles list profiles without unlocking credentials",
1160
+ " " + BIN + " auth use <name> select the default profile",
1161
+ " " + BIN + " auth remove <name> remove a profile after logout",
1162
+ " --profile <name> overrides TYPESHIP_PROFILE, then the saved selection, then default.",
1055
1163
  " --live also call the API's identity endpoint" + (WHOAMI ? "" : " (none in this API; --live is a no-op)"),
1056
1164
  "",
1057
1165
  "Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
1058
1166
  ].join("\n") + "\n");
1059
1167
  await flushExit(parsed.help ? 0 : 2);
1060
1168
  }
1061
- const stored = readCreds();
1062
- let source = "none";
1063
- if (AUTH_SCALARS.some((a) => typeof parsed.flags.get(a.flag) === "string"))
1064
- source = "flags";
1065
- else {
1066
- const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
1067
- if (envScalar)
1068
- source = "env:" + envScalar.env;
1069
- else if (stored && (stored.scalars || stored.basic || stored.oauth))
1070
- source = "login";
1071
- }
1169
+ const source = credentialSource(parsed.flags) ?? "none";
1072
1170
  const authenticated = source !== "none";
1171
+ const savedIdentity = source === "login" ? readCreds() : null;
1172
+ if (savedIdentity)
1173
+ assertStoredIdentity(savedIdentity, identityConfiguration());
1073
1174
  const report = {
1074
1175
  status: authenticated ? "ok" : "action_required",
1075
1176
  authenticated,
1076
1177
  source,
1077
1178
  credentials_path: existsSync(credsPath()) ? credsPath() : null,
1179
+ credential_storage: credentialStore().backend,
1180
+ verified_identity: savedIdentity?.identity?.values ?? null, identity_checked_at: savedIdentity?.identity?.checkedAt ?? null,
1181
+ profile: PROFILE.name, profile_source: PROFILE.source,
1078
1182
  auth_env_vars: agentContext().authEnvVars,
1079
1183
  base_url: resolveBaseUrl(parsed.flags) ?? null,
1080
1184
  next_steps: authenticated ? [] : [
@@ -1085,11 +1189,14 @@ async function cmdAuth(parsed) {
1085
1189
  if (authenticated && parsed.flags.get("live") === true && WHOAMI) {
1086
1190
  const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
1087
1191
  if (op) {
1088
- const client = await makeClient(parsed.flags);
1192
+ const client = await makeClient(parsed.flags, op);
1089
1193
  const target = client[op.resource];
1090
1194
  const result = await target[op.method]();
1091
- if (result.ok)
1195
+ if (result.ok) {
1196
+ if (identityConfiguration() && savedIdentity?.identity)
1197
+ assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
1092
1198
  report.identity = result.data;
1199
+ }
1093
1200
  else {
1094
1201
  const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
1095
1202
  report.status = "action_required";
@@ -1110,10 +1217,16 @@ async function cmdDoctor(parsed) {
1110
1217
  const nodeMajor = Number(process.versions.node.split(".")[0]);
1111
1218
  checks.push({ name: "node", ok: nodeMajor >= 18, detail: process.version, ...(nodeMajor >= 18 ? {} : { fix: "Install Node 18 or newer." }) });
1112
1219
  checks.push({ name: "cli", ok: true, detail: BIN + " " + VERSION + " (" + PKG_NAME + ")" });
1113
- const stored = readCreds();
1114
- const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
1115
- const hasCred = Boolean(envScalar) || Boolean(stored && (stored.scalars || stored.basic || stored.oauth));
1116
- checks.push({ name: "credentials", ok: hasCred, detail: envScalar ? "env:" + envScalar.env : hasCred ? credsPath() : "none", ...(hasCred ? {} : { fix: AUTH_SCALARS[0] ? "Set " + AUTH_SCALARS[0].env + " or run '" + BIN + " login'." : "Run '" + BIN + " login'." }) });
1220
+ let source = null;
1221
+ let storageProblem;
1222
+ try {
1223
+ source = credentialSource(parsed.flags);
1224
+ }
1225
+ catch (error) {
1226
+ storageProblem = error.message;
1227
+ }
1228
+ const hasCred = source !== null;
1229
+ checks.push({ name: "credentials", ok: hasCred, detail: storageProblem ?? (source === "login" ? credentialStore().backend : source ?? "none"), ...(hasCred ? {} : { fix: storageProblem ?? (AUTH_SCALARS[0] ? "Set " + AUTH_SCALARS[0].env + " or run '" + BIN + " login'." : "Run '" + BIN + " login'.") }) });
1117
1230
  const baseUrl = resolveBaseUrl(parsed.flags);
1118
1231
  if (baseUrl) {
1119
1232
  try {
@@ -1131,7 +1244,7 @@ async function cmdDoctor(parsed) {
1131
1244
  const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
1132
1245
  if (op) {
1133
1246
  try {
1134
- const client = await makeClient(parsed.flags);
1247
+ const client = await makeClient(parsed.flags, op);
1135
1248
  const target = client[op.resource];
1136
1249
  const result = await target[op.method]();
1137
1250
  checks.push(result.ok ? { name: "identity", ok: true, detail: op.command.join(" ") + " ok" } : { name: "identity", ok: false, detail: classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT }).message, fix: "The credential was rejected; run '" + BIN + " login' with a current one." });
@@ -1184,27 +1297,39 @@ async function cmdInit(parsed) {
1184
1297
  const cwd = process.cwd();
1185
1298
  const harness = detectHarness();
1186
1299
  // 1. credential
1187
- const stored = readCreds() ?? {};
1188
1300
  const first = AUTH_SCALARS[0];
1301
+ const named = flagCredentials(parsed.flags), envNamed = environmentCredentials();
1302
+ const stored = Object.keys(named).length || Object.keys(envNamed).length || first && process.env[first.env] ? {} : readCreds() ?? {};
1189
1303
  const given = (typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") : undefined)
1190
1304
  ?? (first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) : undefined);
1191
- if (given && first) {
1192
- stored.scalars = { ...stored.scalars, [first.option]: given };
1193
- writeCreds(stored);
1305
+ if (Object.keys(named).length) {
1306
+ await saveLoginCredentials({ named }, parsed.flags);
1307
+ report.credential = { status: "stored", path: credsPath() };
1308
+ }
1309
+ else if (given && first) {
1310
+ await saveLoginCredentials({ scalars: { [first.option]: given } }, parsed.flags);
1194
1311
  report.credential = { status: "stored", path: credsPath() };
1195
1312
  }
1196
1313
  else if (first && process.env[first.env]) {
1197
1314
  report.credential = { status: "env", variable: first.env };
1198
1315
  }
1199
- else if (stored.scalars || stored.basic || stored.oauth) {
1316
+ else if (Object.keys(envNamed).length) {
1317
+ report.credential = { status: "env", variable: "TYPESHIP_CREDENTIALS" };
1318
+ }
1319
+ else if (stored.scalars || stored.basic || stored.oauth || stored.named) {
1200
1320
  report.credential = { status: "stored", path: credsPath() };
1201
1321
  }
1322
+ else if (first && HAS_OAUTH_LOGIN && OAUTH_LOGIN_METHOD === "browser" && (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID) && !explicitNonInteractive(parsed)) {
1323
+ await acquireOAuthBrowserSession(parsed, (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID));
1324
+ report.credential = { status: "stored", method: "oauth_browser", path: credsPath() };
1325
+ }
1202
1326
  else if (first && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
1203
1327
  // Nothing anywhere: approve a credential in the browser, as `login`
1204
1328
  // would, then carry on. Under an agent the URL is printed for the person
1205
1329
  // and polled; only the explicit non-interactive switch skips this.
1330
+ await credentialStore().prepare();
1206
1331
  const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
1207
- storeMinted(stored, minted);
1332
+ await storeMinted(minted, parsed.flags);
1208
1333
  report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
1209
1334
  }
1210
1335
  else {
@@ -1315,7 +1440,7 @@ async function cmdUpgrade(parsed) {
1315
1440
  fail(1, "This run came through npx, which fetches the latest version each time; there is nothing to upgrade in place.");
1316
1441
  if (manager !== "npm") {
1317
1442
  const command = manager === "pnpm" ? "pnpm add -g " + PKG_NAME + "@" + latest : manager === "bun" ? "bun add -g " + PKG_NAME + "@" + latest : "yarn global add " + PKG_NAME + "@" + latest;
1318
- failWith({ status: "action_required", code: "COMMAND_FAILED", message: PKG_NAME + " was installed with " + manager + ", so npm can't upgrade it in place.", nextSteps: ["Run: " + command] });
1443
+ failWith({ status: "action_required", code: "CALL_FAILED", message: PKG_NAME + " was installed with " + manager + ", so npm can't upgrade it in place.", nextSteps: ["Run: " + command] });
1319
1444
  }
1320
1445
  process.stderr.write("Upgrading " + PKG_NAME + " " + VERSION + " -> " + latest + "\n");
1321
1446
  // Windows resolves npm to npm.cmd, which only a shell can start.
@@ -1329,7 +1454,7 @@ async function cmdUpgrade(parsed) {
1329
1454
  // ---------------------------------------------------------------------------
1330
1455
  // completion — shell completion scripts built from the op table
1331
1456
  // ---------------------------------------------------------------------------
1332
- const TOP_WORDS = ["login", "logout", "whoami", "config", "mcp", "upgrade", "docs", "webhooks", "feedback", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
1457
+ const TOP_WORDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
1333
1458
  /** Every flag an API command accepts, with the values a flag completes to (enums, on|off|auto, agent|human). */
1334
1459
  function completionFlagsFor(op) {
1335
1460
  const flags = [...op.params.filter((p) => p.kind !== "path").map((p) => "--" + p.flag), ...COMPLETION_GLOBAL_FLAGS];
@@ -1343,13 +1468,12 @@ function completionFlagsFor(op) {
1343
1468
  }
1344
1469
  return { flags, values };
1345
1470
  }
1346
- const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--base-url", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
1471
+ 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)];
1347
1472
  const BUILTIN_WORDS = {
1348
1473
  config: ["list", "get", "set", "unset", "path"],
1349
1474
  completion: ["bash", "zsh", "fish"],
1350
1475
  auth: ["check"],
1351
1476
  mcp: ["install"],
1352
- webhooks: ["listen", "fake"],
1353
1477
  };
1354
1478
  /** bash (and zsh via bashcompinit): resources, commands, flags, and the values a flag takes. */
1355
1479
  function completionScript() {
@@ -1446,16 +1570,18 @@ async function cmdCompletion(parsed) {
1446
1570
  function docsSiteUrl() {
1447
1571
  return readConfig().docsUrl ?? DOCS_URL_DEFAULT;
1448
1572
  }
1573
+ function docsIndexUrl() {
1574
+ const configured = readConfig().docsUrl;
1575
+ return configured
1576
+ ? resolveDocsContentUrl(configured, null, "llms.txt")
1577
+ : DOCS_INDEX_URL_DEFAULT ?? resolveDocsContentUrl(DOCS_URL_DEFAULT, null, "llms.txt");
1578
+ }
1449
1579
  /** Fetch llms.txt / llms-full.txt / a prose page from the docs site,
1450
1580
  * with a 1h cache in the config dir. Explicit command = explicit fetch;
1451
1581
  * nothing here runs unless the user asked for docs. */
1452
1582
  async function fetchDocs(pathOrFile) {
1453
1583
  const base = docsSiteUrl();
1454
- const url = /^https?:\/\//.test(pathOrFile)
1455
- ? pathOrFile
1456
- : base === null
1457
- ? null
1458
- : base.replace(/\/+$/, "") + "/" + pathOrFile.replace(/^\/+/, "");
1584
+ const url = resolveDocsContentUrl(base, docsIndexUrl(), pathOrFile);
1459
1585
  if (url === null)
1460
1586
  return null;
1461
1587
  const cacheFile = join(configDir(), "docs-cache", url.replace(/[^a-zA-Z0-9.]+/g, "_").slice(-120));
@@ -1466,13 +1592,9 @@ async function fetchDocs(pathOrFile) {
1466
1592
  }
1467
1593
  catch { /* not cached */ }
1468
1594
  try {
1469
- const response = await fetch(url, {
1470
- headers: { Accept: "text/markdown, text/plain, */*" },
1471
- signal: AbortSignal.timeout(10_000),
1472
- });
1473
- if (!response.ok)
1595
+ const text = await fetchDocsText(url);
1596
+ if (text === null)
1474
1597
  return null;
1475
- const text = await response.text();
1476
1598
  mkdirSync(join(configDir(), "docs-cache"), { recursive: true, mode: 0o700 });
1477
1599
  writeFileSync(cacheFile, text);
1478
1600
  return text;
@@ -1618,31 +1740,7 @@ async function cmdDocs(parsed) {
1618
1740
  .filter((match) => match.score > 0)
1619
1741
  .sort((a, b) => b.score - a.score || a.op.command.join(" ").localeCompare(b.op.command.join(" ")))
1620
1742
  .map((match) => match.op);
1621
- const prose = await fetchDocs("llms-full.txt");
1622
- const proseMatches = [];
1623
- if (prose !== null) {
1624
- let heading = "";
1625
- const terms = searchTerms(term);
1626
- for (const line of prose.split("\n")) {
1627
- if (/^#{1,3} /.test(line))
1628
- heading = line.replace(/^#+ /, "").trim();
1629
- else {
1630
- const lowerHeading = heading.toLowerCase();
1631
- const lowerLine = line.toLowerCase();
1632
- const matched = terms.filter((word) => lowerHeading.includes(word) || lowerLine.includes(word));
1633
- if (matched.length > 0) {
1634
- const allTerms = matched.length === terms.length;
1635
- proseMatches.push({
1636
- heading,
1637
- excerpt: line.trim().slice(0, 160),
1638
- score: matched.length * 10 + (allTerms ? 50 : 0) + (lowerHeading.includes(term.toLowerCase()) || lowerLine.includes(term.toLowerCase()) ? 25 : 0),
1639
- });
1640
- }
1641
- }
1642
- }
1643
- proseMatches.sort((a, b) => b.score - a.score || a.heading.localeCompare(b.heading) || a.excerpt.localeCompare(b.excerpt));
1644
- }
1645
- const docsStatus = docsSiteUrl() === null ? "not_configured" : prose === null ? "unavailable" : "ok";
1743
+ const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
1646
1744
  if (jsonOutput) {
1647
1745
  out({
1648
1746
  schema_version: "1",
@@ -1654,7 +1752,7 @@ async function cmdDocs(parsed) {
1654
1752
  ...(op.summary ? { summary: op.summary } : {}),
1655
1753
  details_command: BIN + " docs " + op.command.join(" ") + " --json",
1656
1754
  })),
1657
- guides: proseMatches.slice(0, 15).map(({ heading, excerpt }) => ({ heading, excerpt })),
1755
+ guides: proseMatches.slice(0, 15).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
1658
1756
  totals: { reference: refMatches.length, guides: proseMatches.length },
1659
1757
  guides_status: docsStatus,
1660
1758
  ...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
@@ -1671,7 +1769,7 @@ async function cmdDocs(parsed) {
1671
1769
  if (proseMatches.length > 0) {
1672
1770
  lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
1673
1771
  for (const match of proseMatches.slice(0, 15))
1674
- lines.push(" " + padPaint("cyan", match.heading.slice(0, 32), 34) + match.excerpt.slice(0, 100));
1772
+ lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
1675
1773
  }
1676
1774
  else if (docsStatus === "not_configured") {
1677
1775
  lines.push(...(lines.length > 0 ? [""] : []), "(no docs site configured for guide search: '" + BIN + " config set docs-url <url>')");
@@ -1691,10 +1789,7 @@ async function cmdDocs(parsed) {
1691
1789
  let target = page;
1692
1790
  if (!/^https?:\/\//.test(target)) {
1693
1791
  const index = await fetchDocs("llms.txt");
1694
- const linked = index?.match(/\((https?:[^)]+)\)/g)?.map((m) => m.slice(1, -1)) ?? [];
1695
- const hit = linked.find((u) => u.toLowerCase().includes(target.toLowerCase()));
1696
- if (hit !== undefined)
1697
- target = hit;
1792
+ target = docsReadTarget(index, docsSiteUrl(), docsIndexUrl(), target);
1698
1793
  }
1699
1794
  const text = await fetchDocs(target);
1700
1795
  if (text === null) {
@@ -1708,6 +1803,9 @@ async function cmdDocs(parsed) {
1708
1803
  if (sub !== undefined) {
1709
1804
  const op = findOp(sub, parsed.positionals[2] ?? "");
1710
1805
  if (!op) {
1806
+ const omitted = omittedCommand(sub, parsed.positionals[2]);
1807
+ if (omitted)
1808
+ failOmitted(omitted);
1711
1809
  const list = OPS.filter((o) => o.command[0] === sub);
1712
1810
  if (list.length === 0)
1713
1811
  fail(2, "Unknown docs topic: " + sub + ". Run '" + BIN + " docs' for the overview.");
@@ -1759,182 +1857,9 @@ async function cmdDocs(parsed) {
1759
1857
  process.stdout.write(lines.join("\n") + "\n");
1760
1858
  await flushExit(0);
1761
1859
  }
1762
- function eventTypeOf(body) {
1763
- try {
1764
- const parsed = JSON.parse(body);
1765
- for (const key of ["type", "event", "event_type", "eventType", "name"]) {
1766
- if (typeof parsed[key] === "string")
1767
- return parsed[key];
1768
- }
1769
- }
1770
- catch { /* not JSON */ }
1771
- return null;
1772
- }
1773
- async function cmdWebhooksFake(parsed) {
1774
- void parsed;
1775
- fail(2, "This API's spec declares no webhooks, so there is nothing to fake.");
1776
- }
1777
- async function cmdWebhooks(parsed) {
1778
- const sub = parsed.positionals[1];
1779
- if (parsed.help || sub === undefined) {
1780
- const lines = [
1781
- BIN + " webhooks — work with this API's webhook events",
1782
- "",
1783
- " " + BIN + " webhooks listen --forward-to localhost:3000/webhooks",
1784
- " " + BIN + " webhooks listen --forward-to localhost:3000/hooks --events account.created,account.updated",
1785
- " " + BIN + " webhooks fake [<event>] [--key whsec_...] [--forward-to <url>]",
1786
- "",
1787
- "listen mints a private relay URL to point the API's webhook settings",
1788
- "at; events replay locally with their original headers, so signature",
1789
- "verification keeps working. No tunnels, no exposed ports.",
1790
- "fake sends (or prints) a signed sample event for local handler testing.",
1791
- ];
1792
- process.stdout.write(lines.join("\n") + "\n");
1793
- await flushExit(parsed.help ? 0 : 2);
1794
- }
1795
- if (sub === "fake") {
1796
- await cmdWebhooksFake(parsed);
1797
- }
1798
- if (sub !== "listen")
1799
- fail(2, "Unknown webhooks command: " + String(sub) + ". Try '" + BIN + " webhooks listen' or '" + BIN + " webhooks fake'.");
1800
- if (!RELAY) {
1801
- fail(2, "The webhook relay isn't enabled for this package. The API provider can enable it in their typeship console; the next regeneration bakes it in.");
1802
- }
1803
- const forwardRaw = parsed.flags.get("forward-to");
1804
- if (typeof forwardRaw !== "string") {
1805
- fail(2, "webhooks listen requires --forward-to <url>, e.g. --forward-to localhost:3000/webhooks");
1806
- }
1807
- const forwardTo = /^https?:\/\//.test(forwardRaw) ? forwardRaw : "http://" + forwardRaw;
1808
- const eventsRaw = parsed.flags.get("events");
1809
- const eventsFilter = typeof eventsRaw === "string"
1810
- ? eventsRaw.split(",").map((s) => s.trim()).filter(Boolean)
1811
- : null;
1812
- let minted = null;
1813
- try {
1814
- const response = await fetch(RELAY.mintUrl, {
1815
- method: "POST",
1816
- headers: { "Content-Type": "application/json", Accept: "application/json" },
1817
- body: JSON.stringify({ project: RELAY.project }),
1818
- signal: AbortSignal.timeout(15_000),
1819
- });
1820
- minted = await response.json().catch(() => null);
1821
- if (!response.ok || typeof minted?.ingest_url !== "string" || typeof minted.poll_url !== "string") {
1822
- const detail = minted?.errors?.[0]?.message ?? "HTTP " + response.status;
1823
- fail(1, "Couldn't start a relay session: " + detail);
1824
- }
1825
- }
1826
- catch (e) {
1827
- if (e instanceof ExitPending)
1828
- throw e;
1829
- fail(1, "Couldn't reach the relay: " + e.message);
1830
- }
1831
- process.stderr.write(paintErr("green", "Ready!") + " Forwarding relayed events to " + forwardTo + "\n");
1832
- process.stderr.write("Point this API's webhook endpoint at: " + paintErr("cyan", minted.ingest_url) + " (^C to quit)\n");
1833
- let cursor = 0;
1834
- let pollFailures = 0;
1835
- for (;;) {
1836
- let payload;
1837
- try {
1838
- const response = await fetch(minted.poll_url + "?after=" + cursor + "&wait=1", {
1839
- headers: { Accept: "application/json" },
1840
- signal: AbortSignal.timeout(30_000),
1841
- });
1842
- if (response.status === 404)
1843
- fail(1, "The relay session expired. Run '" + BIN + " webhooks listen' again.");
1844
- if (!response.ok)
1845
- throw new Error("HTTP " + response.status);
1846
- payload = await response.json();
1847
- pollFailures = 0;
1848
- }
1849
- catch (e) {
1850
- if (e instanceof ExitPending)
1851
- throw e;
1852
- pollFailures++;
1853
- if (pollFailures > 5)
1854
- fail(1, "Lost the relay: " + e.message);
1855
- await new Promise((resolve) => setTimeout(resolve, 2000 * pollFailures));
1856
- continue;
1857
- }
1858
- for (const event of payload.events) {
1859
- cursor = event.seq;
1860
- const type = eventTypeOf(event.body);
1861
- if (eventsFilter !== null && (type === null || !eventsFilter.includes(type)))
1862
- continue;
1863
- const started = Date.now();
1864
- try {
1865
- const forwarded = await fetch(forwardTo, {
1866
- method: event.method,
1867
- headers: event.headers,
1868
- body: event.method === "GET" ? undefined : event.body,
1869
- signal: AbortSignal.timeout(30_000),
1870
- });
1871
- process.stderr.write((forwarded.ok ? paintErr("green", String(forwarded.status)) : paintErr("yellow", String(forwarded.status))) +
1872
- " " + (type ?? event.method) + " [" + event.seq + "] (" + (Date.now() - started) + "ms)\n");
1873
- }
1874
- catch (e) {
1875
- process.stderr.write(paintErr("yellow", "unreachable") + " " + (type ?? event.method) + " [" + event.seq + "]: " + e.message + "\n");
1876
- }
1877
- }
1878
- if (payload.next > cursor)
1879
- cursor = payload.next;
1880
- }
1881
- }
1882
- async function cmdFeedback(parsed) {
1883
- if (parsed.help) {
1884
- process.stdout.write(BIN + " feedback — open the API provider's issue tracker with environment details prefilled\n");
1885
- await flushExit(0);
1886
- }
1887
- if (!SUPPORT_URL) {
1888
- fail(2, "No support URL is configured for this CLI. The API provider can set one in their typeship console.");
1889
- }
1890
- let target = SUPPORT_URL;
1891
- if (/github\.com\/[^/]+\/[^/]+\/issues\/new/.test(target)) {
1892
- const bodyLines = [
1893
- "<!-- describe the problem or request above the line -->",
1894
- "",
1895
- "---",
1896
- "- cli: " + BIN + " " + VERSION + " (api " + API_VERSION + ")",
1897
- "- node: " + process.version,
1898
- "- platform: " + process.platform + "/" + process.arch,
1899
- ];
1900
- const sep = target.includes("?") ? "&" : "?";
1901
- target += sep + "title=" + encodeURIComponent("[" + BIN + "] ") + "&body=" + encodeURIComponent(bodyLines.join("\n"));
1902
- }
1903
- if (!nonInteractive(parsed))
1904
- process.stderr.write("Opening " + paintErr("cyan", SUPPORT_URL) + "\n");
1905
- const opened = openInBrowser(target, parsed);
1906
- if (opened.opened)
1907
- out({ ok: true, opened: SUPPORT_URL });
1908
- await flushExit(0);
1909
- }
1910
- /** Opt-in (console setting) once-a-day upgrade hint. Off by default:
1911
- * generated code phones nobody unless the project owner chose this. The
1912
- * notice prints the previous run's cached answer; at most once a day it
1913
- * refreshes the cache first, with a 1.5s cap so a slow registry can't
1914
- * hold a command hostage. */
1915
- async function maybeUpdateNotice(commandWord, quiet) {
1916
- if (!UPDATE_NOTICE || quiet)
1917
- return;
1918
- if (commandWord === undefined || ["upgrade", "version", "help", "login", "logout"].includes(commandWord))
1919
- return;
1920
- const cacheFile = join(configDir(), "update-check.json");
1921
- let cache = {};
1922
- try {
1923
- cache = JSON.parse(readFileSync(cacheFile, "utf8"));
1924
- }
1925
- catch { /* no cache yet */ }
1926
- if ((cache.checkedAt ?? 0) < Date.now() - 24 * 60 * 60 * 1000) {
1927
- const latest = await latestVersion(1500);
1928
- if (latest !== null) {
1929
- cache = { checkedAt: Date.now(), latest };
1930
- mkdirSync(configDir(), { recursive: true, mode: 0o700 });
1931
- writeFileSync(cacheFile, JSON.stringify(cache) + "\n");
1932
- }
1933
- }
1934
- if (cache.latest !== undefined && semverLess(VERSION, cache.latest)) {
1935
- process.stderr.write(paintErr("yellow", "A newer " + BIN + " is available: " + VERSION + " -> " + cache.latest + ". Run '" + BIN + " upgrade'.") + "\n");
1936
- }
1937
- }
1860
+ // ---------------------------------------------------------------------------
1861
+ // webhooks listen — forward relayed events to a local handler
1862
+ // ---------------------------------------------------------------------------
1938
1863
  /** The command that fetches the next page: same positionals, the next page's query flags. */
1939
1864
  function nextCommandFor(op, pathValues, next) {
1940
1865
  const parts = [BIN, op.command[0], op.command[1], ...pathValues.map(shellQuote)];
@@ -2083,7 +2008,7 @@ function printRoot(stream = process.stdout) {
2083
2008
  }
2084
2009
  const width = termWidth();
2085
2010
  const lines = [];
2086
- lines.push(paintOut("bold", BIN) + ": " + "typeship" + " (v" + "0.6.0" + ")");
2011
+ lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.9.1");
2087
2012
  lines.push("");
2088
2013
  lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
2089
2014
  lines.push("");
@@ -2101,12 +2026,18 @@ function printRoot(stream = process.stdout) {
2101
2026
  for (const more of wrapped.slice(1))
2102
2027
  lines.push(" ".repeat(2 + col) + more);
2103
2028
  }
2029
+ if (EXCLUDED_OPS > 0) {
2030
+ lines.push("");
2031
+ lines.push(...labeled(paintOut("yellow", "Plan limit:") + " ", "generated " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations", width, 14));
2032
+ lines.push(...labeled("Omitted: ", OMITTED_OPS.map((op) => op.command.join(" ") + " (" + op.httpMethod + " " + op.path + ")").join(", "), width, 14));
2033
+ lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
2034
+ }
2104
2035
  lines.push("");
2105
- const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check bodies)" +
2036
+ 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 bodies)" +
2106
2037
  (AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
2107
2038
  lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
2108
2039
  lines.push(...labeled("Credential env vars: ", [
2109
- ...AUTH_SCALARS.map((a) => a.env),
2040
+ "TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
2110
2041
  ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
2111
2042
  ].join(", ") || "none", width, 21));
2112
2043
  lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
@@ -2118,9 +2049,9 @@ function printRoot(stream = process.stdout) {
2118
2049
  lines.push("Webhooks: " + BIN + " webhooks listen --forward-to <url> (local event forwarding)");
2119
2050
  if (SUPPORT_URL)
2120
2051
  lines.push("Feedback: " + BIN + " feedback (opens the provider's issue tracker)");
2121
- if (EXCLUDED_OPS > 0 && HAS_MCP) {
2052
+ if (MCP_EXCLUDED_OPS > 0 && HAS_MCP) {
2122
2053
  lines.push("");
2123
- lines.push("Note: " + EXCLUDED_OPS + " operation(s) with uploads or event streams are CLI/SDK-only, not MCP tools.");
2054
+ lines.push("Note: " + MCP_EXCLUDED_OPS + " generated operation(s) with uploads or event streams are CLI/SDK-only, not MCP tools.");
2124
2055
  }
2125
2056
  lines.push("");
2126
2057
  lines.push("Run '" + BIN + " <resource>' for a resource's commands, '" + BIN + " <resource> <command> --help' for flags.");
@@ -2151,6 +2082,8 @@ function printResource(resource, stream = process.stdout) {
2151
2082
  /** Flags the CLI itself adds to an API command, as [flag, description] rows. */
2152
2083
  function commandExtras(op) {
2153
2084
  const extras = [];
2085
+ if (op.auth !== "none" && Object.keys(NAMED_SCHEMES).length)
2086
+ extras.push(["--credentials @<file>|-", "named credentials as JSON; use - for stdin"]);
2154
2087
  if (op.hasBody && op.bodyKind === "binary")
2155
2088
  extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
2156
2089
  else if (op.hasBody)
@@ -2202,7 +2135,7 @@ function exampleLine(op) {
2202
2135
  }
2203
2136
  /** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
2204
2137
  function authNote(op) {
2205
- const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || OAUTH_TOKEN_URL !== null;
2138
+ const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
2206
2139
  return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
2207
2140
  }
2208
2141
  function printOp(op) {
@@ -2379,38 +2312,89 @@ function coerce(spec, raw, repeated) {
2379
2312
  }
2380
2313
  /** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
2381
2314
  let LAST_CLIENT_HAD_CREDENTIAL = false;
2315
+ function validateCredentialsInput(flags) {
2316
+ const input = flags.get("credentials");
2317
+ if (BASIC && flags.has("username") !== flags.has("password"))
2318
+ fail(2, "Supply --username and --password together, or use a complete named Basic credential.");
2319
+ if (input === undefined)
2320
+ return;
2321
+ if (typeof input !== "string" || (input !== "-" && (!input.startsWith("@") || input.length < 2)))
2322
+ fail(2, "--credentials expects @<JSON-file> or - to read JSON from stdin.");
2323
+ if (input === "-" && (flags.get("data") === "-" || flags.get("file") === "-" || flags.get("with-token") === true))
2324
+ fail(2, "Stdin cannot supply both named credentials and another input. Use a credential file instead.");
2325
+ }
2326
+ let namedFlagCache;
2327
+ function flagCredentials(flags) {
2328
+ if (namedFlagCache)
2329
+ return namedFlagCache;
2330
+ validateCredentialsInput(flags);
2331
+ const input = flags.get("credentials");
2332
+ if (input === undefined)
2333
+ return {};
2334
+ try {
2335
+ const named = readNamedCredentialsFile(input, NAMED_SCHEMES);
2336
+ if (!Object.keys(named).length)
2337
+ fail(2, "The named credential file must contain at least one security scheme.");
2338
+ return namedFlagCache = named;
2339
+ }
2340
+ catch (error) {
2341
+ fail(2, error.message);
2342
+ }
2343
+ }
2344
+ function environmentCredentials() {
2345
+ if (BASIC && (process.env[BASIC.envUser] !== undefined) !== (process.env[BASIC.envPass] !== undefined))
2346
+ fail(2, "Supply both Basic-auth environment variables together, or use a complete named Basic credential.");
2347
+ const value = process.env["TYPESHIP_CREDENTIALS"];
2348
+ return value === undefined ? {} : parseNamedCredentials(value, NAMED_SCHEMES);
2349
+ }
2382
2350
  /** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
2383
2351
  function credentialSource(flags) {
2352
+ if (Object.keys(flagCredentials(flags)).length)
2353
+ return "flags";
2384
2354
  if (AUTH_SCALARS.some((a) => typeof flags.get(a.flag) === "string"))
2385
2355
  return "flags";
2386
2356
  if (BASIC && typeof flags.get("username") === "string" && typeof flags.get("password") === "string")
2387
2357
  return "flags";
2358
+ if (Object.keys(environmentCredentials()).length)
2359
+ return "env:TYPESHIP_CREDENTIALS";
2388
2360
  const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
2389
2361
  if (envScalar)
2390
2362
  return "env:" + envScalar.env;
2391
2363
  if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined)
2392
2364
  return "env:" + BASIC.envUser;
2393
2365
  const stored = readCreds();
2394
- if (stored && (stored.scalars || stored.basic || stored.oauth))
2366
+ if (stored && (stored.scalars || stored.basic || stored.oauth || stored.named))
2395
2367
  return "login";
2396
2368
  return null;
2397
2369
  }
2398
2370
  /** Base URL resolution: --base-url > env > config base-url > config environment > spec default. */
2399
- function resolveBaseUrl(flags) {
2400
- const config = readConfig();
2371
+ function resolveBaseUrl(flags, config = readConfig()) {
2401
2372
  return (typeof flags.get("base-url") === "string" ? flags.get("base-url") : undefined)
2402
2373
  ?? process.env["TYPESHIP_BASE_URL"]
2403
2374
  ?? config.baseUrl
2404
2375
  ?? (config.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
2405
2376
  ?? DEFAULT_BASE_URL ?? undefined;
2406
2377
  }
2407
- async function makeClient(flags) {
2408
- const stored = readCreds();
2409
- const options = {};
2410
- const baseUrl = resolveBaseUrl(flags);
2378
+ async function makeClient(flags, op, candidate, forIdentity = false) {
2379
+ const flagNamed = flagCredentials(flags), envNamed = environmentCredentials();
2380
+ const explicitOptions = new Set(AUTH_SCALARS.filter((a) => typeof flags.get(a.flag) === "string" || process.env[a.env] !== undefined).map((a) => a.option));
2381
+ if (BASIC && (typeof flags.get("username") === "string" || process.env[BASIC.envUser] !== undefined) && (typeof flags.get("password") === "string" || process.env[BASIC.envPass] !== undefined))
2382
+ explicitOptions.add("basicAuth");
2383
+ namedCredentialAvailability(NAMED_SCHEMES, explicitOptions, { ...envNamed, ...flagNamed });
2384
+ const explicitCredentials = op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => explicitOptions.has(option)));
2385
+ // A complete explicit alternative for this endpoint does not unlock or mix
2386
+ // in saved credentials for another API, account, or authentication method.
2387
+ const stored = candidate ?? (op.auth === "none" || explicitCredentials ? null : readCreds());
2388
+ const config = readConfig();
2389
+ const baseUrl = resolveBaseUrl(flags, config);
2411
2390
  if (baseUrl === undefined)
2412
2391
  fail(2, "No base URL. Pass --base-url, set TYPESHIP_BASE_URL, or run '" + BIN + " config set base-url <url>'.");
2413
- options.baseUrl = baseUrl;
2392
+ if (stored) {
2393
+ assertCredentialDestination(stored, { apiBaseUrl: baseUrl, environment: config.environment, profile: PROFILE.name });
2394
+ if (!forIdentity)
2395
+ assertStoredIdentity(stored, identityConfiguration());
2396
+ }
2397
+ const options = { baseUrl };
2414
2398
  for (const a of AUTH_SCALARS) {
2415
2399
  const v = (typeof flags.get(a.flag) === "string" ? flags.get(a.flag) : undefined)
2416
2400
  ?? process.env[a.env] ?? stored?.scalars?.[a.option];
@@ -2425,10 +2409,28 @@ async function makeClient(flags) {
2425
2409
  if (username !== undefined && password !== undefined)
2426
2410
  options.basicAuth = { username, password };
2427
2411
  }
2428
- if (options.bearerToken === undefined && stored?.oauth) {
2429
- const token = await refreshedOauthToken(stored);
2430
- if (token !== undefined)
2431
- options.bearerToken = token;
2412
+ const envOptions = {}, flagOptions = {};
2413
+ for (const a of AUTH_SCALARS) {
2414
+ if (process.env[a.env] !== undefined)
2415
+ envOptions[a.option] = process.env[a.env];
2416
+ if (typeof flags.get(a.flag) === "string")
2417
+ flagOptions[a.option] = flags.get(a.flag);
2418
+ }
2419
+ if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined)
2420
+ envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
2421
+ if (BASIC && typeof flags.get("username") === "string" && typeof flags.get("password") === "string")
2422
+ flagOptions.basicAuth = { username: flags.get("username"), password: flags.get("password") };
2423
+ options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
2424
+ { named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
2425
+ { named: envNamed, options: envOptions }, { named: flagNamed, options: flagOptions },
2426
+ ...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
2427
+ ]);
2428
+ if (stored?.oauth && (forIdentity || options.bearerToken === undefined)) {
2429
+ const sessionId = stored.oauth.sessionId;
2430
+ options.bearerToken = forIdentity ? stored.oauth.accessToken : () => oauthSessionToken(credentialStore(), {
2431
+ ...sessionConfiguration(baseUrl, config),
2432
+ ...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken) => verifyClientIdentity((values) => new TypeshipClient(values), { ...options, bearerToken: accessToken, credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ named: options.credentials }, { options: { bearerToken: accessToken } }]) }, WHOAMI, IDENTITY_POLICY) } : {}),
2433
+ }, sessionId, OAUTH_TOKEN_PARAMS);
2432
2434
  }
2433
2435
  if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
2434
2436
  options.debug = (event) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
@@ -2441,7 +2443,7 @@ async function makeClient(flags) {
2441
2443
  if (value !== undefined)
2442
2444
  options[g.option] = value;
2443
2445
  }
2444
- LAST_CLIENT_HAD_CREDENTIAL = AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
2446
+ LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
2445
2447
  // Who is calling: the CLI, under which agent harness, and whether an
2446
2448
  // agent is driving. "agent" means a harness was detected or the caller
2447
2449
  // said so (--mode agent / env); a bare non-TTY run (CI, a pipeline) is
@@ -2455,6 +2457,11 @@ async function makeClient(flags) {
2455
2457
  ...options.defaultHeaders,
2456
2458
  "User-Agent": PKG_NAME + "-cli/" + VERSION + " (typeship" + (harness ? "; harness=" + harness : "") + caller + ")",
2457
2459
  };
2460
+ if (forIdentity) {
2461
+ options.fetch = identityFetch(baseUrl);
2462
+ options.maxRetries = 0;
2463
+ options.timeoutMs = 10_000;
2464
+ }
2458
2465
  return new TypeshipClient(options);
2459
2466
  }
2460
2467
  function editDistance(a, b) {
@@ -2483,10 +2490,31 @@ function didYouMean(input, candidates) {
2483
2490
  }
2484
2491
  return best;
2485
2492
  }
2486
- const BUILTIN_COMMANDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "feedback", "completion", "webhooks", "help", "version", "init", "agent-guide", "auth", "doctor"];
2493
+ function omittedCommand(resource, method) {
2494
+ return OMITTED_OPS.find((op) => op.command[0] === resource &&
2495
+ (method === undefined || op.command[1] === method || op.commandAlias === method));
2496
+ }
2497
+ function failOmitted(op) {
2498
+ return failWith({
2499
+ status: "action_required",
2500
+ code: "PLAN_LIMIT",
2501
+ message: "The command '" + BIN + " " + op.command.join(" ") + "' exists in the API Definition but was omitted from this generated package by its plan limit.",
2502
+ detail: { operation: op.tool, method: op.httpMethod, path: op.path, generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
2503
+ nextSteps: ["Upgrade at https://typeship.dev/pricing and regenerate the package without the operation cap.", "Do not invent or retry an omitted command against this generated package."],
2504
+ });
2505
+ }
2506
+ const BUILTIN_COMMANDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
2487
2507
  async function main() {
2488
2508
  const argv = process.argv.slice(2);
2489
2509
  const parsed = parseArgv(argv);
2510
+ if (!parsed.help)
2511
+ validateCredentialsInput(parsed.flags);
2512
+ const profileFlag = parsed.flags.get("profile");
2513
+ if (profileFlag !== undefined && typeof profileFlag !== "string")
2514
+ fail(2, "--profile requires a profile name.");
2515
+ PROFILE = resolveProfile(configRoot(), { flag: profileFlag, environment: process.env["TYPESHIP_PROFILE"], allowMissing: ["login", "config", "auth", "init", "help"].includes(parsed.positionals[0] ?? "") || parsed.help });
2516
+ if (!parsed.help && ["login", "init"].includes(parsed.positionals[0] ?? ""))
2517
+ expectedLoginIdentity(parsed.flags);
2490
2518
  if (parsed.positionals[0] === "help") {
2491
2519
  // help --json: the command surface as data (agents read this once).
2492
2520
  if (parsed.flags.get("json") === true || parsed.flags.get("format") === "json") {
@@ -2511,9 +2539,8 @@ async function main() {
2511
2539
  COLOR_ERR = colorEnabled(process.stderr, parsed);
2512
2540
  // Prose errors for a person: stderr is a terminal (or --mode human says
2513
2541
  // to behave as if), and nothing asked for JSON.
2514
- const forcedHuman = parsed.flags.get("mode") === "human" || parsed.flags.get("mode") === "interactive" || process.env["TYPESHIP_MODE"] === "human";
2542
+ const forcedHuman = parsed.flags.get("mode") === "human" || process.env["TYPESHIP_MODE"] === "human";
2515
2543
  HUMAN_ERRORS = (process.stderr.isTTY === true || forcedHuman) && !isAgentMode(parsed) && parsed.flags.get("format") !== "json" && parsed.flags.get("json") !== true;
2516
- await maybeUpdateNotice(resourceCmd, explicitNonInteractive(parsed));
2517
2544
  if (resourceCmd === "init") {
2518
2545
  await cmdInit(parsed);
2519
2546
  }
@@ -2529,12 +2556,6 @@ async function main() {
2529
2556
  if (resourceCmd === "upgrade") {
2530
2557
  await cmdUpgrade(parsed);
2531
2558
  }
2532
- if (resourceCmd === "webhooks") {
2533
- await cmdWebhooks(parsed);
2534
- }
2535
- if (resourceCmd === "feedback") {
2536
- await cmdFeedback(parsed);
2537
- }
2538
2559
  if (resourceCmd === "docs") {
2539
2560
  await cmdDocs(parsed);
2540
2561
  }
@@ -2552,10 +2573,10 @@ async function main() {
2552
2573
  }
2553
2574
  if (resourceCmd === "logout") {
2554
2575
  if (parsed.help) {
2555
- process.stdout.write(BIN + " logout — remove " + credsPath() + "\n");
2576
+ process.stdout.write(BIN + " logout — remove " + credsPath() + "\n --local remove the local session without unlocking OS storage or revoking tokens\n");
2556
2577
  await flushExit(0);
2557
2578
  }
2558
- await cmdLogout();
2579
+ await cmdLogout(parsed);
2559
2580
  }
2560
2581
  if (resourceCmd === "whoami") {
2561
2582
  if (parsed.help) {
@@ -2574,6 +2595,9 @@ async function main() {
2574
2595
  }
2575
2596
  const resourceExists = OPS.some((o) => o.command[0] === resourceCmd);
2576
2597
  if (!resourceExists) {
2598
+ const omitted = omittedCommand(resourceCmd, methodCmd);
2599
+ if (omitted)
2600
+ failOmitted(omitted);
2577
2601
  const suggestion = didYouMean(resourceCmd, [...new Set(OPS.map((o) => o.command[0])), ...BUILTIN_COMMANDS]);
2578
2602
  fail(2, "Unknown command: " + resourceCmd + "." + (suggestion ? " Did you mean '" + BIN + " " + suggestion + "'?" : ""), undefined, ["Run '" + BIN + " --help' for the commands."]);
2579
2603
  }
@@ -2583,6 +2607,9 @@ async function main() {
2583
2607
  }
2584
2608
  const op = findOp(resourceCmd, methodCmd);
2585
2609
  if (!op) {
2610
+ const omitted = omittedCommand(resourceCmd, methodCmd);
2611
+ if (omitted)
2612
+ failOmitted(omitted);
2586
2613
  const suggestion = didYouMean(methodCmd, OPS.filter((o) => o.command[0] === resourceCmd).map((o) => o.command[1]));
2587
2614
  fail(2, "Unknown command: " + resourceCmd + " " + methodCmd + "." + (suggestion ? " Did you mean '" + BIN + " " + resourceCmd + " " + suggestion + "'?" : ""), undefined, ["Run '" + BIN + " " + resourceCmd + "' for its commands."]);
2588
2615
  }
@@ -2622,7 +2649,7 @@ async function main() {
2622
2649
  // Mirrors opReservedFlags() in the generator: API parameters never use these
2623
2650
  // names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
2624
2651
  // check can be exact.
2625
- const RESERVED_FLAGS = new Set(["data", "all", "select", "base-url", "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)]);
2652
+ 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)]);
2626
2653
  for (const spec of op.params) {
2627
2654
  if (spec.kind === "path")
2628
2655
  continue;
@@ -2678,7 +2705,7 @@ async function main() {
2678
2705
  }
2679
2706
  // The spec says this operation needs a credential and none resolved:
2680
2707
  // say so now, locally, instead of sending a request to learn it.
2681
- if (op.auth === "required" && (AUTH_SCALARS.length > 0 || BASIC || OAUTH_TOKEN_URL) && credentialSource(parsed.flags) === null) {
2708
+ if (op.auth === "required" && (AUTH_SCALARS.length > 0 || BASIC || HAS_OAUTH_LOGIN) && credentialSource(parsed.flags) === null) {
2682
2709
  failWith({
2683
2710
  status: "action_required",
2684
2711
  code: "NO_AUTH",
@@ -2709,7 +2736,7 @@ async function main() {
2709
2736
  if (answer !== "y" && answer !== "yes")
2710
2737
  failWith({ status: "action_required", code: "CONFIRMATION_REQUIRED", message: "Cancelled.", nextSteps: ["Run again with --force to skip the prompt: " + rerun] });
2711
2738
  }
2712
- const client = await makeClient(parsed.flags);
2739
+ const client = await makeClient(parsed.flags, op);
2713
2740
  const selectValue = typeof parsed.flags.get("select") === "string" ? parsed.flags.get("select") : undefined;
2714
2741
  const args = buildArgs(op, values, dataBody, selectValue);
2715
2742
  const target = client[op.resource];
@@ -2758,6 +2785,7 @@ async function main() {
2758
2785
  items: project(page.items),
2759
2786
  hasMore: next !== null,
2760
2787
  ...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
2788
+ ...(page.response.requestId ? { request_id: page.response.requestId } : {}),
2761
2789
  });
2762
2790
  }
2763
2791
  else if (FIELDS !== null && collectionField !== null && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) && Array.isArray(result.data[collectionField])) {
@@ -2789,16 +2817,6 @@ function errorMessage(error) {
2789
2817
  }
2790
2818
  return base;
2791
2819
  }
2792
- function serializeError(error) {
2793
- if (error && typeof error === "object" && "violations" in error) {
2794
- return { violations: error.violations };
2795
- }
2796
- if (error && typeof error === "object" && "status" in error) {
2797
- const e = error;
2798
- return { status: e.status, body: e.body };
2799
- }
2800
- return undefined;
2801
- }
2802
2820
  main().catch((e) => {
2803
2821
  if (e instanceof ExitPending)
2804
2822
  return;