@cargo-ai/cli 1.0.45 → 1.0.46

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 (56) hide show
  1. package/README.md +50 -10
  2. package/build/api.d.ts +2 -1
  3. package/build/api.d.ts.map +1 -1
  4. package/build/api.js +5 -1
  5. package/build/commands/auth/auth0.d.ts +33 -0
  6. package/build/commands/auth/auth0.d.ts.map +1 -0
  7. package/build/commands/auth/auth0.js +90 -0
  8. package/build/commands/auth/email.d.ts +16 -0
  9. package/build/commands/auth/email.d.ts.map +1 -0
  10. package/build/commands/auth/email.js +93 -0
  11. package/build/commands/auth/index.d.ts +19 -1
  12. package/build/commands/auth/index.d.ts.map +1 -1
  13. package/build/commands/auth/index.js +121 -59
  14. package/build/commands/auth/loginChannel.d.ts +21 -0
  15. package/build/commands/auth/loginChannel.d.ts.map +1 -0
  16. package/build/commands/auth/loginChannel.js +42 -0
  17. package/build/commands/auth/oauth.d.ts +2 -6
  18. package/build/commands/auth/oauth.d.ts.map +1 -1
  19. package/build/commands/auth/oauth.js +54 -91
  20. package/build/commands/auth/passwordless.d.ts +22 -0
  21. package/build/commands/auth/passwordless.d.ts.map +1 -0
  22. package/build/commands/auth/passwordless.js +70 -0
  23. package/build/commands/auth/session.d.ts +21 -0
  24. package/build/commands/auth/session.d.ts.map +1 -0
  25. package/build/commands/auth/session.js +55 -0
  26. package/build/commands/auth/workspace.d.ts +9 -1
  27. package/build/commands/auth/workspace.d.ts.map +1 -1
  28. package/build/commands/auth/workspace.js +42 -69
  29. package/build/commands/billing/addPaymentMethod.d.ts +26 -0
  30. package/build/commands/billing/addPaymentMethod.d.ts.map +1 -0
  31. package/build/commands/billing/addPaymentMethod.js +57 -0
  32. package/build/commands/billing/subscription.d.ts.map +1 -1
  33. package/build/commands/billing/subscription.js +28 -1
  34. package/build/commands/doctor.js +3 -3
  35. package/build/commands/mcp.d.ts.map +1 -1
  36. package/build/commands/mcp.js +35 -8
  37. package/build/commands/runHandler.d.ts +6 -0
  38. package/build/commands/runHandler.d.ts.map +1 -1
  39. package/build/commands/runHandler.js +34 -4
  40. package/build/config.d.ts +22 -1
  41. package/build/config.d.ts.map +1 -1
  42. package/build/config.js +44 -3
  43. package/build/credentials.d.ts +1 -10
  44. package/build/credentials.d.ts.map +1 -1
  45. package/build/credentials.js +4 -34
  46. package/build/index.js +14 -7
  47. package/build/oauthConfig.d.ts +1 -6
  48. package/build/oauthConfig.d.ts.map +1 -1
  49. package/build/oauthConfig.js +9 -14
  50. package/build/utils/openBrowser.d.ts +7 -0
  51. package/build/utils/openBrowser.d.ts.map +1 -0
  52. package/build/utils/openBrowser.js +29 -0
  53. package/build/utils/prompt.d.ts +12 -0
  54. package/build/utils/prompt.d.ts.map +1 -0
  55. package/build/utils/prompt.js +38 -0
  56. package/package.json +4 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loginChannel.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/loginChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtB;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,qBAAqB,CAmD5E"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Picks the sign-in channel and rejects flag combinations whose extra options
