@cargo-ai/cli 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/build/api.d.ts +5 -1
- package/build/api.d.ts.map +1 -1
- package/build/api.js +6 -7
- package/build/commands/{auth.d.ts → auth/index.d.ts} +2 -2
- package/build/commands/auth/index.d.ts.map +1 -0
- package/build/commands/auth/index.js +131 -0
- package/build/commands/auth/oauth.d.ts +7 -0
- package/build/commands/auth/oauth.d.ts.map +1 -0
- package/build/commands/auth/oauth.js +140 -0
- package/build/commands/auth/workspace.d.ts +10 -0
- package/build/commands/auth/workspace.d.ts.map +1 -0
- package/build/commands/auth/workspace.js +120 -0
- package/build/commands/billing/subscription.d.ts.map +1 -1
- package/build/commands/billing/subscription.js +55 -17
- package/build/commands/orchestration/index.d.ts.map +1 -1
- package/build/commands/orchestration/index.js +2 -0
- package/build/commands/orchestration/query.d.ts +4 -0
- package/build/commands/orchestration/query.d.ts.map +1 -0
- package/build/commands/orchestration/query.js +23 -0
- package/build/commands/orchestration/run.d.ts.map +1 -1
- package/build/commands/orchestration/run.js +6 -1
- package/build/commands/runHandler.d.ts +32 -1
- package/build/commands/runHandler.d.ts.map +1 -1
- package/build/commands/runHandler.js +191 -24
- package/build/commands/storage/index.d.ts.map +1 -1
- package/build/commands/storage/index.js +2 -0
- package/build/commands/storage/query.d.ts +4 -0
- package/build/commands/storage/query.d.ts.map +1 -0
- package/build/commands/storage/query.js +23 -0
- package/build/commands/workspaceManagement/file.d.ts.map +1 -0
- package/build/commands/workspaceManagement/folder.d.ts.map +1 -0
- package/build/commands/workspaceManagement/index.d.ts +4 -0
- package/build/commands/workspaceManagement/index.d.ts.map +1 -0
- package/build/commands/workspaceManagement/index.js +19 -0
- package/build/commands/workspaceManagement/report.d.ts +4 -0
- package/build/commands/workspaceManagement/report.d.ts.map +1 -0
- package/build/commands/workspaceManagement/report.js +17 -0
- package/build/commands/workspaceManagement/role.d.ts.map +1 -0
- package/build/commands/workspaceManagement/token.d.ts.map +1 -0
- package/build/commands/{workspace → workspaceManagement}/token.js +3 -2
- package/build/commands/workspaceManagement/user.d.ts.map +1 -0
- package/build/commands/{workspace → workspaceManagement}/user.js +30 -22
- package/build/commands/workspaceManagement/workspaces.d.ts.map +1 -0
- package/build/commands/{workspace → workspaceManagement}/workspaces.js +36 -7
- package/build/config.d.ts +3 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +19 -17
- package/build/index.js +24 -10
- package/build/oauthConfig.d.ts +13 -0
- package/build/oauthConfig.d.ts.map +1 -0
- package/build/oauthConfig.js +17 -0
- package/package.json +2 -2
- package/build/commands/auth.d.ts.map +0 -1
- package/build/commands/auth.js +0 -104
- package/build/commands/workspace/file.d.ts.map +0 -1
- package/build/commands/workspace/folder.d.ts.map +0 -1
- package/build/commands/workspace/index.d.ts +0 -4
- package/build/commands/workspace/index.d.ts.map +0 -1
- package/build/commands/workspace/index.js +0 -17
- package/build/commands/workspace/role.d.ts.map +0 -1
- package/build/commands/workspace/token.d.ts.map +0 -1
- package/build/commands/workspace/user.d.ts.map +0 -1
- package/build/commands/workspace/workspaces.d.ts.map +0 -1
- /package/build/commands/{workspace → workspaceManagement}/file.d.ts +0 -0
- /package/build/commands/{workspace → workspaceManagement}/file.js +0 -0
- /package/build/commands/{workspace → workspaceManagement}/folder.d.ts +0 -0
- /package/build/commands/{workspace → workspaceManagement}/folder.js +0 -0
- /package/build/commands/{workspace → workspaceManagement}/role.d.ts +0 -0
- /package/build/commands/{workspace → workspaceManagement}/role.js +0 -0
- /package/build/commands/{workspace → workspaceManagement}/token.d.ts +0 -0
- /package/build/commands/{workspace → workspaceManagement}/user.d.ts +0 -0
- /package/build/commands/{workspace → workspaceManagement}/workspaces.d.ts +0 -0
package/README.md
CHANGED
|
@@ -23,10 +23,14 @@ npx @cargo-ai/cli orchestration workflow list
|
|
|
23
23
|
|
|
24
24
|
## Authentication
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
A single `cargo-ai login` command supports two modes — pass exactly one of `--token` or `--oauth`:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
+
# 1. Sign in with an existing workspace-scoped API token
|
|
29
30
|
cargo-ai login --token <your-api-token>
|
|
31
|
+
|
|
32
|
+
# 2. Sign in via your browser using the OAuth 2.0 Device Authorization Flow
|
|
33
|
+
cargo-ai login --oauth
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
Credentials are stored in `~/.config/cargo-ai/credentials.json` (file mode `0600`).
|
|
@@ -39,6 +43,23 @@ cargo-ai whoami
|
|
|
39
43
|
cargo-ai logout
|
|
40
44
|
```
|
|
41
45
|
|
|
46
|
+
### Browser sign-in (`--oauth`)
|
|
47
|
+
|
|
48
|
+
`cargo-ai login --oauth` runs the standard [OAuth 2.0 Device Authorization Flow](https://datatracker.ietf.org/doc/html/rfc8628) (similar UX to `gh auth login`) against the Cargo OAuth provider, prints a verification URL and user code, opens your default browser, and polls until you complete sign-in. On success the access token is written to `~/.config/cargo-ai/credentials.json`.
|
|
49
|
+
|
|
50
|
+
The CLI ships with a built-in OAuth client, so no setup is required. Advanced users can override the provider via `--client-id`, `--domain`, and `--audience`.
|
|
51
|
+
|
|
52
|
+
### Workspace selection
|
|
53
|
+
|
|
54
|
+
API tokens (`--token`) are workspace-scoped, so the workspace is implicit and nothing is persisted alongside the token.
|
|
55
|
+
|
|
56
|
+
OAuth sessions (`--oauth`) are user-scoped, so after authentication the CLI picks a default workspace and saves it next to the token:
|
|
57
|
+
|
|
58
|
+
- **One workspace**: it is selected automatically.
|
|
59
|
+
- **Multiple workspaces**: you are prompted to pick one (in a TTY); in non-interactive shells the CLI prints the list and asks you to re-run with `--workspace-uuid <uuid>`.
|
|
60
|
+
|
|
61
|
+
Pass `cargo-ai login --oauth --workspace-uuid <uuid>` to skip the prompt. `CARGO_WORKSPACE_UUID` (or per-command `--workspace-uuid` flags where supported) overrides the saved default at runtime.
|
|
62
|
+
|
|
42
63
|
### Environment variables (override)
|
|
43
64
|
|
|
44
65
|
Environment variables take precedence over saved credentials, useful for CI or temporary overrides.
|
|
@@ -81,7 +102,7 @@ cargo-ai orchestration workflow --help
|
|
|
81
102
|
| ------------------------ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
82
103
|
| **init** | Workspace initialisation (user, workspace, datasets, etc.) | `cargo-ai init` |
|
|
83
104
|
| **orchestration** | Workflows, plays, runs, batches, tools, templates | `cargo-ai orchestration workflow list`, `cargo-ai orchestration run list --workflow-uuid <uuid>` |
|
|
84
|
-
| **
|
|
105
|
+
| **workspaceManagement** | Workspaces, users, tokens, roles, folders | `cargo-ai workspaceManagement workspaces list`, `cargo-ai workspaceManagement token list` |
|
|
85
106
|
| **storage** | Datasets, models, relationships, runs, records | `cargo-ai storage dataset list`, `cargo-ai storage model list` |
|
|
86
107
|
| **connection** | Connectors and integrations | `cargo-ai connection connector list`, `cargo-ai connection integration list` |
|
|
87
108
|
| **billing** | Usage and subscription | `cargo-ai billing subscription get`, `cargo-ai billing usage get-metrics --payload '{"from":"2025-01-01","to":"2025-01-31"}'` |
|
package/build/api.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type Api } from "@cargo-ai/api";
|
|
2
2
|
export type { Api };
|
|
3
|
-
export declare function createApi(
|
|
3
|
+
export declare function createApi(opts: {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
accessToken: string;
|
|
6
|
+
workspaceUuid: string | undefined;
|
|
7
|
+
}): Api;
|
|
4
8
|
//# sourceMappingURL=api.d.ts.map
|
package/build/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,eAAe,CAAC;AAEnD,YAAY,EAAE,GAAG,EAAE,CAAC;AAQpB,wBAAgB,SAAS,CAAC,IAAI,EAAE;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG,GAAG,CAON"}
|
package/build/api.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { buildApi } from "@cargo-ai/api";
|
|
3
|
-
import { getConfig } from "./config.js";
|
|
4
3
|
const require = createRequire(import.meta.url);
|
|
5
4
|
const { version } = require("../package.json");
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const ORIGIN = { name: "cli", version };
|
|
6
|
+
export function createApi(opts) {
|
|
8
7
|
return buildApi({
|
|
9
|
-
baseUrl,
|
|
10
|
-
workspaceUuid,
|
|
11
|
-
|
|
12
|
-
origin:
|
|
8
|
+
baseUrl: opts.baseUrl,
|
|
9
|
+
workspaceUuid: opts.workspaceUuid,
|
|
10
|
+
accessToken: opts.accessToken,
|
|
11
|
+
origin: ORIGIN,
|
|
13
12
|
});
|
|
14
13
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import type { Api } from "
|
|
2
|
+
import type { Api } from "../../api.js";
|
|
3
3
|
export declare function registerAuthCommands(program: Command, getApi: () => Api): void;
|
|
4
|
-
//# sourceMappingURL=
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AA8BxC,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAiFN"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getConfig } from "../../config.js";
|
|
2
|
+
import { clearCredentials, getCredentialsPath, saveCredentials, } from "../../credentials.js";
|
|
3
|
+
import { ExitCodes, failWith, handleApiCall, info, outputJson, success, } from "../runHandler.js";
|
|
4
|
+
import { runOAuthDeviceFlow } from "./oauth.js";
|
|
5
|
+
import { resolveWorkspace } from "./workspace.js";
|
|
6
|
+
const DEFAULT_BASE_URL = "https://api.getcargo.io";
|
|
7
|
+
export function registerAuthCommands(program, getApi) {
|
|
8
|
+
program
|
|
9
|
+
.command("login")
|
|
10
|
+
.description("Sign in to Cargo with an API token (--token) or via browser (--oauth)")
|
|
11
|
+
.option("--token <token>", "Cargo API token; mutually exclusive with --oauth")
|
|
12
|
+
.option("--oauth", "Sign in via browser using the OAuth 2.0 Device Authorization Flow")
|
|
13
|
+
.option("--base-url <url>", `Cargo API base URL (string, default: ${DEFAULT_BASE_URL})`)
|
|
14
|
+
.option("--workspace-uuid <uuid>", "Default workspace UUID to save (--oauth only; skips the workspace prompt)")
|
|
15
|
+
.option("--client-id <id>", "OAuth native application client ID (--oauth only; defaults to the built-in Cargo CLI client)")
|
|
16
|
+
.option("--domain <domain>", "OAuth provider domain (--oauth only; default: auth.getcargo.io)")
|
|
17
|
+
.option("--audience <audience>", "OAuth API audience (--oauth only; default: https://api.getcargo.io)")
|
|
18
|
+
.addHelpText("after", `
|
|
19
|
+
Examples:
|
|
20
|
+
$ cargo-ai login --token sk_live_abc123
|
|
21
|
+
$ cargo-ai login --token sk_live_abc123 --base-url https://api.custom.io
|
|
22
|
+
$ cargo-ai login --oauth
|
|
23
|
+
$ cargo-ai login --oauth --workspace-uuid 550e8400-e29b-41d4-a716-446655440000
|
|
24
|
+
|
|
25
|
+
Credentials are stored in ~/.config/cargo-ai/credentials.json.
|
|
26
|
+
Environment variables (CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL) take precedence over saved credentials.`)
|
|
27
|
+
.action(async (opts) => {
|
|
28
|
+
await runLogin(opts);
|
|
29
|
+
});
|
|
30
|
+
program
|
|
31
|
+
.command("logout")
|
|
32
|
+
.description("Remove saved API credentials")
|
|
33
|
+
.action(() => {
|
|
34
|
+
const removed = clearCredentials();
|
|
35
|
+
if (removed === true) {
|
|
36
|
+
success("Signed out.");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
info("No saved credentials to remove.");
|
|
40
|
+
}
|
|
41
|
+
outputJson({
|
|
42
|
+
ok: true,
|
|
43
|
+
removed,
|
|
44
|
+
credentialsFile: getCredentialsPath(),
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
program
|
|
48
|
+
.command("whoami")
|
|
49
|
+
.description("Show current user and workspace")
|
|
50
|
+
.addHelpText("after", `
|
|
51
|
+
Returns JSON with: user (uuid, email, name), workspace (uuid, name),
|
|
52
|
+
authentication source ("environment" or "credentials-file"), and base URL.`)
|
|
53
|
+
.action(async () => {
|
|
54
|
+
await runWhoami(getApi);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function runLogin(opts) {
|
|
58
|
+
const hasToken = opts.token !== undefined;
|
|
59
|
+
const wantsOAuth = opts.oauth === true;
|
|
60
|
+
if (hasToken === true && wantsOAuth === true) {
|
|
61
|
+
failWith("Cannot combine --token and --oauth; pass only one.", {
|
|
62
|
+
code: ExitCodes.InvalidUsage,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (hasToken === false && wantsOAuth === false) {
|
|
66
|
+
failWith("Must pass --token <token> (API token) or --oauth (browser sign-in).", { code: ExitCodes.InvalidUsage });
|
|
67
|
+
}
|
|
68
|
+
const baseUrl = opts.baseUrl !== undefined && opts.baseUrl.length > 0
|
|
69
|
+
? opts.baseUrl
|
|
70
|
+
: DEFAULT_BASE_URL;
|
|
71
|
+
if (hasToken === true) {
|
|
72
|
+
const token = opts.token;
|
|
73
|
+
saveCredentials({
|
|
74
|
+
accessToken: token,
|
|
75
|
+
baseUrl: opts.baseUrl,
|
|
76
|
+
});
|
|
77
|
+
success("Signed in with API token.");
|
|
78
|
+
outputJson({
|
|
79
|
+
ok: true,
|
|
80
|
+
credentialsFile: getCredentialsPath(),
|
|
81
|
+
baseUrl,
|
|
82
|
+
tokenType: "api-token",
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const result = await runOAuthDeviceFlow({
|
|
87
|
+
clientId: opts.clientId,
|
|
88
|
+
domain: opts.domain,
|
|
89
|
+
audience: opts.audience,
|
|
90
|
+
});
|
|
91
|
+
const workspace = await resolveWorkspace({
|
|
92
|
+
baseUrl,
|
|
93
|
+
accessToken: result.accessToken,
|
|
94
|
+
explicitUuid: opts.workspaceUuid,
|
|
95
|
+
});
|
|
96
|
+
saveCredentials({
|
|
97
|
+
accessToken: result.accessToken,
|
|
98
|
+
workspaceUuid: workspace.uuid,
|
|
99
|
+
baseUrl: opts.baseUrl,
|
|
100
|
+
});
|
|
101
|
+
success(`Signed in. Default workspace: ${workspace.name}.`);
|
|
102
|
+
outputJson({
|
|
103
|
+
ok: true,
|
|
104
|
+
credentialsFile: getCredentialsPath(),
|
|
105
|
+
baseUrl,
|
|
106
|
+
workspace,
|
|
107
|
+
tokenType: "oauth-jwt",
|
|
108
|
+
expiresIn: result.expiresIn,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function runWhoami(getApi) {
|
|
112
|
+
const { accessToken, baseUrl, source } = getConfig();
|
|
113
|
+
if (accessToken === undefined) {
|
|
114
|
+
failWith('Not authenticated. Run "cargo-ai login --token <token>" or "cargo-ai login --oauth".', { code: ExitCodes.NotAuthenticated });
|
|
115
|
+
}
|
|
116
|
+
const api = getApi();
|
|
117
|
+
const { user } = await handleApiCall(() => api.userManagement.user.getCurrent());
|
|
118
|
+
const { workspace } = await handleApiCall(() => api.workspaceManagement.workspace.getCurrent());
|
|
119
|
+
outputJson({
|
|
120
|
+
user: {
|
|
121
|
+
uuid: user.uuid,
|
|
122
|
+
email: user.email,
|
|
123
|
+
firstName: user.firstName,
|
|
124
|
+
lastName: user.lastName,
|
|
125
|
+
},
|
|
126
|
+
workspace: { uuid: workspace.uuid, name: workspace.name },
|
|
127
|
+
source,
|
|
128
|
+
baseUrl,
|
|
129
|
+
credentialsFile: getCredentialsPath(),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
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>;
|
|
7
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
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";
|
|
5
|
+
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
6
|
+
const SLOW_DOWN_BACKOFF_SECONDS = 5;
|
|
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);
|
|
11
|
+
info([
|
|
12
|
+
"",
|
|
13
|
+
"To complete sign-in, open this URL in a browser:",
|
|
14
|
+
` ${deviceCode.verification_uri_complete}`,
|
|
15
|
+
"",
|
|
16
|
+
`If your browser does not show the code prefilled, enter it manually: ${deviceCode.user_code}`,
|
|
17
|
+
"",
|
|
18
|
+
"Waiting for you to finish in the browser...",
|
|
19
|
+
"",
|
|
20
|
+
].join("\n"));
|
|
21
|
+
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
|
+
};
|
|
27
|
+
}
|
|
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(),
|
|
39
|
+
});
|
|
40
|
+
if (response.ok === false) {
|
|
41
|
+
const text = await safeReadText(response);
|
|
42
|
+
failWith(`Failed to start device authorization (HTTP ${String(response.status)}): ${text}`);
|
|
43
|
+
}
|
|
44
|
+
const json = (await response.json());
|
|
45
|
+
if (typeof json.device_code !== "string" ||
|
|
46
|
+
typeof json.user_code !== "string" ||
|
|
47
|
+
typeof json.verification_uri_complete !== "string" ||
|
|
48
|
+
typeof json.expires_in !== "number" ||
|
|
49
|
+
typeof json.interval !== "number") {
|
|
50
|
+
failWith(`Unexpected device authorization response from OAuth provider: ${JSON.stringify(json)}`);
|
|
51
|
+
}
|
|
52
|
+
return json;
|
|
53
|
+
}
|
|
54
|
+
async function pollForToken(config, deviceCode) {
|
|
55
|
+
const startedAtMs = Date.now();
|
|
56
|
+
const expiresAtMs = startedAtMs + deviceCode.expires_in * 1000;
|
|
57
|
+
let intervalSeconds = deviceCode.interval > 0
|
|
58
|
+
? deviceCode.interval
|
|
59
|
+
: DEFAULT_POLL_INTERVAL_SECONDS;
|
|
60
|
+
while (Date.now() < expiresAtMs) {
|
|
61
|
+
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
|
+
});
|
|
71
|
+
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;
|
|
77
|
+
}
|
|
78
|
+
const errorBody = (await safeReadJson(response));
|
|
79
|
+
const errorCode = errorBody !== undefined ? errorBody.error : undefined;
|
|
80
|
+
if (errorCode === "authorization_pending") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (errorCode === "slow_down") {
|
|
84
|
+
intervalSeconds = intervalSeconds + SLOW_DOWN_BACKOFF_SECONDS;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
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 } });
|
|
95
|
+
}
|
|
96
|
+
failWith("Authorization timed out before the user completed the flow.");
|
|
97
|
+
}
|
|
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",
|
|
105
|
+
});
|
|
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
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ResolvedWorkspace = {
|
|
2
|
+
uuid: string;
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function resolveWorkspace(opts: {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
accessToken: string;
|
|
8
|
+
explicitUuid: string | undefined;
|
|
9
|
+
}): Promise<ResolvedWorkspace>;
|
|
10
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/workspace.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,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,CAiG7B"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as readline from "node:readline/promises";
|
|
2
|
+
import { createApi } from "../../api.js";
|
|
3
|
+
import { ExitCodes, failWith, info, startSpinner, success, } from "../runHandler.js";
|
|
4
|
+
export async function resolveWorkspace(opts) {
|
|
5
|
+
const api = createApi({
|
|
6
|
+
baseUrl: opts.baseUrl,
|
|
7
|
+
accessToken: opts.accessToken,
|
|
8
|
+
workspaceUuid: undefined,
|
|
9
|
+
});
|
|
10
|
+
let workspaces;
|
|
11
|
+
try {
|
|
12
|
+
const result = await api.workspaceManagement.workspace.all({
|
|
13
|
+
refresh: false,
|
|
14
|
+
});
|
|
15
|
+
workspaces = result.workspaces;
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19
|
+
failWith(`Failed to list workspaces: ${message}`);
|
|
20
|
+
}
|
|
21
|
+
if (opts.explicitUuid !== undefined) {
|
|
22
|
+
const matchedWorkspace = workspaces.find((workspace) => {
|
|
23
|
+
return workspace.uuid === opts.explicitUuid;
|
|
24
|
+
});
|
|
25
|
+
if (matchedWorkspace === undefined) {
|
|
26
|
+
failWith(`Workspace ${opts.explicitUuid} is not in your accessible workspaces.`, { code: ExitCodes.NotFound });
|
|
27
|
+
}
|
|
28
|
+
return { uuid: matchedWorkspace.uuid, name: matchedWorkspace.name };
|
|
29
|
+
}
|
|
30
|
+
if (workspaces.length === 0) {
|
|
31
|
+
if (process.stdin.isTTY !== true) {
|
|
32
|
+
failWith("No workspaces found.", { code: ExitCodes.NotFound });
|
|
33
|
+
}
|
|
34
|
+
const name = await promptWorkspaceName();
|
|
35
|
+
if (name === undefined) {
|
|
36
|
+
failWith("No workspace name provided.", {
|
|
37
|
+
code: ExitCodes.InvalidUsage,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const spinner = startSpinner("Creating workspace...");
|
|
41
|
+
let createdWorkspace;
|
|
42
|
+
try {
|
|
43
|
+
createdWorkspace = await api.workspaceManagement.workspace.create({
|
|
44
|
+
name,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
spinner.stop();
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
failWith(`Failed to create workspace: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
spinner.stop();
|
|
53
|
+
success(`Created workspace: ${createdWorkspace.workspace.name} (${createdWorkspace.workspace.uuid})`);
|
|
54
|
+
return {
|
|
55
|
+
uuid: createdWorkspace.workspace.uuid,
|
|
56
|
+
name: createdWorkspace.workspace.name,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (workspaces.length === 1) {
|
|
60
|
+
const [firstWorkspace] = workspaces;
|
|
61
|
+
if (firstWorkspace === undefined) {
|
|
62
|
+
failWith("Unexpected empty workspace list after length check.");
|
|
63
|
+
}
|
|
64
|
+
return { uuid: firstWorkspace.uuid, name: firstWorkspace.name };
|
|
65
|
+
}
|
|
66
|
+
if (process.stdin.isTTY !== true) {
|
|
67
|
+
failWith("Multiple workspaces available; pass --workspace-uuid <uuid> to select one.", { code: ExitCodes.InvalidUsage });
|
|
68
|
+
}
|
|
69
|
+
const selectedWorkspace = await promptWorkspaceSelection(workspaces);
|
|
70
|
+
if (selectedWorkspace === undefined) {
|
|
71
|
+
failWith("No workspace selected.", { code: ExitCodes.InvalidUsage });
|
|
72
|
+
}
|
|
73
|
+
return selectedWorkspace;
|
|
74
|
+
}
|
|
75
|
+
async function promptWorkspaceName() {
|
|
76
|
+
const rl = readline.createInterface({
|
|
77
|
+
input: process.stdin,
|
|
78
|
+
output: process.stderr,
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
info("You don't have any workspaces yet. Let's create one.");
|
|
82
|
+
const answer = await rl.question("Workspace name (or press Enter to skip): ");
|
|
83
|
+
const trimmed = answer.trim();
|
|
84
|
+
if (trimmed === "") {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
rl.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function promptWorkspaceSelection(workspaces) {
|
|
94
|
+
const rl = readline.createInterface({
|
|
95
|
+
input: process.stdin,
|
|
96
|
+
output: process.stderr,
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
info("You have multiple workspaces. Pick one to set as the default for this CLI:");
|
|
100
|
+
workspaces.forEach((workspace, idx) => {
|
|
101
|
+
info(` ${String(idx + 1)}) ${workspace.name} (${workspace.uuid})`);
|
|
102
|
+
});
|
|
103
|
+
info("");
|
|
104
|
+
const maxAttempts = 3;
|
|
105
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
106
|
+
const answer = await rl.question(`Enter a number [1-${String(workspaces.length)}]: `);
|
|
107
|
+
const trimmed = answer.trim();
|
|
108
|
+
const index = Number.parseInt(trimmed, 10);
|
|
109
|
+
const selectedWorkspace = workspaces[index - 1];
|
|
110
|
+
if (selectedWorkspace !== undefined) {
|
|
111
|
+
return { uuid: selectedWorkspace.uuid, name: selectedWorkspace.name };
|
|
112
|
+
}
|
|
113
|
+
info(`Invalid selection: ${trimmed}`);
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
rl.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../../src/commands/billing/subscription.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAGxC,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,
|
|
1
|
+
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../../src/commands/billing/subscription.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAGxC,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAsPN"}
|
|
@@ -5,7 +5,11 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
5
5
|
.description("Subscription operations");
|
|
6
6
|
subscription
|
|
7
7
|
.command("get")
|
|
8
|
-
.description("Get current subscription")
|
|
8
|
+
.description("Get the current subscription for this workspace")
|
|
9
|
+
.addHelpText("after", `
|
|
10
|
+
Examples:
|
|
11
|
+
$ cargo-ai billing subscription get
|
|
12
|
+
$ cargo-ai billing subscription get | jq '.subscription.plan'`)
|
|
9
13
|
.action(async () => {
|
|
10
14
|
const api = getApi();
|
|
11
15
|
const result = await handleApiCall(() => api.billing.subscription.get());
|
|
@@ -13,7 +17,11 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
13
17
|
});
|
|
14
18
|
subscription
|
|
15
19
|
.command("get-invoices")
|
|
16
|
-
.description("
|
|
20
|
+
.description("List invoices for the current workspace")
|
|
21
|
+
.addHelpText("after", `
|
|
22
|
+
Examples:
|
|
23
|
+
$ cargo-ai billing subscription get-invoices
|
|
24
|
+
$ cargo-ai billing subscription get-invoices | jq '.invoices | length'`)
|
|
17
25
|
.action(async () => {
|
|
18
26
|
const api = getApi();
|
|
19
27
|
const result = await handleApiCall(() => api.billing.subscription.getInvoices());
|
|
@@ -21,7 +29,10 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
21
29
|
});
|
|
22
30
|
subscription
|
|
23
31
|
.command("get-credit-card")
|
|
24
|
-
.description("Get credit card")
|
|
32
|
+
.description("Get the credit card on file for the current workspace")
|
|
33
|
+
.addHelpText("after", `
|
|
34
|
+
Example:
|
|
35
|
+
$ cargo-ai billing subscription get-credit-card`)
|
|
25
36
|
.action(async () => {
|
|
26
37
|
const api = getApi();
|
|
27
38
|
const result = await handleApiCall(() => api.billing.subscription.getCreditCard());
|
|
@@ -29,7 +40,10 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
29
40
|
});
|
|
30
41
|
subscription
|
|
31
42
|
.command("create-setup-intent")
|
|
32
|
-
.description("Create a setup intent")
|
|
43
|
+
.description("Create a Stripe setup intent for adding/updating a payment method")
|
|
44
|
+
.addHelpText("after", `
|
|
45
|
+
Example:
|
|
46
|
+
$ cargo-ai billing subscription create-setup-intent | jq -r .clientSecret`)
|
|
33
47
|
.action(async () => {
|
|
34
48
|
const api = getApi();
|
|
35
49
|
const result = await handleApiCall(() => api.billing.subscription.createSetupIntent());
|
|
@@ -37,8 +51,11 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
37
51
|
});
|
|
38
52
|
subscription
|
|
39
53
|
.command("update-payment-method")
|
|
40
|
-
.description("
|
|
41
|
-
.requiredOption("--payment-method-id <id>", "Stripe payment method ID")
|
|
54
|
+
.description("Replace the workspace's payment method")
|
|
55
|
+
.requiredOption("--payment-method-id <id>", "Stripe payment method ID (e.g. pm_1OabcXYZ...)")
|
|
56
|
+
.addHelpText("after", `
|
|
57
|
+
Example:
|
|
58
|
+
$ cargo-ai billing subscription update-payment-method --payment-method-id pm_1OabcXYZ`)
|
|
42
59
|
.action(async (opts) => {
|
|
43
60
|
const api = getApi();
|
|
44
61
|
const result = await handleApiCall(() => api.billing.subscription.updatePaymentMethod({
|
|
@@ -48,7 +65,10 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
48
65
|
});
|
|
49
66
|
subscription
|
|
50
67
|
.command("create-portal-session")
|
|
51
|
-
.description("Create a portal session")
|
|
68
|
+
.description("Create a Stripe customer portal session URL")
|
|
69
|
+
.addHelpText("after", `
|
|
70
|
+
Example:
|
|
71
|
+
$ cargo-ai billing subscription create-portal-session | jq -r .url`)
|
|
52
72
|
.action(async () => {
|
|
53
73
|
const api = getApi();
|
|
54
74
|
const result = await handleApiCall(() => api.billing.subscription.createPortalSession());
|
|
@@ -56,7 +76,10 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
56
76
|
});
|
|
57
77
|
subscription
|
|
58
78
|
.command("create-stripe-credits-checkout-session")
|
|
59
|
-
.description("Create a Stripe
|
|
79
|
+
.description("Create a Stripe checkout session for purchasing credits")
|
|
80
|
+
.addHelpText("after", `
|
|
81
|
+
Example:
|
|
82
|
+
$ cargo-ai billing subscription create-stripe-credits-checkout-session | jq -r .url`)
|
|
60
83
|
.action(async () => {
|
|
61
84
|
const api = getApi();
|
|
62
85
|
const result = await handleApiCall(() => api.billing.subscription.createStripeCreditsCheckoutSession());
|
|
@@ -64,9 +87,13 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
64
87
|
});
|
|
65
88
|
subscription
|
|
66
89
|
.command("update-plan")
|
|
67
|
-
.description("
|
|
68
|
-
.requiredOption("--credits-count <n>", "Number of credits")
|
|
69
|
-
.requiredOption("--cadence <cadence>", "
|
|
90
|
+
.description("Change the credit allotment and billing cadence")
|
|
91
|
+
.requiredOption("--credits-count <n>", "Number of credits per period")
|
|
92
|
+
.requiredOption("--cadence <cadence>", "Billing cadence. Allowed values: monthly, yearly")
|
|
93
|
+
.addHelpText("after", `
|
|
94
|
+
Examples:
|
|
95
|
+
$ cargo-ai billing subscription update-plan --credits-count 10000 --cadence monthly
|
|
96
|
+
$ cargo-ai billing subscription update-plan --credits-count 50000 --cadence yearly`)
|
|
70
97
|
.action(async (opts) => {
|
|
71
98
|
const api = getApi();
|
|
72
99
|
const result = await handleApiCall(() => api.billing.subscription.updatePlan({
|
|
@@ -77,10 +104,14 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
77
104
|
});
|
|
78
105
|
subscription
|
|
79
106
|
.command("update-topup")
|
|
80
|
-
.description("
|
|
81
|
-
.option("--enabled", "Enable
|
|
82
|
-
.requiredOption("--credits-count <n>", "Number of credits")
|
|
83
|
-
.requiredOption("--threshold <n>", "
|
|
107
|
+
.description("Configure automatic credit top-ups")
|
|
108
|
+
.option("--enabled", "Enable automatic top-ups (omit to disable)")
|
|
109
|
+
.requiredOption("--credits-count <n>", "Number of credits to add per top-up")
|
|
110
|
+
.requiredOption("--threshold <n>", "Trigger top-up when remaining credits drop below this number")
|
|
111
|
+
.addHelpText("after", `
|
|
112
|
+
Examples:
|
|
113
|
+
$ cargo-ai billing subscription update-topup --enabled --credits-count 5000 --threshold 1000
|
|
114
|
+
$ cargo-ai billing subscription update-topup --credits-count 5000 --threshold 1000 # disabled`)
|
|
84
115
|
.action(async (opts) => {
|
|
85
116
|
const api = getApi();
|
|
86
117
|
const result = await handleApiCall(() => api.billing.subscription.updateTopup({
|
|
@@ -92,11 +123,18 @@ export function registerSubscriptionCommands(parent, getApi) {
|
|
|
92
123
|
});
|
|
93
124
|
subscription
|
|
94
125
|
.command("update-credits-overage-behaviors")
|
|
95
|
-
.description("
|
|
96
|
-
.requiredOption("--behavior <unit:action>", "Behavior as unit:action (e.g. billing.credits:deny)
|
|
126
|
+
.description("Set how the workspace behaves when a credit unit runs out (allow vs deny)")
|
|
127
|
+
.requiredOption("--behavior <unit:action>", "Behavior as unit:action (e.g. billing.credits:deny). Repeat to set multiple units.", (val, acc) => {
|
|
97
128
|
acc.push(val);
|
|
98
129
|
return acc;
|
|
99
130
|
}, [])
|
|
131
|
+
.addHelpText("after", `
|
|
132
|
+
Examples:
|
|
133
|
+
$ cargo-ai billing subscription update-credits-overage-behaviors \\
|
|
134
|
+
--behavior billing.credits:deny
|
|
135
|
+
$ cargo-ai billing subscription update-credits-overage-behaviors \\
|
|
136
|
+
--behavior billing.credits:allow \\
|
|
137
|
+
--behavior data.records:deny`)
|
|
100
138
|
.action(async (opts) => {
|
|
101
139
|
const creditsOverageBehaviors = opts.behavior.map((b) => {
|
|
102
140
|
const parts = b.split(":");
|