@primitivedotdev/sdk 0.17.0 → 0.19.0
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 +165 -65
- package/dist/api/generated/index.js +1 -1
- package/dist/api/generated/sdk.gen.js +49 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +39 -7
- package/dist/{api-DrAZhxS-.js → api-C5VR_Opg.js} +81 -7
- package/dist/contract/index.d.ts +2 -2
- package/dist/contract/index.js +1 -1
- package/dist/{index-CbEivn3S.d.ts → index-CDlwyxdp.d.ts} +7 -7
- package/dist/{index-CHWqMBs6.d.ts → index-oRkCqj6u.d.ts} +195 -13
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/oclif/api-command.js +176 -92
- package/dist/oclif/auth.js +168 -0
- package/dist/oclif/commands/emails-latest.js +54 -35
- package/dist/oclif/commands/login.js +233 -0
- package/dist/oclif/commands/logout.js +87 -0
- package/dist/oclif/commands/send.js +61 -34
- package/dist/oclif/commands/whoami.js +51 -32
- package/dist/oclif/fish-completion.js +1 -1
- package/dist/oclif/index.js +6 -0
- package/dist/openapi/openapi.generated.js +385 -2
- package/dist/openapi/operations.generated.js +178 -1
- package/dist/webhook/index.d.ts +1 -1
- package/dist/webhook/index.js +1 -1
- package/dist/{webhook-zkN4wUTs.js → webhook-rUjGV6Zu.js} +4 -4
- package/oclif.manifest.json +507 -38
- package/package.json +5 -2
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { Command, Errors, Flags } from "@oclif/core";
|
|
4
|
+
import { getAccount, pollCliLogin, startCliLogin, } from "../../api/generated/sdk.gen.js";
|
|
5
|
+
import { PrimitiveApiClient } from "../../api/index.js";
|
|
6
|
+
import { API_ERROR_CODES, extractErrorCode, extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
|
|
7
|
+
import { acquireCliCredentialsLock, credentialsPath, loadCliCredentials, normalizeBaseUrl, saveCliCredentials, } from "../auth.js";
|
|
8
|
+
const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
|
|
9
|
+
function cliError(message) {
|
|
10
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
11
|
+
}
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
function openBrowser(url) {
|
|
16
|
+
const command = process.platform === "darwin"
|
|
17
|
+
? "open"
|
|
18
|
+
: process.platform === "win32"
|
|
19
|
+
? "cmd"
|
|
20
|
+
: "xdg-open";
|
|
21
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
22
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
23
|
+
child.on("error", () => undefined);
|
|
24
|
+
child.unref();
|
|
25
|
+
}
|
|
26
|
+
function unwrapData(value) {
|
|
27
|
+
const envelope = value;
|
|
28
|
+
return envelope?.data ?? null;
|
|
29
|
+
}
|
|
30
|
+
function retryAfterSeconds(result) {
|
|
31
|
+
const response = result.response;
|
|
32
|
+
const raw = response?.headers.get("retry-after");
|
|
33
|
+
if (!raw)
|
|
34
|
+
return null;
|
|
35
|
+
const parsed = Number.parseInt(raw, 10);
|
|
36
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
37
|
+
}
|
|
38
|
+
export async function checkExistingLogin(params) {
|
|
39
|
+
const baseUrlOverridden = params.baseUrl !== undefined;
|
|
40
|
+
const probeBaseUrl = baseUrlOverridden
|
|
41
|
+
? normalizeBaseUrl(params.baseUrl)
|
|
42
|
+
: params.credentials.base_url;
|
|
43
|
+
const apiClient = new PrimitiveApiClient({
|
|
44
|
+
apiKey: params.credentials.api_key,
|
|
45
|
+
baseUrl: probeBaseUrl,
|
|
46
|
+
});
|
|
47
|
+
const result = await (params.checkAccount ??
|
|
48
|
+
((client) => getAccount({
|
|
49
|
+
client: client.client,
|
|
50
|
+
responseStyle: "fields",
|
|
51
|
+
})))(apiClient);
|
|
52
|
+
if (!result.error)
|
|
53
|
+
return { status: "valid" };
|
|
54
|
+
const payload = extractErrorPayload(result.error);
|
|
55
|
+
const auth = {
|
|
56
|
+
apiKey: params.credentials.api_key,
|
|
57
|
+
baseUrl: probeBaseUrl,
|
|
58
|
+
credentials: params.credentials,
|
|
59
|
+
source: "stored",
|
|
60
|
+
};
|
|
61
|
+
const removed = removeStaleSavedCredentialOnUnauthorized({
|
|
62
|
+
auth,
|
|
63
|
+
baseUrlOverridden,
|
|
64
|
+
configDir: params.configDir,
|
|
65
|
+
payload,
|
|
66
|
+
});
|
|
67
|
+
if (removed)
|
|
68
|
+
return { status: "removed_stale" };
|
|
69
|
+
const code = extractErrorCode(payload);
|
|
70
|
+
return {
|
|
71
|
+
status: "blocked",
|
|
72
|
+
payload,
|
|
73
|
+
message: code === API_ERROR_CODES.unauthorized
|
|
74
|
+
? "Saved Primitive CLI credentials were rejected. Run `primitive logout` to remove them before logging in again."
|
|
75
|
+
: "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again.",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
class LoginCommand extends Command {
|
|
79
|
+
static description = "Log in by opening Primitive in your browser and saving an org-scoped CLI API key locally.";
|
|
80
|
+
static summary = "Log in with browser approval";
|
|
81
|
+
static examples = [
|
|
82
|
+
"<%= config.bin %> login",
|
|
83
|
+
"<%= config.bin %> login --device-name work-laptop",
|
|
84
|
+
"<%= config.bin %> login --force",
|
|
85
|
+
];
|
|
86
|
+
static flags = {
|
|
87
|
+
"base-url": Flags.string({
|
|
88
|
+
description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
|
|
89
|
+
env: "PRIMITIVE_API_URL",
|
|
90
|
+
}),
|
|
91
|
+
"device-name": Flags.string({
|
|
92
|
+
description: "Device name shown in the browser approval screen",
|
|
93
|
+
}),
|
|
94
|
+
"no-browser": Flags.boolean({
|
|
95
|
+
description: "Do not attempt to open the browser automatically",
|
|
96
|
+
}),
|
|
97
|
+
force: Flags.boolean({
|
|
98
|
+
char: "f",
|
|
99
|
+
description: "Replace saved credentials without first verifying the existing login",
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
async run() {
|
|
103
|
+
const { flags } = await this.parse(LoginCommand);
|
|
104
|
+
let releaseCredentialsLock;
|
|
105
|
+
try {
|
|
106
|
+
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
110
|
+
throw cliError(detail);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
await this.runWithCredentialLock(flags);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
releaseCredentialsLock();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async runWithCredentialLock(flags) {
|
|
120
|
+
const baseUrl = normalizeBaseUrl(flags["base-url"]);
|
|
121
|
+
let existing;
|
|
122
|
+
try {
|
|
123
|
+
existing = loadCliCredentials(this.config.configDir);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!flags.force)
|
|
127
|
+
throw error;
|
|
128
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
129
|
+
process.stderr.write(`Replacing unreadable Primitive CLI credentials because --force was set: ${detail}\n`);
|
|
130
|
+
existing = null;
|
|
131
|
+
}
|
|
132
|
+
if (existing && flags.force) {
|
|
133
|
+
process.stderr.write("Replacing saved Primitive CLI credentials after browser approval because --force was set.\n");
|
|
134
|
+
}
|
|
135
|
+
else if (existing) {
|
|
136
|
+
const existingStatus = await checkExistingLogin({
|
|
137
|
+
baseUrl: flags["base-url"],
|
|
138
|
+
configDir: this.config.configDir,
|
|
139
|
+
credentials: existing,
|
|
140
|
+
});
|
|
141
|
+
if (existingStatus.status === "removed_stale") {
|
|
142
|
+
process.stderr.write("Continuing with a new Primitive CLI login...\n");
|
|
143
|
+
}
|
|
144
|
+
else if (existingStatus.status === "blocked") {
|
|
145
|
+
writeErrorWithHints(existingStatus.payload);
|
|
146
|
+
throw cliError(existingStatus.message);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const org = existing.org_name ? ` for ${existing.org_name}` : "";
|
|
150
|
+
throw cliError(`Already logged in${org}. Run \`primitive logout\` before logging in again.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const apiClient = new PrimitiveApiClient({ baseUrl });
|
|
154
|
+
const deviceName = flags["device-name"] ?? hostname();
|
|
155
|
+
const started = await startCliLogin({
|
|
156
|
+
body: {
|
|
157
|
+
device_name: deviceName,
|
|
158
|
+
},
|
|
159
|
+
client: apiClient.client,
|
|
160
|
+
responseStyle: "fields",
|
|
161
|
+
});
|
|
162
|
+
if (started.error) {
|
|
163
|
+
writeErrorWithHints(extractErrorPayload(started.error));
|
|
164
|
+
throw cliError("Could not start Primitive CLI login.");
|
|
165
|
+
}
|
|
166
|
+
const start = unwrapData(started.data);
|
|
167
|
+
if (!start) {
|
|
168
|
+
throw cliError("Primitive API returned an empty CLI login response.");
|
|
169
|
+
}
|
|
170
|
+
process.stderr.write(`Your login code is: ${start.user_code}\n`);
|
|
171
|
+
if (!flags["no-browser"]) {
|
|
172
|
+
openBrowser(start.verification_uri_complete);
|
|
173
|
+
process.stderr.write("Opening Primitive in your browser...\n");
|
|
174
|
+
}
|
|
175
|
+
process.stderr.write(`If the browser did not open, visit: ${start.verification_uri_complete}\n`);
|
|
176
|
+
process.stderr.write("Waiting for browser approval...\n");
|
|
177
|
+
const deadline = Date.now() + start.expires_in * 1000;
|
|
178
|
+
let interval = Math.min(Math.max(1, start.interval), MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
|
|
179
|
+
let nextPollDelay = 1;
|
|
180
|
+
while (Date.now() < deadline) {
|
|
181
|
+
await sleep(nextPollDelay * 1000);
|
|
182
|
+
nextPollDelay = interval;
|
|
183
|
+
const polled = await pollCliLogin({
|
|
184
|
+
body: { device_code: start.device_code },
|
|
185
|
+
client: apiClient.client,
|
|
186
|
+
responseStyle: "fields",
|
|
187
|
+
});
|
|
188
|
+
if (polled.data) {
|
|
189
|
+
const login = unwrapData(polled.data);
|
|
190
|
+
if (!login) {
|
|
191
|
+
throw cliError("Primitive API returned an empty CLI poll response.");
|
|
192
|
+
}
|
|
193
|
+
saveCliCredentials(this.config.configDir, {
|
|
194
|
+
api_key: login.api_key,
|
|
195
|
+
base_url: baseUrl,
|
|
196
|
+
created_at: new Date().toISOString(),
|
|
197
|
+
key_id: login.key_id,
|
|
198
|
+
key_prefix: login.key_prefix,
|
|
199
|
+
org_id: login.org_id,
|
|
200
|
+
org_name: login.org_name,
|
|
201
|
+
});
|
|
202
|
+
const org = login.org_name ? ` (${login.org_name})` : "";
|
|
203
|
+
process.stderr.write(`Logged in to org ${login.org_id}${org}.\n`);
|
|
204
|
+
process.stderr.write(`Saved credentials to ${credentialsPath(this.config.configDir)}.\n`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const payload = extractErrorPayload(polled.error);
|
|
208
|
+
const code = extractErrorCode(payload);
|
|
209
|
+
if (code === API_ERROR_CODES.authorizationPending) {
|
|
210
|
+
nextPollDelay = interval;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (code === API_ERROR_CODES.slowDown) {
|
|
214
|
+
interval = Math.min(retryAfterSeconds(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
|
|
215
|
+
nextPollDelay = interval;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (code === API_ERROR_CODES.accessDenied) {
|
|
219
|
+
throw cliError("Primitive CLI login was denied in the browser.");
|
|
220
|
+
}
|
|
221
|
+
if (code === API_ERROR_CODES.expiredToken) {
|
|
222
|
+
throw cliError("Primitive CLI login expired. Run `primitive login` again.");
|
|
223
|
+
}
|
|
224
|
+
if (code === API_ERROR_CODES.invalidDeviceCode) {
|
|
225
|
+
throw cliError("Primitive CLI login device code is invalid. Run `primitive login` again.");
|
|
226
|
+
}
|
|
227
|
+
writeErrorWithHints(payload);
|
|
228
|
+
throw cliError("Primitive CLI login failed while polling for approval.");
|
|
229
|
+
}
|
|
230
|
+
throw cliError("Primitive CLI login expired. Run `primitive login` again.");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export default LoginCommand;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command, Errors, Flags } from "@oclif/core";
|
|
2
|
+
import { cliLogout } from "../../api/generated/sdk.gen.js";
|
|
3
|
+
import { PrimitiveApiClient } from "../../api/index.js";
|
|
4
|
+
import { API_ERROR_CODES, extractErrorCode, extractErrorPayload, writeErrorWithHints, } from "../api-command.js";
|
|
5
|
+
import { acquireCliCredentialsLock, deleteCliCredentials, loadCliCredentials, normalizeBaseUrl, } from "../auth.js";
|
|
6
|
+
function cliError(message) {
|
|
7
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
8
|
+
}
|
|
9
|
+
function unwrapData(value) {
|
|
10
|
+
const envelope = value;
|
|
11
|
+
return envelope?.data ?? null;
|
|
12
|
+
}
|
|
13
|
+
class LogoutCommand extends Command {
|
|
14
|
+
static description = "Log out by revoking the saved Primitive CLI API key and deleting local credentials.";
|
|
15
|
+
static summary = "Log out and revoke the saved CLI key";
|
|
16
|
+
static examples = ["<%= config.bin %> logout"];
|
|
17
|
+
static flags = {
|
|
18
|
+
"base-url": Flags.string({
|
|
19
|
+
description: "Override the API base URL used for key revocation",
|
|
20
|
+
env: "PRIMITIVE_API_URL",
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { flags } = await this.parse(LogoutCommand);
|
|
25
|
+
let releaseCredentialsLock;
|
|
26
|
+
try {
|
|
27
|
+
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
31
|
+
throw cliError(detail);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await this.runWithCredentialLock(flags);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
releaseCredentialsLock();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async runWithCredentialLock(flags) {
|
|
41
|
+
let credentials;
|
|
42
|
+
try {
|
|
43
|
+
credentials = loadCliCredentials(this.config.configDir);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
deleteCliCredentials(this.config.configDir);
|
|
47
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
48
|
+
process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing API key was not revoked: ${detail}\n`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!credentials) {
|
|
53
|
+
throw cliError("Not logged in. Run `primitive login` to create saved CLI credentials.");
|
|
54
|
+
}
|
|
55
|
+
const baseUrl = flags["base-url"]
|
|
56
|
+
? normalizeBaseUrl(flags["base-url"])
|
|
57
|
+
: credentials.base_url;
|
|
58
|
+
const apiClient = new PrimitiveApiClient({
|
|
59
|
+
apiKey: credentials.api_key,
|
|
60
|
+
baseUrl,
|
|
61
|
+
});
|
|
62
|
+
const result = await cliLogout({
|
|
63
|
+
body: { key_id: credentials.key_id },
|
|
64
|
+
client: apiClient.client,
|
|
65
|
+
responseStyle: "fields",
|
|
66
|
+
});
|
|
67
|
+
if (result.error) {
|
|
68
|
+
const payload = extractErrorPayload(result.error);
|
|
69
|
+
const code = extractErrorCode(payload);
|
|
70
|
+
if (code === API_ERROR_CODES.unauthorized ||
|
|
71
|
+
code === API_ERROR_CODES.notFound) {
|
|
72
|
+
deleteCliCredentials(this.config.configDir);
|
|
73
|
+
writeErrorWithHints(payload);
|
|
74
|
+
process.stderr.write("Removed saved Primitive CLI credentials because the backing API key is already unavailable.\n");
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
writeErrorWithHints(payload);
|
|
79
|
+
throw cliError("Could not revoke the saved Primitive CLI API key.");
|
|
80
|
+
}
|
|
81
|
+
const logout = unwrapData(result.data);
|
|
82
|
+
deleteCliCredentials(this.config.configDir);
|
|
83
|
+
const keyId = logout?.key_id ?? credentials.key_id;
|
|
84
|
+
process.stderr.write(`Logged out and revoked API key ${keyId}.\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export default LogoutCommand;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command, Errors, Flags } from "@oclif/core";
|
|
2
2
|
import { listDomains, sendEmail } from "../../api/generated/sdk.gen.js";
|
|
3
3
|
import { PrimitiveApiClient } from "../../api/index.js";
|
|
4
|
-
import { extractErrorCode, extractErrorPayload, formatErrorPayload, writeErrorWithHints, } from "../api-command.js";
|
|
4
|
+
import { API_ERROR_CODES, extractErrorCode, extractErrorPayload, formatErrorPayload, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
|
|
5
|
+
import { resolveCliAuth } from "../auth.js";
|
|
5
6
|
// `primitive send` is the agent-grade shortcut for the most common
|
|
6
7
|
// case: send a fresh outbound email. It wraps `sending:send-email`
|
|
7
8
|
// with two ergonomic defaults that the underlying operation can't
|
|
@@ -53,7 +54,7 @@ function deriveSubject(body) {
|
|
|
53
54
|
function isVerifiedDomain(domain) {
|
|
54
55
|
return domain.is_active === true;
|
|
55
56
|
}
|
|
56
|
-
async function pickDefaultFromAddress(apiClient) {
|
|
57
|
+
async function pickDefaultFromAddress(apiClient, authFailureContext) {
|
|
57
58
|
const result = await listDomains({
|
|
58
59
|
client: apiClient.client,
|
|
59
60
|
responseStyle: "fields",
|
|
@@ -65,8 +66,12 @@ async function pickDefaultFromAddress(apiClient) {
|
|
|
65
66
|
// Surface the auth hint via writeErrorWithHints and bail with
|
|
66
67
|
// a focused message instead of the verbose "underlying error"
|
|
67
68
|
// wrapping.
|
|
68
|
-
if (extractErrorCode(errorPayload) ===
|
|
69
|
+
if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
|
|
69
70
|
writeErrorWithHints(errorPayload);
|
|
71
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
72
|
+
...authFailureContext,
|
|
73
|
+
payload: errorPayload,
|
|
74
|
+
});
|
|
70
75
|
// exit: 1 to match the run() unauthorized path (which uses
|
|
71
76
|
// `process.exitCode = 1`). oclif's CLIError defaults to 2,
|
|
72
77
|
// so without this override the same "unauthorized" condition
|
|
@@ -107,7 +112,7 @@ class SendCommand extends Command {
|
|
|
107
112
|
];
|
|
108
113
|
static flags = {
|
|
109
114
|
"api-key": Flags.string({
|
|
110
|
-
description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
|
|
115
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
111
116
|
env: "PRIMITIVE_API_KEY",
|
|
112
117
|
}),
|
|
113
118
|
"base-url": Flags.string({
|
|
@@ -139,43 +144,65 @@ class SendCommand extends Command {
|
|
|
139
144
|
"wait-timeout-ms": Flags.integer({
|
|
140
145
|
description: "Maximum time to wait when --wait is set. Defaults to 30000ms.",
|
|
141
146
|
}),
|
|
147
|
+
time: Flags.boolean({
|
|
148
|
+
description: TIME_FLAG_DESCRIPTION,
|
|
149
|
+
}),
|
|
142
150
|
};
|
|
143
151
|
async run() {
|
|
144
152
|
const { flags } = await this.parse(SendCommand);
|
|
145
153
|
if (!flags.body && !flags.html) {
|
|
146
154
|
throw new Errors.CLIError("Either --body or --html (or both) is required.");
|
|
147
155
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
156
|
+
await runWithTiming(flags.time, async () => {
|
|
157
|
+
const baseUrlOverridden = flags["base-url"] !== undefined;
|
|
158
|
+
const auth = resolveCliAuth({
|
|
159
|
+
apiKey: flags["api-key"],
|
|
160
|
+
baseUrl: flags["base-url"],
|
|
161
|
+
configDir: this.config.configDir,
|
|
162
|
+
});
|
|
163
|
+
const apiClient = new PrimitiveApiClient({
|
|
164
|
+
apiKey: auth.apiKey,
|
|
165
|
+
baseUrl: auth.baseUrl,
|
|
166
|
+
});
|
|
167
|
+
const authFailureContext = {
|
|
168
|
+
auth,
|
|
169
|
+
baseUrlOverridden,
|
|
170
|
+
configDir: this.config.configDir,
|
|
171
|
+
};
|
|
172
|
+
const from = flags.from ??
|
|
173
|
+
(await pickDefaultFromAddress(apiClient, authFailureContext));
|
|
174
|
+
const subject = flags.subject ?? (flags.body ? deriveSubject(flags.body) : "Message");
|
|
175
|
+
const result = await sendEmail({
|
|
176
|
+
body: {
|
|
177
|
+
from,
|
|
178
|
+
to: flags.to,
|
|
179
|
+
subject,
|
|
180
|
+
...(flags.body !== undefined ? { body_text: flags.body } : {}),
|
|
181
|
+
...(flags.html !== undefined ? { body_html: flags.html } : {}),
|
|
182
|
+
...(flags["in-reply-to"] !== undefined
|
|
183
|
+
? { in_reply_to: flags["in-reply-to"] }
|
|
184
|
+
: {}),
|
|
185
|
+
...(flags.wait !== undefined ? { wait: flags.wait } : {}),
|
|
186
|
+
...(flags["wait-timeout-ms"] !== undefined
|
|
187
|
+
? { wait_timeout_ms: flags["wait-timeout-ms"] }
|
|
188
|
+
: {}),
|
|
189
|
+
},
|
|
190
|
+
client: apiClient.client,
|
|
191
|
+
responseStyle: "fields",
|
|
192
|
+
});
|
|
193
|
+
if (result.error) {
|
|
194
|
+
const errorPayload = extractErrorPayload(result.error);
|
|
195
|
+
writeErrorWithHints(errorPayload);
|
|
196
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
197
|
+
...authFailureContext,
|
|
198
|
+
payload: errorPayload,
|
|
199
|
+
});
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const envelope = result.data;
|
|
204
|
+
this.log(JSON.stringify(envelope?.data ?? null, null, 2));
|
|
171
205
|
});
|
|
172
|
-
if (result.error) {
|
|
173
|
-
writeErrorWithHints(extractErrorPayload(result.error));
|
|
174
|
-
process.exitCode = 1;
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const envelope = result.data;
|
|
178
|
-
this.log(JSON.stringify(envelope?.data ?? null, null, 2));
|
|
179
206
|
}
|
|
180
207
|
}
|
|
181
208
|
export default SendCommand;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command, Errors, Flags } from "@oclif/core";
|
|
2
2
|
import { getAccount } from "../../api/generated/sdk.gen.js";
|
|
3
3
|
import { PrimitiveApiClient } from "../../api/index.js";
|
|
4
|
-
import { extractErrorPayload, writeErrorWithHints } from "../api-command.js";
|
|
4
|
+
import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
|
|
5
|
+
import { resolveCliAuth } from "../auth.js";
|
|
5
6
|
// `primitive whoami` is the credentials smoke-test the AGX
|
|
6
7
|
// walkthrough kept asking for. Before this command, a user with a
|
|
7
8
|
// suspect API key had no fast way to verify "is my key live and
|
|
@@ -21,47 +22,65 @@ class WhoamiCommand extends Command {
|
|
|
21
22
|
];
|
|
22
23
|
static flags = {
|
|
23
24
|
"api-key": Flags.string({
|
|
24
|
-
description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
|
|
25
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
25
26
|
env: "PRIMITIVE_API_KEY",
|
|
26
27
|
}),
|
|
27
28
|
"base-url": Flags.string({
|
|
28
29
|
description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
|
|
29
30
|
env: "PRIMITIVE_API_URL",
|
|
30
31
|
}),
|
|
32
|
+
time: Flags.boolean({
|
|
33
|
+
description: TIME_FLAG_DESCRIPTION,
|
|
34
|
+
}),
|
|
31
35
|
};
|
|
32
36
|
async run() {
|
|
33
37
|
const { flags } = await this.parse(WhoamiCommand);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
await runWithTiming(flags.time, async () => {
|
|
39
|
+
const baseUrlOverridden = flags["base-url"] !== undefined;
|
|
40
|
+
const auth = resolveCliAuth({
|
|
41
|
+
apiKey: flags["api-key"],
|
|
42
|
+
baseUrl: flags["base-url"],
|
|
43
|
+
configDir: this.config.configDir,
|
|
44
|
+
});
|
|
45
|
+
const apiClient = new PrimitiveApiClient({
|
|
46
|
+
apiKey: auth.apiKey,
|
|
47
|
+
baseUrl: auth.baseUrl,
|
|
48
|
+
});
|
|
49
|
+
const result = await getAccount({
|
|
50
|
+
client: apiClient.client,
|
|
51
|
+
responseStyle: "fields",
|
|
52
|
+
});
|
|
53
|
+
if (result.error) {
|
|
54
|
+
const errorPayload = extractErrorPayload(result.error);
|
|
55
|
+
writeErrorWithHints(errorPayload);
|
|
56
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
57
|
+
auth,
|
|
58
|
+
baseUrlOverridden,
|
|
59
|
+
configDir: this.config.configDir,
|
|
60
|
+
payload: errorPayload,
|
|
61
|
+
});
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const envelope = result.data;
|
|
66
|
+
const account = envelope?.data;
|
|
67
|
+
if (!account) {
|
|
68
|
+
process.stderr.write("Server returned an empty account body; this should not happen for a valid key.\n");
|
|
69
|
+
throw new Errors.CLIError("unexpected empty response");
|
|
70
|
+
}
|
|
71
|
+
// Concise human-readable summary on stderr; the full account
|
|
72
|
+
// JSON goes to stdout so a script can pipe it.
|
|
73
|
+
const onboarding = account.onboarding_completed === true
|
|
74
|
+
? "complete"
|
|
75
|
+
: account.onboarding_step
|
|
76
|
+
? `in progress (step: ${account.onboarding_step})`
|
|
77
|
+
: "incomplete";
|
|
78
|
+
process.stderr.write(`Authenticated as ${account.email}\n`);
|
|
79
|
+
process.stderr.write(` Account id: ${account.id}\n`);
|
|
80
|
+
process.stderr.write(` Plan: ${account.plan}\n`);
|
|
81
|
+
process.stderr.write(` Onboarding: ${onboarding}\n`);
|
|
82
|
+
this.log(JSON.stringify(account, null, 2));
|
|
41
83
|
});
|
|
42
|
-
if (result.error) {
|
|
43
|
-
writeErrorWithHints(extractErrorPayload(result.error));
|
|
44
|
-
process.exitCode = 1;
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const envelope = result.data;
|
|
48
|
-
const account = envelope?.data;
|
|
49
|
-
if (!account) {
|
|
50
|
-
process.stderr.write("Server returned an empty account body; this should not happen for a valid key.\n");
|
|
51
|
-
throw new Errors.CLIError("unexpected empty response");
|
|
52
|
-
}
|
|
53
|
-
// Concise human-readable summary on stderr; the full account
|
|
54
|
-
// JSON goes to stdout so a script can pipe it.
|
|
55
|
-
const onboarding = account.onboarding_completed === true
|
|
56
|
-
? "complete"
|
|
57
|
-
: account.onboarding_step
|
|
58
|
-
? `in progress (step: ${account.onboarding_step})`
|
|
59
|
-
: "incomplete";
|
|
60
|
-
process.stderr.write(`Authenticated as ${account.email}\n`);
|
|
61
|
-
process.stderr.write(` Account id: ${account.id}\n`);
|
|
62
|
-
process.stderr.write(` Plan: ${account.plan}\n`);
|
|
63
|
-
process.stderr.write(` Onboarding: ${onboarding}\n`);
|
|
64
|
-
this.log(JSON.stringify(account, null, 2));
|
|
65
84
|
}
|
|
66
85
|
}
|
|
67
86
|
export default WhoamiCommand;
|
|
@@ -73,7 +73,7 @@ export function renderFishCompletion(binName) {
|
|
|
73
73
|
]) {
|
|
74
74
|
lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l '${fishEscape(parameter.name.replace(/_/g, "-"))}' -r -d '${fishEscape(parameter.description ?? parameter.name)}'`);
|
|
75
75
|
}
|
|
76
|
-
lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'api-key' -r -d 'Primitive API key (defaults to PRIMITIVE_API_KEY)'`, `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'base-url' -r -d 'API base URL (defaults to PRIMITIVE_API_URL or production)'`);
|
|
76
|
+
lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'api-key' -r -d 'Primitive API key (defaults to PRIMITIVE_API_KEY or saved primitive login credentials)'`, `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'base-url' -r -d 'API base URL (defaults to PRIMITIVE_API_URL or production)'`);
|
|
77
77
|
if (operation.hasJsonBody) {
|
|
78
78
|
lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'body' -r -d 'JSON request body'`, `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'body-file' -r -d 'Path to a JSON file used as the request body'`);
|
|
79
79
|
}
|
package/dist/oclif/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import { Args, Command, Errors } from "@oclif/core";
|
|
|
2
2
|
import { operationManifest, } from "../openapi/index.js";
|
|
3
3
|
import { createOperationCommand } from "./api-command.js";
|
|
4
4
|
import EmailsLatestCommand from "./commands/emails-latest.js";
|
|
5
|
+
import LoginCommand from "./commands/login.js";
|
|
6
|
+
import LogoutCommand from "./commands/logout.js";
|
|
5
7
|
import SendCommand from "./commands/send.js";
|
|
6
8
|
import WhoamiCommand from "./commands/whoami.js";
|
|
7
9
|
import { renderFishCompletion } from "./fish-completion.js";
|
|
@@ -116,6 +118,10 @@ export const COMMANDS = {
|
|
|
116
118
|
// operation stays available under sending:send-email for callers
|
|
117
119
|
// who want every flag.
|
|
118
120
|
send: SendCommand,
|
|
121
|
+
// `login` creates and stores an org-scoped CLI API key via browser approval.
|
|
122
|
+
login: LoginCommand,
|
|
123
|
+
// `logout` revokes the saved CLI API key and removes local credentials.
|
|
124
|
+
logout: LogoutCommand,
|
|
119
125
|
// `whoami` is the credentials smoke test. Prints the account the
|
|
120
126
|
// current API key authenticates as. AGX walkthroughs kept
|
|
121
127
|
// wanting this before risking a real call against a possibly-
|