3
+ * would otherwise be accepted and quietly ignored.
4
+ */
5
+ export function selectLoginChannel(opts) {
6
+ const wantsEmail = opts.email !== undefined;
7
+ const wantsOAuth = opts.oauth === true;
8
+ const wantsToken = opts.token !== undefined;
9
+ const selectedCount = [wantsEmail, wantsOAuth, wantsToken].filter((selected) => {
10
+ return selected === true;
11
+ }).length;
12
+ if (selectedCount > 1) {
13
+ return { error: "Pass only one of --email, --oauth or --token." };
14
+ }
15
+ if (selectedCount === 0) {
16
+ return {
17
+ error: "Must pass --email <email> (emailed code), --oauth (browser sign-in) or --token <token> (existing API token).",
18
+ };
19
+ }
20
+ if (opts.code !== undefined && wantsEmail === false) {
21
+ return { error: "--code only applies to --email <email>." };
22
+ }
23
+ if (opts.workspaceName !== undefined && opts.workspaceUuid !== undefined) {
24
+ return {
25
+ error: "Pass only one of --workspace-name and --workspace-uuid; they select different workspaces.",
26
+ };
27
+ }
28
+ // An API token already carries its workspace, so creating one for it cannot
29
+ // be honoured and must not look like it was.
30
+ if (wantsToken === true && opts.workspaceName !== undefined) {
31
+ return {
32
+ error: "--workspace-name does not apply to --token; an API token is already scoped to a workspace.",
33
+ };
34
+ }
35
+ if (opts.email !== undefined) {
36
+ return { channel: "email" };
37
+ }
38
+ if (opts.token !== undefined) {
39
+ return { channel: "token", token: opts.token };
40
+ }
41
+ return { channel: "oauth" };
42
+ }
@@ -1,7 +1,3 @@
1
- import { type OAuthConfigOverrides } from "../../oauthConfig.js";
2
- export type OAuthLoginResult = {
3
- accessToken: string;
4
- expiresIn: number;
5
- };
6
- export declare function runOAuthDeviceFlow(overrides: OAuthConfigOverrides): Promise<OAuthLoginResult>;
1
+ import { type IdentityTokens } from "./auth0.js";
2
+ export declare function runOAuthDeviceFlow(): Promise<IdentityTokens>;
7
3
  //# sourceMappingURL=oauth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/oauth.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AA8B9B,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,oBAAoB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CA0B3B"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/oauth.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,cAAc,EAIpB,MAAM,YAAY,CAAC;AAepB,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,CAAC,CAmBlE"}
@@ -1,13 +1,12 @@
1
- import { spawn } from "node:child_process";
2
- import { platform } from "node:process";
3
- import { getOAuthConfig, } from "../../oauthConfig.js";
4
- import { failWith, info } from "../runHandler.js";
1
+ import { getOAuthConfig } from "../../oauthConfig.js";
2
+ import { openBrowser } from "../../utils/openBrowser.js";
3
+ import { failWith, info, sleep } from "../runHandler.js";
4
+ import { describeError, errorCodeOf, parseTokenResponse, postToAuth0, readErrorBody, } from "./auth0.js";
5
5
  const DEFAULT_POLL_INTERVAL_SECONDS = 5;
6
6
  const SLOW_DOWN_BACKOFF_SECONDS = 5;
7
7
  const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
