@primitivedotdev/cli 0.26.1 → 0.26.3

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.
@@ -1,238 +0,0 @@
1
- import { Command, Flags } from "@oclif/core";
2
- import { getEmail, PrimitiveApiClient, testFunction, } from "@primitivedotdev/sdk/api";
3
- import { API_BASE_URL_1_FLAG_DESCRIPTION, API_BASE_URL_2_FLAG_DESCRIPTION, baseUrlOverriddenFromFlags, extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
4
- import { resolveCliAuth } from "../auth.js";
5
- import { DEFAULT_EMAIL_POLL_INTERVAL_SECONDS, fetchEmailSearchPage, sleep, } from "./emails-poll.js";
6
- // `primitive functions:test-function` is the agent-grade shortcut for
7
- // triggering a real round-trip and (optionally) waiting for the
8
- // function to actually run before exiting. The underlying
9
- // `POST /functions/{id}/test` operation only kicks off a synthetic
10
- // inbound through MX and returns the queued send id; AGX walkthroughs
11
- // flagged the missing wait-and-show-sends step as the single biggest
12
- // time-sink in the verification loop.
13
- //
14
- // Shapes:
15
- // primitive functions:test-function --id <fn-id>
16
- // Fire-and-forget. Returns the TestInvocationResult JSON
17
- // (recipient, poll_since, watch_url). Same behavior as the
18
- // auto-generated functions:test-function it replaces.
19
- //
20
- // primitive functions:test-function --id <fn-id> --wait
21
- // Blocks until the test inbound has arrived AND the function's
22
- // webhook has fired (or --timeout elapses). Exits non-zero on
23
- // timeout or on exhausted retries.
24
- //
25
- // primitive functions:test-function --id <fn-id> --wait --show-sends
26
- // Same as --wait, plus prints the inbound's `replies` array
27
- // (every outbound the function emitted while processing the
28
- // test inbound), with each send's id, status, recipient,
29
- // subject, and queue id.
30
- //
31
- // The auto-generated functions:test-function entry is filtered out
32
- // of the generated-command set in oclif/index.ts so this hand-rolled
33
- // version owns the id.
34
- const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
35
- // Terminal states from the EmailWebhookStatus enum. `fired` means the
36
- // function returned 2xx; `exhausted` means all retries are spent and
37
- // the delivery is permanently failed. `pending` / `in_flight` /
38
- // `failed` are intermediate (`failed` is a temporary failure that may
39
- // retry into `fired` or eventually `exhausted`), so we keep polling.
40
- const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
41
- class FunctionsTestFunctionCommand extends Command {
42
- static description = "Send a real test email through MX to trigger this function. With --wait, blocks until the function has processed the inbound; with --show-sends, also prints any outbound sends the function emitted in response.";
43
- static summary = "Trigger a test invocation; with --wait, watch it land";
44
- static examples = [
45
- "<%= config.bin %> functions:test-function --id <fn-id>",
46
- "<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize",
47
- "<%= config.bin %> functions:test-function --id <fn-id> --wait --show-sends",
48
- "<%= config.bin %> functions:test-function --id <fn-id> --local-part summarize --wait --timeout 120",
49
- ];
50
- static flags = {
51
- "api-key": Flags.string({
52
- description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
53
- env: "PRIMITIVE_API_KEY",
54
- }),
55
- "api-base-url-1": Flags.string({
56
- description: API_BASE_URL_1_FLAG_DESCRIPTION,
57
- env: "PRIMITIVE_API_BASE_URL_1",
58
- hidden: true,
59
- }),
60
- "api-base-url-2": Flags.string({
61
- description: API_BASE_URL_2_FLAG_DESCRIPTION,
62
- env: "PRIMITIVE_API_BASE_URL_2",
63
- hidden: true,
64
- }),
65
- id: Flags.string({
66
- description: "Function id (UUID).",
67
- required: true,
68
- }),
69
- "local-part": Flags.string({
70
- description: "Override the synthetic local-part the test inbound is addressed to. Otherwise the runtime picks `__primitive_function_test+<random>`.",
71
- }),
72
- wait: Flags.boolean({
73
- description: "Block until the function has processed the test inbound (webhook status is `fired` or `exhausted`) or --timeout elapses. Exits non-zero on timeout or on exhausted retries.",
74
- }),
75
- "show-sends": Flags.boolean({
76
- description: "When the wait resolves, also print the outbound emails the function emitted while processing the test inbound (id, status, to, subject). Implies --wait.",
77
- }),
78
- timeout: Flags.integer({
79
- default: DEFAULT_WAIT_TIMEOUT_SECONDS,
80
- description: "Seconds to wait before exiting non-zero when --wait is set; 0 waits forever.",
81
- min: 0,
82
- }),
83
- "poll-interval": Flags.integer({
84
- default: DEFAULT_EMAIL_POLL_INTERVAL_SECONDS,
85
- description: "Seconds between polls while waiting.",
86
- min: 1,
87
- }),
88
- time: Flags.boolean({
89
- description: TIME_FLAG_DESCRIPTION,
90
- }),
91
- };
92
- async run() {
93
- const { flags } = await this.parse(FunctionsTestFunctionCommand);
94
- // --show-sends implies --wait. You can't print what was sent
95
- // until the function has actually run.
96
- const shouldWait = flags.wait || flags["show-sends"];
97
- const shouldShowSends = flags["show-sends"];
98
- const baseUrlOverridden = baseUrlOverriddenFromFlags(flags);
99
- const auth = resolveCliAuth({
100
- apiKey: flags["api-key"],
101
- apiBaseUrl1: flags["api-base-url-1"],
102
- apiBaseUrl2: flags["api-base-url-2"],
103
- configDir: this.config.configDir,
104
- });
105
- const apiClient = new PrimitiveApiClient({
106
- apiKey: auth.apiKey,
107
- apiBaseUrl1: auth.apiBaseUrl1,
108
- apiBaseUrl2: auth.apiBaseUrl2,
109
- });
110
- await runWithTiming(flags.time, async () => {
111
- // 1. Trigger the test send.
112
- const triggerResult = await testFunction({
113
- client: apiClient.client,
114
- path: { id: flags.id },
115
- body: flags["local-part"]
116
- ? { local_part: flags["local-part"] }
117
- : undefined,
118
- responseStyle: "fields",
119
- });
120
- if (triggerResult.error) {
121
- const payload = extractErrorPayload(triggerResult.error);
122
- writeErrorWithHints(payload);
123
- removeStaleSavedCredentialOnUnauthorized({
124
- auth,
125
- baseUrlOverridden,
126
- configDir: this.config.configDir,
127
- payload,
128
- });
129
- process.exitCode = 1;
130
- return;
131
- }
132
- const invocation = triggerResult.data
133
- .data;
134
- if (!shouldWait) {
135
- // Fire-and-forget path: print the TestInvocationResult JSON
136
- // unchanged. Same shape the auto-generated command emitted.
137
- this.log(JSON.stringify(invocation, null, 2));
138
- return;
139
- }
140
- const startedAt = Date.now();
141
- const timeoutMs = flags.timeout * 1000;
142
- const pollIntervalMs = flags["poll-interval"] * 1000;
143
- const isExpired = () => flags.timeout > 0 && Date.now() - startedAt > timeoutMs;
144
- // 2. Wait for the test inbound to arrive. The synthetic
145
- // recipient is unique per call (random suffix in the local-part
146
- // unless --local-part overrides), so `to` + `since` uniquely
147
- // identifies the test inbound row.
148
- this.log(`Waiting for test inbound to arrive at ${invocation.to}...`);
149
- let inboundId;
150
- while (!isExpired()) {
151
- const page = await fetchEmailSearchPage({
152
- apiClient,
153
- filters: { to: invocation.to },
154
- pageSize: 25,
155
- since: invocation.poll_since,
156
- });
157
- if (!page.ok) {
158
- const payload = extractErrorPayload(page.error);
159
- writeErrorWithHints(payload);
160
- removeStaleSavedCredentialOnUnauthorized({
161
- auth,
162
- baseUrlOverridden,
163
- configDir: this.config.configDir,
164
- payload,
165
- });
166
- process.exitCode = 1;
167
- return;
168
- }
169
- const found = page.rows[0];
170
- if (found) {
171
- inboundId = found.id;
172
- break;
173
- }
174
- await sleep(pollIntervalMs);
175
- }
176
- if (!inboundId) {
177
- this.error(`Timed out after ${flags.timeout}s waiting for test inbound ${invocation.to} to land. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
178
- }
179
- // 3. Wait for the function (webhook) to actually run. We poll
180
- // the email-detail endpoint because it already carries both the
181
- // webhook_status terminal state and the `replies` array we'll
182
- // print under --show-sends. No second endpoint needed.
183
- this.log(`Inbound landed (${inboundId}). Waiting for function to run...`);
184
- let detail;
185
- while (!isExpired()) {
186
- const result = await getEmail({
187
- client: apiClient.client,
188
- path: { id: inboundId },
189
- responseStyle: "fields",
190
- });
191
- if (result.error) {
192
- const payload = extractErrorPayload(result.error);
193
- writeErrorWithHints(payload);
194
- removeStaleSavedCredentialOnUnauthorized({
195
- auth,
196
- baseUrlOverridden,
197
- configDir: this.config.configDir,
198
- payload,
199
- });
200
- process.exitCode = 1;
201
- return;
202
- }
203
- const fetched = result.data.data;
204
- if (fetched.webhook_status &&
205
- TERMINAL_WEBHOOK_STATUSES.has(fetched.webhook_status)) {
206
- detail = fetched;
207
- break;
208
- }
209
- await sleep(pollIntervalMs);
210
- }
211
- if (!detail) {
212
- this.error(`Timed out after ${flags.timeout}s waiting for function webhook to fire for inbound ${inboundId}. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
213
- }
214
- // 4. Emit the outcome.
215
- const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
216
- const outcome = {
217
- function_id: flags.id,
218
- inbound_id: inboundId,
219
- inbound_to: invocation.to,
220
- webhook_status: detail.webhook_status,
221
- webhook_attempt_count: detail.webhook_attempt_count,
222
- webhook_last_status_code: detail.webhook_last_status_code,
223
- webhook_last_error: detail.webhook_last_error,
224
- elapsed_seconds: elapsedSeconds,
225
- };
226
- if (shouldShowSends) {
227
- outcome.sent_emails = detail.replies;
228
- }
229
- this.log(JSON.stringify(outcome, null, 2));
230
- // Exit non-zero when the function failed permanently so CI
231
- // scripts can gate on the exit code.
232
- if (detail.webhook_status === "exhausted") {
233
- process.exitCode = 1;
234
- }
235
- });
236
- }
237
- }
238
- export default FunctionsTestFunctionCommand;
@@ -1,236 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { hostname } from "node:os";
3
- import { Command, Errors, Flags } from "@oclif/core";
4
- import { getAccount, PrimitiveApiClient, pollCliLogin, startCliLogin, } from "@primitivedotdev/sdk/api";
5
- import { API_ERROR_CODES, extractErrorCode, extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
6
- import { acquireCliCredentialsLock, credentialsPath, loadCliCredentials, normalizeApiBaseUrl1, normalizeApiBaseUrl2, saveCliCredentials, } from "../auth.js";
7
- const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
8
- function cliError(message) {
9
- return new Errors.CLIError(message, { exit: 1 });
10
- }
11
- function sleep(ms) {
12
- return new Promise((resolve) => setTimeout(resolve, ms));
13
- }
14
- function openBrowser(url) {
15
- const command = process.platform === "darwin"
16
- ? "open"
17
- : process.platform === "win32"
18
- ? "cmd"
19
- : "xdg-open";
20
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
21
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
22
- child.on("error", () => undefined);
23
- child.unref();
24
- }
25
- function unwrapData(value) {
26
- const envelope = value;
27
- return envelope?.data ?? null;
28
- }
29
- function retryAfterSeconds(result) {
30
- const response = result.response;
31
- const raw = response?.headers.get("retry-after");
32
- if (!raw)
33
- return null;
34
- const parsed = Number.parseInt(raw, 10);
35
- return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
36
- }
37
- export async function checkExistingLogin(params) {
38
- const baseUrlOverridden = params.apiBaseUrl1 !== undefined;
39
- const probeApiBaseUrl1 = baseUrlOverridden
40
- ? normalizeApiBaseUrl1(params.apiBaseUrl1)
41
- : params.credentials.api_base_url_1;
42
- const apiClient = new PrimitiveApiClient({
43
- apiKey: params.credentials.api_key,
44
- apiBaseUrl1: probeApiBaseUrl1,
45
- });
46
- const result = await (params.checkAccount ??
47
- ((client) => getAccount({
48
- client: client.client,
49
- responseStyle: "fields",
50
- })))(apiClient);
51
- if (!result.error)
52
- return { status: "valid" };
53
- const payload = extractErrorPayload(result.error);
54
- const auth = {
55
- apiKey: params.credentials.api_key,
56
- apiBaseUrl1: probeApiBaseUrl1,
57
- // Host-2 isn't relevant to checkExistingLogin (login is on host-1
58
- // only), but the auth shape requires it. Use the default.
59
- apiBaseUrl2: normalizeApiBaseUrl2(undefined),
60
- credentials: params.credentials,
61
- source: "stored",
62
- };
63
- const removed = removeStaleSavedCredentialOnUnauthorized({
64
- auth,
65
- baseUrlOverridden,
66
- configDir: params.configDir,
67
- payload,
68
- });
69
- if (removed)
70
- return { status: "removed_stale" };
71
- const code = extractErrorCode(payload);
72
- return {
73
- status: "blocked",
74
- payload,
75
- message: code === API_ERROR_CODES.unauthorized
76
- ? "Saved Primitive CLI credentials were rejected. Run `primitive logout` to remove them before logging in again."
77
- : "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again.",
78
- };
79
- }
80
- class LoginCommand extends Command {
81
- static description = "Log in by opening Primitive in your browser and saving an org-scoped CLI API key locally.";
82
- static summary = "Log in with browser approval";
83
- static examples = [
84
- "<%= config.bin %> login",
85
- "<%= config.bin %> login --device-name work-laptop",
86
- "<%= config.bin %> login --force",
87
- ];
88
- static flags = {
89
- "api-base-url-1": Flags.string({
90
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
91
- env: "PRIMITIVE_API_BASE_URL_1",
92
- hidden: true,
93
- }),
94
- "device-name": Flags.string({
95
- description: "Device name shown in the browser approval screen",
96
- }),
97
- "no-browser": Flags.boolean({
98
- description: "Do not attempt to open the browser automatically",
99
- }),
100
- force: Flags.boolean({
101
- char: "f",
102
- description: "Replace saved credentials without first verifying the existing login",
103
- }),
104
- };
105
- async run() {
106
- const { flags } = await this.parse(LoginCommand);
107
- let releaseCredentialsLock;
108
- try {
109
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
110
- }
111
- catch (error) {
112
- const detail = error instanceof Error ? error.message : String(error);
113
- throw cliError(detail);
114
- }
115
- try {
116
- await this.runWithCredentialLock(flags);
117
- }
118
- finally {
119
- releaseCredentialsLock();
120
- }
121
- }
122
- async runWithCredentialLock(flags) {
123
- const apiBaseUrl1 = normalizeApiBaseUrl1(flags["api-base-url-1"]);
124
- let existing;
125
- try {
126
- existing = loadCliCredentials(this.config.configDir);
127
- }
128
- catch (error) {
129
- if (!flags.force)
130
- throw error;
131
- const detail = error instanceof Error ? error.message : String(error);
132
- process.stderr.write(`Replacing unreadable Primitive CLI credentials because --force was set: ${detail}\n`);
133
- existing = null;
134
- }
135
- if (existing && flags.force) {
136
- process.stderr.write("Replacing saved Primitive CLI credentials after browser approval because --force was set.\n");
137
- }
138
- else if (existing) {
139
- const existingStatus = await checkExistingLogin({
140
- apiBaseUrl1: flags["api-base-url-1"],
141
- configDir: this.config.configDir,
142
- credentials: existing,
143
- });
144
- if (existingStatus.status === "removed_stale") {
145
- process.stderr.write("Continuing with a new Primitive CLI login...\n");
146
- }
147
- else if (existingStatus.status === "blocked") {
148
- writeErrorWithHints(existingStatus.payload);
149
- throw cliError(existingStatus.message);
150
- }
151
- else {
152
- const org = existing.org_name ? ` for ${existing.org_name}` : "";
153
- throw cliError(`Already logged in${org}. Run \`primitive logout\` before logging in again.`);
154
- }
155
- }
156
- const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
157
- const deviceName = flags["device-name"] ?? hostname();
158
- const started = await startCliLogin({
159
- body: {
160
- device_name: deviceName,
161
- },
162
- client: apiClient.client,
163
- responseStyle: "fields",
164
- });
165
- if (started.error) {
166
- writeErrorWithHints(extractErrorPayload(started.error));
167
- throw cliError("Could not start Primitive CLI login.");
168
- }
169
- const start = unwrapData(started.data);
170
- if (!start) {
171
- throw cliError("Primitive API returned an empty CLI login response.");
172
- }
173
- process.stderr.write(`Your login code is: ${start.user_code}\n`);
174
- if (!flags["no-browser"]) {
175
- openBrowser(start.verification_uri_complete);
176
- process.stderr.write("Opening Primitive in your browser...\n");
177
- }
178
- process.stderr.write(`If the browser did not open, visit: ${start.verification_uri_complete}\n`);
179
- process.stderr.write("Waiting for browser approval...\n");
180
- const deadline = Date.now() + start.expires_in * 1000;
181
- let interval = Math.min(Math.max(1, start.interval), MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
182
- let nextPollDelay = 1;
183
- while (Date.now() < deadline) {
184
- await sleep(nextPollDelay * 1000);
185
- nextPollDelay = interval;
186
- const polled = await pollCliLogin({
187
- body: { device_code: start.device_code },
188
- client: apiClient.client,
189
- responseStyle: "fields",
190
- });
191
- if (polled.data) {
192
- const login = unwrapData(polled.data);
193
- if (!login) {
194
- throw cliError("Primitive API returned an empty CLI poll response.");
195
- }
196
- saveCliCredentials(this.config.configDir, {
197
- api_key: login.api_key,
198
- api_base_url_1: apiBaseUrl1,
199
- created_at: new Date().toISOString(),
200
- key_id: login.key_id,
201
- key_prefix: login.key_prefix,
202
- org_id: login.org_id,
203
- org_name: login.org_name,
204
- });
205
- const org = login.org_name ? ` (${login.org_name})` : "";
206
- process.stderr.write(`Logged in to org ${login.org_id}${org}.\n`);
207
- process.stderr.write(`Saved credentials to ${credentialsPath(this.config.configDir)}.\n`);
208
- return;
209
- }
210
- const payload = extractErrorPayload(polled.error);
211
- const code = extractErrorCode(payload);
212
- if (code === API_ERROR_CODES.authorizationPending) {
213
- nextPollDelay = interval;
214
- continue;
215
- }
216
- if (code === API_ERROR_CODES.slowDown) {
217
- interval = Math.min(retryAfterSeconds(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
218
- nextPollDelay = interval;
219
- continue;
220
- }
221
- if (code === API_ERROR_CODES.accessDenied) {
222
- throw cliError("Primitive CLI login was denied in the browser.");
223
- }
224
- if (code === API_ERROR_CODES.expiredToken) {
225
- throw cliError("Primitive CLI login expired. Run `primitive login` again.");
226
- }
227
- if (code === API_ERROR_CODES.invalidDeviceCode) {
228
- throw cliError("Primitive CLI login device code is invalid. Run `primitive login` again.");
229
- }
230
- writeErrorWithHints(payload);
231
- throw cliError("Primitive CLI login failed while polling for approval.");
232
- }
233
- throw cliError("Primitive CLI login expired. Run `primitive login` again.");
234
- }
235
- }
236
- export default LoginCommand;
@@ -1,87 +0,0 @@
1
- import { Command, Errors, Flags } from "@oclif/core";
2
- import { cliLogout, PrimitiveApiClient } from "@primitivedotdev/sdk/api";
3
- import { API_ERROR_CODES, extractErrorCode, extractErrorPayload, writeErrorWithHints, } from "../api-command.js";
4
- import { acquireCliCredentialsLock, deleteCliCredentials, loadCliCredentials, normalizeApiBaseUrl1, } from "../auth.js";
5
- function cliError(message) {
6
- return new Errors.CLIError(message, { exit: 1 });
7
- }
8
- function unwrapData(value) {
9
- const envelope = value;
10
- return envelope?.data ?? null;
11
- }
12
- class LogoutCommand extends Command {
13
- static description = "Log out by revoking the saved Primitive CLI API key and deleting local credentials.";
14
- static summary = "Log out and revoke the saved CLI key";
15
- static examples = ["<%= config.bin %> logout"];
16
- static flags = {
17
- "api-base-url-1": Flags.string({
18
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19
- env: "PRIMITIVE_API_BASE_URL_1",
20
- hidden: true,
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 apiBaseUrl1 = flags["api-base-url-1"]
56
- ? normalizeApiBaseUrl1(flags["api-base-url-1"])
57
- : credentials.api_base_url_1;
58
- const apiClient = new PrimitiveApiClient({
59
- apiKey: credentials.api_key,
60
- apiBaseUrl1,
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;