8
- export async function runOAuthDeviceFlow(overrides) {
9
- const oauthConfig = getOAuthConfig(overrides);
10
- const deviceCode = await requestDeviceCode(oauthConfig);
8
+ export async function runOAuthDeviceFlow() {
9
+ const deviceCode = await requestDeviceCode();
11
10
  info([
12
11
  "",
13
12
  "To complete sign-in, open this URL in a browser:",
@@ -19,27 +18,22 @@ export async function runOAuthDeviceFlow(overrides) {
19
18
  "",
20
19
  ].join("\n"));
21
20
  openBrowser(deviceCode.verification_uri_complete);
22
- const token = await pollForToken(oauthConfig, deviceCode);
23
- return {
24
- accessToken: token.access_token,
25
- expiresIn: token.expires_in,
26
- };
21
+ return await pollForToken(deviceCode);
27
22
  }
28
- async function requestDeviceCode(config) {
29
- const body = new URLSearchParams({
30
- client_id: config.clientId,
31
- scope: config.scope,
32
- audience: config.audience,
33
- });
34
- body.set("screen_hint", "signup");
35
- const response = await fetch(`https://${config.domain}/oauth/device/code`, {
36
- method: "POST",
37
- headers: { "content-type": "application/x-www-form-urlencoded" },
38
- body: body.toString(),
23
+ async function requestDeviceCode() {
24
+ const config = getOAuthConfig();
25
+ const response = await postToAuth0({
26
+ path: "/oauth/device/code",
27
+ body: new URLSearchParams({
28
+ client_id: config.clientId,
29
+ scope: config.scope,
30
+ audience: config.audience,
31
+ screen_hint: "signup",
32
+ }),
39
33
  });
40
34
  if (response.ok === false) {
41
- const text = await safeReadText(response);
42
- failWith(`Failed to start device authorization (HTTP ${String(response.status)}): ${text}`);
35
+ const body = await readErrorBody(response);
36
+ failWith(`Failed to start device authorization (HTTP ${String(response.status)}): ${describeError(body)}`, { extra: { oauthError: body } });
43
37
  }
44
38
  const json = (await response.json());
45
39
  if (typeof json.device_code !== "string" ||
@@ -51,32 +45,36 @@ async function requestDeviceCode(config) {
51
45
  }
52
46
  return json;
53
47
  }
54
- async function pollForToken(config, deviceCode) {
55
- const startedAtMs = Date.now();
56
- const expiresAtMs = startedAtMs + deviceCode.expires_in * 1000;
48
+ async function pollForToken(deviceCode) {
49
+ const config = getOAuthConfig();
50
+ const expiresAtMs = Date.now() + deviceCode.expires_in * 1000;
57
51
  let intervalSeconds = deviceCode.interval > 0
58
52
  ? deviceCode.interval
59
53
  : DEFAULT_POLL_INTERVAL_SECONDS;
60
54
  while (Date.now() < expiresAtMs) {
61
55
  await sleep(intervalSeconds * 1000);
62
- const response = await fetch(`https://${config.domain}/oauth/token`, {
63
- method: "POST",
64
- headers: { "content-type": "application/x-www-form-urlencoded" },
65
- body: new URLSearchParams({
66
- grant_type: DEVICE_CODE_GRANT,
67
- device_code: deviceCode.device_code,
68
- client_id: config.clientId,
69
- }).toString(),
70
- });
56
+ let response;
57
+ try {
58
+ response = await postToAuth0({
59
+ path: "/oauth/token",
60
+ body: new URLSearchParams({
61
+ grant_type: DEVICE_CODE_GRANT,
62
+ device_code: deviceCode.device_code,
63
+ client_id: config.clientId,
64
+ }),
65
+ });
66
+ }
67
+ catch {
68
+ // The user may already be mid-way through approving in the browser, so a
69
+ // dropped connection is not worth making them start over. Keep polling
70
+ // until the device code itself expires.
71
+ continue;
72
+ }
71
73
  if (response.ok === true) {
72
- const token = (await response.json());
73
- if (typeof token.access_token !== "string") {
74
- failWith(`Unexpected token response from OAuth provider: ${JSON.stringify(token)}`);
75
- }
76
- return token;
74
+ return parseTokenResponse(await response.json());
77
75
  }
78
- const errorBody = (await safeReadJson(response));
79
- const errorCode = errorBody !== undefined ? errorBody.error : undefined;
76
+ const errorBody = await readErrorBody(response);
77
+ const errorCode = errorCodeOf(errorBody);
80
78
  if (errorCode === "authorization_pending") {
81
79
  continue;
82
80
  }
@@ -84,57 +82,22 @@ async function pollForToken(config, deviceCode) {
84
82
  intervalSeconds = intervalSeconds + SLOW_DOWN_BACKOFF_SECONDS;
85
83
  continue;
86
84
  }
87
- if (errorCode === "expired_token" ||
88
- errorCode === "access_denied" ||
89
- errorCode === "invalid_grant") {
90
- failWith(`Authorization failed: ${errorCode}${errorBody !== undefined && errorBody.error_description !== undefined
91
- ? ` - ${errorBody.error_description}`
92
- : ""}`, { extra: { oauthError: errorBody } });
93
- }
94
- failWith(`Token polling failed (HTTP ${String(response.status)}): ${errorBody !== undefined ? JSON.stringify(errorBody) : "no body"}`, { extra: { oauthError: errorBody } });
85
+ failIfTerminal(response.status, errorCode, errorBody);
95
86
  }
96
87
  failWith("Authorization timed out before the user completed the flow.");
97
88
  }
98
- function openBrowser(url) {
99
- const command = pickOpenCommand();
100
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
101
- try {
102
- const child = spawn(command, args, {
103
- detached: true,
104
- stdio: "ignore",
89
+ function failIfTerminal(status, errorCode, errorBody) {
90
+ if (errorCode === "expired_token" ||
91
+ errorCode === "access_denied" ||
92
+ errorCode === "invalid_grant") {
93
+ failWith(`Authorization failed: ${describeError(errorBody)}`, {
94
+ extra: { oauthError: errorBody },
105
95
  });
106
- child.on("error", () => {
107
- // Browser launch failed silently; user can still use the printed URL.
108
- });
109
- child.unref();
110
- }
111
- catch {
112
- // Ignore; user can still use the printed URL.
113
- }
114
- }
115
- function pickOpenCommand() {
116
- if (platform === "darwin")
117
- return "open";
118
- if (platform === "win32")
119
- return "cmd";
120
- return "xdg-open";
121
- }
122
- function sleep(ms) {
123
- return new Promise((resolve) => setTimeout(resolve, ms));
124
- }
125
- async function safeReadText(response) {
126
- try {
127
- return await response.text();
128
- }
129
- catch {
130
- return "<unreadable body>";
131
- }
132
- }
133
- async function safeReadJson(response) {
134
- try {
135
- return await response.json();
136
96
  }
137
- catch {
138
- return undefined;
97
+ // Rate limits and provider hiccups are transient: the browser half of the
98
+ // flow is still valid, so only give up once the device code expires.
99
+ if (status === 429 || status >= 500) {
100
+ return;
139
101
  }
102
+ failWith(`Token polling failed (HTTP ${String(status)}): ${describeError(errorBody)}`, { extra: { oauthError: errorBody } });
140
103
  }
@@ -0,0 +1,22 @@
1
+ import { type IdentityTokens } from "./auth0.js";
2
+ /**
3
+ * Asks the identity provider to email a one-time code. This is the only step of
4
+ * the sign-in flow that needs no credential, which is what makes the flow
5
+ * usable from a terminal with no browser.
6
+ */
7
+ export declare function startEmailPasswordless(opts: {
8
+ email: string;
9
+ }): Promise<void>;
10
+ /**
11
+ * Trades the emailed code for an access token. `offline_access` is requested so
12
+ * the caller can refresh once a workspace exists: the workspace list is baked
13
+ * into the token's claims and a freshly created workspace is not in them yet.
14
+ */
15
+ export declare function exchangeEmailOtp(opts: {
16
+ email: string;
17
+ otp: string;
18
+ }): Promise<IdentityTokens>;
19
+ export declare function refreshAccessToken(opts: {
20
+ refreshToken: string;
21
+ }): Promise<IdentityTokens>;
22
+ //# sourceMappingURL=passwordless.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwordless.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/passwordless.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,cAAc,EAIpB,MAAM,YAAY,CAAC;AAMpB;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBhB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,cAAc,CAAC,CAiC1B;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,cAAc,CAAC,CAqB1B"}
@@ -0,0 +1,70 @@
1
+ import { getOAuthConfig } from "../../oauthConfig.js";
2
+ import { failWith } from "../runHandler.js";
3
+ import { describeError, errorCodeOf, parseTokenResponse, postToAuth0, readErrorBody, } from "./auth0.js";
4
+ const PASSWORDLESS_OTP_GRANT = "http://auth0.com/oauth/grant-type/passwordless/otp";
5
+ const EMAIL_REALM = "email";
6
+ /**
7
+ * Asks the identity provider to email a one-time code. This is the only step of
8
+ * the sign-in flow that needs no credential, which is what makes the flow
9
+ * usable from a terminal with no browser.
10
+ */
11
+ export async function startEmailPasswordless(opts) {
12
+ const config = getOAuthConfig();
13
+ const response = await postToAuth0({
14
+ path: "/passwordless/start",
15
+ body: {
16
+ client_id: config.clientId,
17
+ connection: EMAIL_REALM,
18
+ email: opts.email,
19
+ send: "code",
20
+ },
21
+ });
22
+ if (response.ok === false) {
23
+ const body = await readErrorBody(response);
24
+ failWith(`Failed to send the verification code to ${opts.email} (HTTP ${String(response.status)}): ${describeError(body)}`, { extra: { oauthError: body } });
25
+ }
26
+ }
27
+ /**
28
+ * Trades the emailed code for an access token. `offline_access` is requested so
29
+ * the caller can refresh once a workspace exists: the workspace list is baked
30
+ * into the token's claims and a freshly created workspace is not in them yet.
31
+ */
32
+ export async function exchangeEmailOtp(opts) {
33
+ const config = getOAuthConfig();
34
+ const response = await postToAuth0({
35
+ path: "/oauth/token",
36
+ body: new URLSearchParams({
37
+ grant_type: PASSWORDLESS_OTP_GRANT,
38
+ client_id: config.clientId,
39
+ username: opts.email,
40
+ otp: opts.otp,
41
+ realm: EMAIL_REALM,
42
+ audience: config.audience,
43
+ scope: config.scope,
44
+ }),
45
+ });
46
+ if (response.ok === false) {
47
+ const body = await readErrorBody(response);
48
+ if (errorCodeOf(body) === "invalid_grant") {
49
+ failWith("That code is not valid or has expired. Codes are single-use and short-lived — request a new one by re-running without --code.", { extra: { oauthError: body } });
50
+ }
51
+ failWith(`Failed to verify the code (HTTP ${String(response.status)}): ${describeError(body)}`, { extra: { oauthError: body } });
52
+ }
53
+ return parseTokenResponse(await response.json());
54
+ }
55
+ export async function refreshAccessToken(opts) {
56
+ const config = getOAuthConfig();
57
+ const response = await postToAuth0({
58
+ path: "/oauth/token",
59
+ body: new URLSearchParams({
60
+ grant_type: "refresh_token",
61
+ client_id: config.clientId,
62
+ refresh_token: opts.refreshToken,
63
+ }),
64
+ });
65
+ if (response.ok === false) {
66
+ const body = await readErrorBody(response);
67
+ failWith(`Failed to refresh the access token (HTTP ${String(response.status)}): ${describeError(body)}`, { extra: { oauthError: body } });
68
+ }
69
+ return parseTokenResponse(await response.json());
70
+ }
@@ -0,0 +1,21 @@
1
+ import { type IdentityTokens } from "./auth0.js";
2
+ import { type ResolvedWorkspace } from "./workspace.js";
3
+ export declare const DEFAULT_BASE_URL = "https://api.getcargo.io";
4
+ export declare function resolveBaseUrl(override: string | undefined): string;
5
+ /**
6
+ * Picks the workspace to work in and stores the signed-in session. Every
7
+ * sign-in channel ends here, so the credentials file looks the same however
8
+ * the user signed in.
9
+ *
10
+ * The access token is short-lived and deliberately not treated as the
11
+ * credential: the refresh token beside it is what keeps the CLI signed in, and
12
+ * it is renewed on demand rather than by minting an API token per machine.
13
+ */
14
+ export declare function establishSession(opts: {
15
+ tokens: IdentityTokens;
16
+ /** Raw --base-url, so the default is not written to the credentials file. */
17
+ baseUrlOverride: string | undefined;
18
+ workspaceUuid: string | undefined;
19
+ workspaceName: string | undefined;
20
+ }): Promise<ResolvedWorkspace>;
21
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/session.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,gBAAgB,CAAC;AAE1E,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAE1D,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMnE;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,MAAM,EAAE,cAAc,CAAC;IACvB,6EAA6E;IAC7E,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA4C7B"}
@@ -0,0 +1,55 @@
1
+ import { saveCredentials } from "../../credentials.js";
2
+ import { getOAuthConfig } from "../../oauthConfig.js";
3
+ import { failWith } from "../runHandler.js";
4
+ import { refreshAccessToken } from "./passwordless.js";
5
+ import { resolveWorkspace } from "./workspace.js";
6
+ export const DEFAULT_BASE_URL = "https://api.getcargo.io";
7
+ export function resolveBaseUrl(override) {
8
+ if (override !== undefined && override.length > 0) {
9
+ return override;
10
+ }
11
+ return DEFAULT_BASE_URL;
12
+ }
13
+ /**
14
+ * Picks the workspace to work in and stores the signed-in session. Every
15
+ * sign-in channel ends here, so the credentials file looks the same however
16
+ * the user signed in.
17
+ *
18
+ * The access token is short-lived and deliberately not treated as the
19
+ * credential: the refresh token beside it is what keeps the CLI signed in, and
20
+ * it is renewed on demand rather than by minting an API token per machine.
21
+ */
22
+ export async function establishSession(opts) {
23
+ const baseUrl = resolveBaseUrl(opts.baseUrlOverride);
24
+ const { workspace, wasCreated } = await resolveWorkspace({
25
+ baseUrl,
26
+ accessToken: opts.tokens.accessToken,
27
+ explicitUuid: opts.workspaceUuid,
28
+ explicitName: opts.workspaceName,
29
+ });
30
+ let { accessToken, refreshToken, expiresAt } = opts.tokens;
31
+ if (refreshToken === undefined) {
32
+ failWith("The identity provider issued no refresh token, so the CLI cannot keep this session alive. Enable the Refresh Token grant for the CLI application in Auth0, then sign in again.");
33
+ }
34
+ if (wasCreated === true) {
35
+ // The workspace list is a claim on the access token, so a workspace created
36
+ // moments ago is absent from the one we are holding and every request for
37
+ // it would be rejected. Only reissuing the token picks it up.
38
+ const refreshed = await refreshAccessToken({ refreshToken });
39
+ accessToken = refreshed.accessToken;
40
+ expiresAt = refreshed.expiresAt;
41
+ // Only a tenant that rotates refresh tokens returns a new one; keep the
42
+ // existing one otherwise, since it is still the valid one.
43
+ if (refreshed.refreshToken !== undefined) {
44
+ refreshToken = refreshed.refreshToken;
45
+ }
46
+ }
47
+ const { domain, clientId } = getOAuthConfig();
48
+ saveCredentials({
49
+ accessToken,
50
+ workspaceUuid: workspace.uuid,
51
+ baseUrl: opts.baseUrlOverride,
52
+ session: { refreshToken, expiresAt, domain, clientId },
53
+ });
54
+ return workspace;
55
+ }
@@ -1,10 +1,18 @@
1
+ import { type WorkspaceManagement } from "@cargo-ai/api";
1
2
  export type ResolvedWorkspace = {
2
3
  uuid: string;
3
4
  name: string;
4
5
  };
6
+ export type WorkspaceResolution = {
7
+ workspace: ResolvedWorkspace;
8
+ /** Callers need this: a new workspace is absent from the caller's token claims. */
9
+ wasCreated: boolean;
10
+ };
5
11
  export declare function resolveWorkspace(opts: {
6
12
  baseUrl: string;
7
13
  accessToken: string;
8
14
  explicitUuid: string | undefined;
9
- }): Promise<ResolvedWorkspace>;
15
+ explicitName: string | undefined;
16
+ }): Promise<WorkspaceResolution>;
17
+ export declare function findWorkspaceByName(workspaces: readonly WorkspaceManagement.Workspace[], name: string): ResolvedWorkspace | undefined;
10
18
  //# sourceMappingURL=workspace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/workspace.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAyBF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoH7B"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/workspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAYzD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,iBAAiB,CAAC;IAC7B,mFAAmF;IACnF,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA2H/B;AAcD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,mBAAmB,CAAC,SAAS,EAAE,EACpD,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,SAAS,CAY/B"}
@@ -1,41 +1,26 @@
1
- import * as fs from "node:fs";
2
1
  import * as readline from "node:readline/promises";
3
- import * as tty from "node:tty";
4
2
  import { createApi } from "../../api.js";
5
- import { ExitCodes, failWith, info, startSpinner, success, } from "../runHandler.js";
6
- // Interactive prompts must not assume stdin is a terminal: `curl | sh`
7
- // installers and agent-driven shells pipe stdin while the controlling
8
- // terminal stays reachable via /dev/tty. Prefer stdin when it is a TTY,
9
- // otherwise fall back to opening /dev/tty directly.
10
- function openPromptInput() {
11
- if (process.stdin.isTTY === true) {
12
- return { stream: process.stdin, close: () => undefined };
13
- }
14
- try {
15
- const fd = fs.openSync("/dev/tty", "r");
16
- const stream = new tty.ReadStream(fd);
17
- return { stream, close: () => stream.destroy() };
18
- }
19
- catch {
20
- return undefined;
21
- }
22
- }
3
+ import { askQuestion, openPromptInput } from "../../utils/prompt.js";
4
+ import { ExitCodes, failWith, handleApiCall, info, success, } from "../runHandler.js";
23
5
  export async function resolveWorkspace(opts) {
6
+ const accessToken = opts.accessToken;
24
7
  const api = createApi({
25
8
  baseUrl: opts.baseUrl,
26
- accessToken: opts.accessToken,
9
+ getAccessToken: () => Promise.resolve(accessToken),
27
10
  workspaceUuid: undefined,
28
11
  });
29
- let workspaces;
30
- try {
31
- const result = await api.workspaceManagement.workspace.all({
32
- refresh: false,
33
- });
34
- workspaces = result.workspaces;
35
- }
36
- catch (err) {
37
- const message = err instanceof Error ? err.message : String(err);
38
- failWith(`Failed to list workspaces: ${message}`);
12
+ const { workspaces } = await handleApiCall(() => api.workspaceManagement.workspace.all({ refresh: false }), { spinner: "Loading workspaces..." });
13
+ if (opts.explicitName !== undefined && opts.explicitName.length > 0) {
14
+ // Reuse a same-named workspace rather than creating a duplicate, so that
15
+ // re-running a non-interactive sign-in is idempotent as documented.
16
+ const existingWorkspace = findWorkspaceByName(workspaces, opts.explicitName);
17
+ if (existingWorkspace !== undefined) {
18
+ return { workspace: existingWorkspace, wasCreated: false };
19
+ }
20
+ return {
21
+ workspace: await createWorkspace(api, opts.explicitName),
22
+ wasCreated: true,
23
+ };
39
24
  }
40
25
  if (opts.explicitUuid !== undefined) {
41
26
  const matchedWorkspace = workspaces.find((workspace) => {
@@ -44,16 +29,20 @@ export async function resolveWorkspace(opts) {
44
29
  if (matchedWorkspace === undefined) {
45
30
  failWith(`Workspace ${opts.explicitUuid} is not in your accessible workspaces.`, { code: ExitCodes.NotFound });
46
31
  }
47
- return { uuid: matchedWorkspace.uuid, name: matchedWorkspace.name };
32
+ return {
33
+ workspace: { uuid: matchedWorkspace.uuid, name: matchedWorkspace.name },
34
+ wasCreated: false,
35
+ };
48
36
  }
49
37
  if (workspaces.length === 0) {
50
38
  const promptInput = openPromptInput();
51
39
  if (promptInput === undefined) {
52
- failWith("No workspaces found.", { code: ExitCodes.NotFound });
40
+ failWith("No workspaces found. Re-run with --workspace-name <name> to create one.", { code: ExitCodes.NotFound });
53
41
  }
54
42
  let name;
55
43
  try {
56
- name = await promptWorkspaceName(promptInput.stream);
44
+ info("You don't have any workspaces yet. Let's create one.");
45
+ name = await askQuestion(promptInput.stream, "Workspace name (or press Enter to skip): ");
57
46
  }
58
47
  finally {
59
48
  promptInput.close();
@@ -63,31 +52,19 @@ export async function resolveWorkspace(opts) {
63
52
  code: ExitCodes.InvalidUsage,
64
53
  });
65
54
  }
66
- const spinner = startSpinner("Creating workspace...");
67
- let createdWorkspace;
68
- try {
69
- createdWorkspace = await api.workspaceManagement.workspace.create({
70
- name,
71
- });
72
- }
73
- catch (err) {
74
- spinner.stop();
75
- const message = err instanceof Error ? err.message : String(err);
76
- failWith(`Failed to create workspace: ${message}`);
77
- }
78
- spinner.stop();
79
- success(`Created workspace: ${createdWorkspace.workspace.name} (${createdWorkspace.workspace.uuid})`);
80
- return {
81
- uuid: createdWorkspace.workspace.uuid,
82
- name: createdWorkspace.workspace.name,
83
- };
55
+ const workspace = await createWorkspace(api, name);
56
+ success(`Created workspace: ${workspace.name} (${workspace.uuid})`);
57
+ return { workspace, wasCreated: true };
84
58
  }
85
59
  if (workspaces.length === 1) {
86
60
  const [firstWorkspace] = workspaces;
87
61
  if (firstWorkspace === undefined) {
88
62
  failWith("Unexpected empty workspace list after length check.");
89
63
  }
90
- return { uuid: firstWorkspace.uuid, name: firstWorkspace.name };
64
+ return {
65
+ workspace: { uuid: firstWorkspace.uuid, name: firstWorkspace.name },
66
+ wasCreated: false,
67
+ };
91
68
  }
92
69
  const promptInput = openPromptInput();
93
70
  if (promptInput === undefined) {
@@ -103,25 +80,21 @@ export async function resolveWorkspace(opts) {
103
80
  if (selectedWorkspace === undefined) {
104
81
  failWith("No workspace selected.", { code: ExitCodes.InvalidUsage });
105
82
  }
106
- return selectedWorkspace;
83
+ return { workspace: selectedWorkspace, wasCreated: false };
107
84
  }
108
- async function promptWorkspaceName(input) {
109
- const rl = readline.createInterface({
110
- input,
111
- output: process.stderr,
85
+ async function createWorkspace(api, name) {
86
+ const result = await handleApiCall(() => api.workspaceManagement.workspace.create({ name }), { spinner: "Creating workspace..." });
87
+ return { uuid: result.workspace.uuid, name: result.workspace.name };
88
+ }
89
+ export function findWorkspaceByName(workspaces, name) {
90
+ const wanted = name.trim().toLowerCase();
91
+ const matched = workspaces.find((workspace) => {
92
+ return workspace.name.trim().toLowerCase() === wanted;
112
93
  });
113
- try {
114
- info("You don't have any workspaces yet. Let's create one.");
115
- const answer = await rl.question("Workspace name (or press Enter to skip): ");
116
- const trimmed = answer.trim();
117
- if (trimmed === "") {
118
- return undefined;
119
- }
120
- return trimmed;
121
- }
122
- finally {
123
- rl.close();
94
+ if (matched === undefined) {
95
+ return undefined;
124
96
  }
97
+ return { uuid: matched.uuid, name: matched.name };
125
98
  }
126
99
  async function promptWorkspaceSelection(workspaces, input) {
127
100
  const rl = readline.createInterface({
@@ -0,0 +1,26 @@
1
+ import { type Billing } from "@cargo-ai/api";
2
+ import type { Api } from "../../api.js";
3
+ export declare const DEFAULT_TIMEOUT_SECONDS = 300;
4
+ export declare const DEFAULT_POLL_INTERVAL_SECONDS = 3;
5
+ export type AddPaymentMethodOptions = {
6
+ timeout: number;
7
+ pollInterval: number;
8
+ open: boolean;
9
+ };
10
+ /**
11
+ * Hands card entry off to Stripe's hosted billing portal and waits for the card
12
+ * on file to change.
13
+ *
14
+ * The card never touches this process: the CLI only ever sees the last four
15
+ * digits that the API already exposes. Polling is what makes the handoff usable
16
+ * from a script — the caller gets a definite answer instead of having to guess
17
+ * whether the human finished.
18
+ */
19
+ export declare function runAddPaymentMethod(api: Api, opts: AddPaymentMethodOptions): Promise<void>;
20
+ /**
21
+ * Stripe never exposes a raw card number here, so a change is detected from the
22
+ * fields the API does return. Replacing a card with the very same one is
23
+ * therefore indistinguishable from doing nothing, and the command times out.
24
+ */
25
+ export declare function fingerprintCard(card: Billing.CreditCard | undefined): string;
26
+ //# sourceMappingURL=addPaymentMethod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addPaymentMethod.d.ts","sourceRoot":"","sources":["../../../src/commands/billing/addPaymentMethod.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAWxC,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,uBAAuB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAgDf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,GAAG,SAAS,GAAG,MAAM,CAW5E"}