@runtypelabs/cli 2.59.1 → 2.59.2
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/dist/chunk-0-T9NWjH.js +3899 -0
- package/dist/chunk-0x7UKC1U.js +17 -0
- package/dist/chunk-2ySxXtcg.js +418 -0
- package/dist/chunk-3An_BRHv.js +501 -0
- package/dist/chunk-6uTxgdiY.js +16218 -0
- package/dist/chunk-87nRnNRG.js +1363 -0
- package/dist/chunk-B3wo8uRx.js +322 -0
- package/dist/chunk-BGp3ZdBy.js +560 -0
- package/dist/chunk-BJQv9Elp.js +107 -0
- package/dist/chunk-BOpO6xE-.js +6053 -0
- package/dist/chunk-BQXXCL8B.js +102 -0
- package/dist/chunk-BV-8isOd.js +34 -0
- package/dist/chunk-BVMhjGVe.js +826 -0
- package/dist/chunk-BW2fg46T.js +141 -0
- package/dist/chunk-BWJdY5Ag.js +36 -0
- package/dist/chunk-BYJOrLhh.js +302 -0
- package/dist/chunk-BYab8EVL.js +127 -0
- package/dist/chunk-BfEKvp7h.js +82 -0
- package/dist/chunk-BhgLSfH6.js +1123 -0
- package/dist/chunk-BirUxTtH.js +296 -0
- package/dist/chunk-BmB6UVZ9.js +1559 -0
- package/dist/chunk-BnWhIqW1.js +109 -0
- package/dist/chunk-Bpfnfk0t.js +123 -0
- package/dist/chunk-BrhgdnM3.js +108 -0
- package/dist/chunk-Bt0XAgX5.js +327 -0
- package/dist/chunk-Bu0FJr6S.js +84 -0
- package/dist/chunk-BvEoKpN3.js +255 -0
- package/dist/chunk-BxNJgYbE.js +28 -0
- package/dist/chunk-C1OFhd7F.js +6131 -0
- package/dist/chunk-C7e7WwbU.js +331 -0
- package/dist/chunk-CDTYP6bN.js +558 -0
- package/dist/chunk-CGDQoqDC.js +24 -0
- package/dist/chunk-CJraEBVd.js +127 -0
- package/dist/chunk-CQrNhOtv.js +40 -0
- package/dist/chunk-CW6AzBH6.js +128 -0
- package/dist/chunk-CXgL4w-9.js +317 -0
- package/dist/chunk-CZrqoPyf.js +233 -0
- package/dist/chunk-CcFCyqbB.js +83 -0
- package/dist/chunk-CdaIvO-y.js +485 -0
- package/dist/chunk-CehSXtgK.js +624 -0
- package/dist/chunk-Ciqkop65.js +218 -0
- package/dist/chunk-CoAWdSiE.js +43 -0
- package/dist/chunk-Crb4y7t4.js +830 -0
- package/dist/chunk-CrlqIfmb.js +337 -0
- package/dist/chunk-Cs4pwCQ-.js +324 -0
- package/dist/chunk-CwM_9y01.js +519 -0
- package/dist/chunk-CyRbIJg8.js +440 -0
- package/dist/chunk-D0OHsyMy.js +212 -0
- package/dist/chunk-D3y2bURs.js +1347 -0
- package/dist/chunk-D4_mJk03.js +2338 -0
- package/dist/chunk-D6C8rned.js +578 -0
- package/dist/chunk-D9ZbUnks.js +11 -0
- package/dist/chunk-DEPmLshX.js +834 -0
- package/dist/chunk-DFj1UPB2.js +330 -0
- package/dist/chunk-DGdFdZVf.js +197 -0
- package/dist/chunk-DGv7X9S_.js +63 -0
- package/dist/chunk-DJQ47WYZ.js +664 -0
- package/dist/chunk-DJw3O61j.js +1974 -0
- package/dist/chunk-DK4FbWVK.js +100 -0
- package/dist/chunk-DSRYhUus.js +993 -0
- package/dist/chunk-DajZ9Eg4.js +299 -0
- package/dist/chunk-Df4dunvU.js +195 -0
- package/dist/chunk-DjNTPhfO.js +481 -0
- package/dist/chunk-Dn8eaJuu.js +1042 -0
- package/dist/chunk-DwDWqSxM.js +637 -0
- package/dist/chunk-Dz7cEtxl.js +84 -0
- package/dist/chunk-E_0bgjrS.js +359 -0
- package/dist/chunk-KPYN2vUp.js +776 -0
- package/dist/chunk-KcqPQJBi.js +86 -0
- package/dist/chunk-N9H0JnoG.js +349 -0
- package/dist/chunk-NONFcXkn.js +728 -0
- package/dist/chunk-PrFjcpmH.js +256 -0
- package/dist/chunk-QSfNYiw9.js +11 -0
- package/dist/chunk-R265bzj9.js +226 -0
- package/dist/chunk-X9V_r6jM.js +166 -0
- package/dist/chunk-ZBtPUtyA.js +55 -0
- package/dist/chunk-ZCTfmOs7.js +4939 -0
- package/dist/chunk-h6oavj0t.js +97 -0
- package/dist/chunk-i7DwkvvD.js +42 -0
- package/dist/chunk-mG-4eZ6S.js +203 -0
- package/dist/chunk-o0ygUbCf.js +313 -0
- package/dist/chunk-ucu5lrDZ.js +5715 -0
- package/dist/index.js +11367 -73789
- package/package.json +4 -4
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { b as CredentialStore, c as loadEffectiveContext, i as assertExpectedApiUrl, l as normalizeApiUrl, n as CliContextError, s as getInvocationOptions, t as isTTY } from "./index.js";
|
|
3
|
+
import { EntityCard, MutationResult, React, loadUi, mutationResultProps, render, useEffect, useMutationState, useState } from "./chunk-CoAWdSiE.js";
|
|
4
|
+
import { i as getApiUrl, n as ApiKeyValidationUnavailableError, r as createCliClient } from "./chunk-BW2fg46T.js";
|
|
5
|
+
import { n as browserLogin, r as orgMismatchWarning, t as apiKeyLogin } from "./chunk-BnWhIqW1.js";
|
|
6
|
+
import { n as ensureAuth, r as getAuthenticatedUser, t as authRequiredMessage } from "./chunk-CJraEBVd.js";
|
|
7
|
+
import { a as printJson } from "./chunk-CcFCyqbB.js";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
// src/auth/agent-auth-client.ts
|
|
11
|
+
/**
|
|
12
|
+
* Protocol error with the spec error code and an agent-actionable hint.
|
|
13
|
+
* `hint` is safe to surface verbatim in JSON output as the next step.
|
|
14
|
+
*/
|
|
15
|
+
var AgentAuthError = class extends Error {
|
|
16
|
+
code;
|
|
17
|
+
status;
|
|
18
|
+
hint;
|
|
19
|
+
constructor(code, status, message, hint) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.hint = hint;
|
|
24
|
+
this.name = "AgentAuthError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const ERROR_HINTS = {
|
|
28
|
+
email_already_registered: "This email already has a Runtype account, so it cannot claim a new agent account. Ask the user to sign in at the dashboard, create an API key (Settings > API keys), and give it to you. Then run: runtype auth login --api-key <key> --no-tty",
|
|
29
|
+
invalid_claim_token: "The claim token is invalid or expired. Run: runtype auth register --force",
|
|
30
|
+
claim_expired: "The claim window expired. Run: runtype auth register --force",
|
|
31
|
+
credential_expired: "The temporary credential expired. Run: runtype auth register --force",
|
|
32
|
+
claimed_or_in_flight: "A claim is already in progress. Ask the user for the 6-digit code from their email, then run: runtype auth verify <code>. If the code expired, re-run: runtype auth claim <email>",
|
|
33
|
+
otp_invalid: "That code is not correct. Ask the user to re-check the 6-digit code from the most recent Runtype email and run: runtype auth verify <code>",
|
|
34
|
+
otp_expired: "The code expired. Re-send it with: runtype auth claim <email>",
|
|
35
|
+
previously_claimed: "This registration was already claimed. Run: runtype auth status to inspect stored credentials.",
|
|
36
|
+
email_send_failed: "The verification email could not be sent. Retry: runtype auth claim <email>",
|
|
37
|
+
rate_limited: "Too many attempts. Wait a minute, then retry the same command.",
|
|
38
|
+
service_disabled: "Agent registration is temporarily disabled. Try again later."
|
|
39
|
+
};
|
|
40
|
+
async function postJson(apiUrl, endpoint, body) {
|
|
41
|
+
assertExpectedApiUrl(apiUrl);
|
|
42
|
+
if (getInvocationOptions().expectOrg !== void 0) throw new CliContextError("--expect-org is not supported for anonymous registration or claim/verify actions.");
|
|
43
|
+
const url = `${apiUrl}${endpoint}`;
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"content-type": "application/json",
|
|
50
|
+
"user-agent": "runtype-cli"
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(body)
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new AgentAuthError("network_error", 0, `Could not reach ${url}: ${error instanceof Error ? error.message : String(error)}`, "Check network access and the --api-url value, then retry the same command.");
|
|
56
|
+
}
|
|
57
|
+
let data = {};
|
|
58
|
+
try {
|
|
59
|
+
data = await response.json();
|
|
60
|
+
} catch {}
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const code = typeof data.error === "string" ? data.error : `http_${response.status}`;
|
|
63
|
+
const message = typeof data.message === "string" ? data.message : `Request failed (${response.status})`;
|
|
64
|
+
throw new AgentAuthError(code, response.status, message, ERROR_HINTS[code]);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
status: response.status,
|
|
68
|
+
data
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function requireString(data, key) {
|
|
72
|
+
const value = data[key];
|
|
73
|
+
if (typeof value !== "string" || value.length === 0) throw new AgentAuthError("invalid_response", 0, `Registration response is missing "${key}". The API may be an incompatible version.`);
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
function stringArray(value) {
|
|
77
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
78
|
+
}
|
|
79
|
+
/** POST /agent/auth — anonymous registration returning a temporary credential + claim token. */
|
|
80
|
+
async function registerAnonymousAgent(apiUrl) {
|
|
81
|
+
const { data } = await postJson(apiUrl || getApiUrl(), "/agent/auth", {
|
|
82
|
+
type: "anonymous",
|
|
83
|
+
requested_credential_type: "api_key"
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
registrationId: requireString(data, "registration_id"),
|
|
87
|
+
credential: requireString(data, "credential"),
|
|
88
|
+
credentialExpires: requireString(data, "credential_expires"),
|
|
89
|
+
scopes: stringArray(data.scopes),
|
|
90
|
+
claimToken: requireString(data, "claim_token"),
|
|
91
|
+
claimTokenExpires: requireString(data, "claim_token_expires"),
|
|
92
|
+
postClaimScopes: stringArray(data.post_claim_scopes)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** POST /agent/auth/claim — email the user a 6-digit code that binds the account to them. */
|
|
96
|
+
async function initiateClaim(claimToken, email, apiUrl) {
|
|
97
|
+
const { data } = await postJson(apiUrl || getApiUrl(), "/agent/auth/claim", {
|
|
98
|
+
claim_token: claimToken,
|
|
99
|
+
email
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
registrationId: requireString(data, "registration_id"),
|
|
103
|
+
status: typeof data.status === "string" ? data.status : "initiated",
|
|
104
|
+
otpExpiresAt: requireString(data, "expires_at")
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** POST /agent/auth/claim/complete — exchange the OTP for a fresh full-access credential. */
|
|
108
|
+
async function completeClaim(claimToken, otp, apiUrl) {
|
|
109
|
+
const { data } = await postJson(apiUrl || getApiUrl(), "/agent/auth/claim/complete", {
|
|
110
|
+
claim_token: claimToken,
|
|
111
|
+
otp
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
registrationId: requireString(data, "registration_id"),
|
|
115
|
+
credential: requireString(data, "credential"),
|
|
116
|
+
scopes: stringArray(data.scopes)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// src/commands/auth-agent.ts
|
|
120
|
+
const VERIFY_NEXT = "Ask the user for the 6-digit code from their email, then run: runtype auth verify <code>";
|
|
121
|
+
function claimNext(email) {
|
|
122
|
+
return email ? `Send the verification email with: runtype auth claim ${email}` : "Ask the user for their email address, then run: runtype auth claim <email>";
|
|
123
|
+
}
|
|
124
|
+
function fail(payload) {
|
|
125
|
+
console.error(JSON.stringify(payload, null, 2));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
function failFromError(error, fallbackNext) {
|
|
129
|
+
if (error instanceof AgentAuthError) fail({
|
|
130
|
+
status: "error",
|
|
131
|
+
code: error.code,
|
|
132
|
+
error: error.message,
|
|
133
|
+
...error.hint || fallbackNext ? { next: error.hint || fallbackNext } : {}
|
|
134
|
+
});
|
|
135
|
+
fail({
|
|
136
|
+
status: "error",
|
|
137
|
+
error: error instanceof Error ? error.message : String(error),
|
|
138
|
+
...fallbackNext ? { next: fallbackNext } : {}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function fetchIdentity(apiKey, apiUrl) {
|
|
142
|
+
const profile = await createCliClient(apiKey, apiUrl).get("/users/profile");
|
|
143
|
+
return {
|
|
144
|
+
userId: typeof profile.id === "string" ? profile.id : "unknown",
|
|
145
|
+
orgId: typeof profile.orgId === "string" ? profile.orgId : void 0,
|
|
146
|
+
email: typeof profile.email === "string" ? profile.email : void 0
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function registerAction(options) {
|
|
150
|
+
const store = new CredentialStore();
|
|
151
|
+
const apiUrl = options.apiUrl || getApiUrl();
|
|
152
|
+
const existingClaim = await store.getClaimState();
|
|
153
|
+
const hasCredentials = await store.hasCredentials();
|
|
154
|
+
if (existingClaim && !options.force) {
|
|
155
|
+
const resumeDeadline = existingClaim.claimTokenExpires ?? existingClaim.credentialExpires;
|
|
156
|
+
if (!(resumeDeadline && new Date(resumeDeadline) < /* @__PURE__ */ new Date())) {
|
|
157
|
+
const payload = {
|
|
158
|
+
status: "registration_pending",
|
|
159
|
+
registrationId: existingClaim.registrationId,
|
|
160
|
+
credentialExpires: existingClaim.credentialExpires,
|
|
161
|
+
next: existingClaim.claimEmail ? VERIFY_NEXT : claimNext(options.email)
|
|
162
|
+
};
|
|
163
|
+
if (existingClaim.claimEmail) payload.claimEmail = existingClaim.claimEmail;
|
|
164
|
+
printJson(payload);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (hasCredentials && !existingClaim && !options.force) fail({
|
|
169
|
+
status: "error",
|
|
170
|
+
code: "already_authenticated",
|
|
171
|
+
error: "Credentials are already stored for a claimed account.",
|
|
172
|
+
next: "Run: runtype auth whoami --no-tty to confirm, or runtype auth logout first if the user wants a different account."
|
|
173
|
+
});
|
|
174
|
+
let registration;
|
|
175
|
+
try {
|
|
176
|
+
registration = await registerAnonymousAgent(apiUrl);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
failFromError(error, "Retry: runtype auth register");
|
|
179
|
+
}
|
|
180
|
+
let identity = { userId: "unknown" };
|
|
181
|
+
try {
|
|
182
|
+
identity = await fetchIdentity(registration.credential, apiUrl);
|
|
183
|
+
} catch {}
|
|
184
|
+
const claimState = {
|
|
185
|
+
claimToken: registration.claimToken,
|
|
186
|
+
registrationId: registration.registrationId,
|
|
187
|
+
credentialExpires: registration.credentialExpires,
|
|
188
|
+
claimTokenExpires: registration.claimTokenExpires,
|
|
189
|
+
apiUrl
|
|
190
|
+
};
|
|
191
|
+
const registered = await store.saveCredentials({
|
|
192
|
+
apiKey: registration.credential,
|
|
193
|
+
userId: identity.userId,
|
|
194
|
+
orgId: identity.orgId,
|
|
195
|
+
apiUrl
|
|
196
|
+
}, claimState);
|
|
197
|
+
const base = {
|
|
198
|
+
status: "registered",
|
|
199
|
+
accountState: "temporary",
|
|
200
|
+
registrationId: registration.registrationId,
|
|
201
|
+
userId: identity.userId,
|
|
202
|
+
credentialExpires: registration.credentialExpires,
|
|
203
|
+
note: "A temporary account is active: CLI commands work now with limited models and quota. Verifying an email upgrades it to a full account and replaces the stored credential automatically."
|
|
204
|
+
};
|
|
205
|
+
if (options.email) try {
|
|
206
|
+
const claim = await initiateClaim(registration.claimToken, options.email, apiUrl);
|
|
207
|
+
await store.saveClaimState({
|
|
208
|
+
...claimState,
|
|
209
|
+
claimEmail: options.email,
|
|
210
|
+
accountId: registered.id
|
|
211
|
+
});
|
|
212
|
+
printJson({
|
|
213
|
+
...base,
|
|
214
|
+
status: "otp_sent",
|
|
215
|
+
claimEmail: options.email,
|
|
216
|
+
otpExpiresAt: claim.otpExpiresAt,
|
|
217
|
+
next: VERIFY_NEXT
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error instanceof AgentAuthError) fail({
|
|
222
|
+
...base,
|
|
223
|
+
status: "registered_claim_failed",
|
|
224
|
+
code: error.code,
|
|
225
|
+
error: error.message,
|
|
226
|
+
next: error.hint || claimNext(options.email)
|
|
227
|
+
});
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
printJson({
|
|
231
|
+
...base,
|
|
232
|
+
next: claimNext()
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async function claimAction(email, options) {
|
|
236
|
+
const store = new CredentialStore();
|
|
237
|
+
const claimState = await store.getClaimState();
|
|
238
|
+
const apiUrl = options.apiUrl || claimState?.apiUrl || (await store.getCredentials())?.apiUrl || getApiUrl();
|
|
239
|
+
if (!claimState) {
|
|
240
|
+
const hasCredentials = await store.hasCredentials();
|
|
241
|
+
fail({
|
|
242
|
+
status: "error",
|
|
243
|
+
code: "no_pending_registration",
|
|
244
|
+
error: hasCredentials ? "Stored credentials are already claimed; there is nothing to claim." : "No registration found.",
|
|
245
|
+
next: hasCredentials ? "Run: runtype auth whoami --no-tty" : `Run: runtype auth register --email ${email}`
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const claim = await initiateClaim(claimState.claimToken, email, apiUrl);
|
|
250
|
+
await store.saveClaimState({
|
|
251
|
+
...claimState,
|
|
252
|
+
claimEmail: email
|
|
253
|
+
});
|
|
254
|
+
printJson({
|
|
255
|
+
status: "otp_sent",
|
|
256
|
+
registrationId: claim.registrationId,
|
|
257
|
+
claimEmail: email,
|
|
258
|
+
otpExpiresAt: claim.otpExpiresAt,
|
|
259
|
+
next: VERIFY_NEXT
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
failFromError(error, `Retry: runtype auth claim ${email}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function verifyAction(code, options) {
|
|
266
|
+
const store = new CredentialStore();
|
|
267
|
+
const claimState = await store.getClaimState();
|
|
268
|
+
const apiUrl = options.apiUrl || claimState?.apiUrl || (await store.getCredentials())?.apiUrl || getApiUrl();
|
|
269
|
+
if (!claimState) fail({
|
|
270
|
+
status: "error",
|
|
271
|
+
code: "no_pending_registration",
|
|
272
|
+
error: "No claim in progress.",
|
|
273
|
+
next: "Run: runtype auth register --email <email> first."
|
|
274
|
+
});
|
|
275
|
+
if (!claimState.claimEmail) fail({
|
|
276
|
+
status: "error",
|
|
277
|
+
code: "claim_not_initiated",
|
|
278
|
+
error: "No verification email has been sent yet, so there is no code to verify.",
|
|
279
|
+
next: claimNext()
|
|
280
|
+
});
|
|
281
|
+
let completed;
|
|
282
|
+
try {
|
|
283
|
+
completed = await completeClaim(claimState.claimToken, code.trim(), apiUrl);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
failFromError(error);
|
|
286
|
+
}
|
|
287
|
+
let identity = { userId: "unknown" };
|
|
288
|
+
try {
|
|
289
|
+
identity = await fetchIdentity(completed.credential, apiUrl);
|
|
290
|
+
} catch {}
|
|
291
|
+
const preClaimAccountId = claimState.accountId;
|
|
292
|
+
const wasDefault = (await store.getDefaultAccount())?.id === preClaimAccountId;
|
|
293
|
+
const saved = await store.saveCredentials({
|
|
294
|
+
apiKey: completed.credential,
|
|
295
|
+
userId: identity.userId,
|
|
296
|
+
orgId: identity.orgId,
|
|
297
|
+
apiUrl
|
|
298
|
+
}, void 0, { makeDefault: wasDefault });
|
|
299
|
+
if (preClaimAccountId && preClaimAccountId !== saved.id) await store.removeAccount(preClaimAccountId);
|
|
300
|
+
printJson({
|
|
301
|
+
status: "authenticated",
|
|
302
|
+
accountState: "full",
|
|
303
|
+
registrationId: completed.registrationId,
|
|
304
|
+
userId: identity.userId,
|
|
305
|
+
email: identity.email ?? claimState.claimEmail,
|
|
306
|
+
note: "The account is fully activated and the stored credential was rotated. All limits from the temporary account are removed.",
|
|
307
|
+
next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami --no-tty)"
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function registerAgentAuthCommands(authCommand) {
|
|
311
|
+
authCommand.command("register").description("Create a Runtype account without a browser (agent signup, step 1 of 2)").option("--email <email>", "Also send the verification email in the same step").option("--force", "Register a new account even if one is stored or pending").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(registerAction);
|
|
312
|
+
authCommand.command("claim <email>").description("Email the user a 6-digit code to attach the account to them").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(claimAction);
|
|
313
|
+
authCommand.command("verify <code>").description("Complete signup with the 6-digit code from the verification email (step 2 of 2)").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(verifyAction);
|
|
314
|
+
}
|
|
315
|
+
// src/commands/auth.ts
|
|
316
|
+
const authCommand = new Command("auth").description("Manage authentication");
|
|
317
|
+
const NON_TTY_LOGIN_GUIDANCE = {
|
|
318
|
+
status: "error",
|
|
319
|
+
code: "tty_required",
|
|
320
|
+
error: "Browser login needs an interactive terminal, which this environment does not have.",
|
|
321
|
+
next: ["New account (no browser needed): ask the user for their email, then run: runtype auth register --email <email> — a 6-digit code is emailed to them; finish with: runtype auth verify <code>", "Existing account: ask the user for an API key from the dashboard (Settings > API keys), then run: runtype auth login --api-key <key>"]
|
|
322
|
+
};
|
|
323
|
+
const API_UNREACHABLE_NEXT = ["Confirm this environment can reach the Runtype API directly over HTTPS.", "If egress is restricted, allowlist api.runtype.com (staging: api.runtype-staging.com), then re-run: runtype auth login --api-key <key>"];
|
|
324
|
+
/**
|
|
325
|
+
* Forcing --tty from a non-terminal cannot make browser login work.
|
|
326
|
+
* The flag only switches output rendering; the OAuth callback still cannot complete here.
|
|
327
|
+
* Observed in the field: `auth login --tty` hangs until the harness kills it.
|
|
328
|
+
* Refuse up front with the same guidance instead of hanging.
|
|
329
|
+
*/
|
|
330
|
+
function refuseForcedTtyLogin(options) {
|
|
331
|
+
if (options.tty === true && process.stdout.isTTY !== true) {
|
|
332
|
+
console.error(JSON.stringify({
|
|
333
|
+
...NON_TTY_LOGIN_GUIDANCE,
|
|
334
|
+
error: "Browser login needs a real interactive terminal. Forcing --tty from a non-terminal only changes output formatting; the browser OAuth callback still cannot complete here, so the command would hang."
|
|
335
|
+
}, null, 2));
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
async function getBillingInfo(apiKey, apiUrl) {
|
|
341
|
+
try {
|
|
342
|
+
return await createCliClient(apiKey, apiUrl).get("/billing/status");
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/** Run one async step under the shared spinner/result chrome. */
|
|
348
|
+
async function renderTask(loadingLabel, task) {
|
|
349
|
+
await loadUi();
|
|
350
|
+
const App = () => {
|
|
351
|
+
const [label, setLabel] = useState(loadingLabel);
|
|
352
|
+
const [message, setMessage] = useState("");
|
|
353
|
+
const mutation = useMutationState();
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
task(setLabel).then((value) => {
|
|
356
|
+
setMessage(value.message);
|
|
357
|
+
mutation.succeed(value.result);
|
|
358
|
+
}).catch((err) => mutation.fail(err));
|
|
359
|
+
}, []);
|
|
360
|
+
return React.createElement(MutationResult, {
|
|
361
|
+
...mutationResultProps(mutation.state),
|
|
362
|
+
loadingLabel: label,
|
|
363
|
+
successMessage: message
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
const { waitUntilExit } = render(React.createElement(App));
|
|
367
|
+
await waitUntilExit();
|
|
368
|
+
}
|
|
369
|
+
function loginFields(login) {
|
|
370
|
+
return [
|
|
371
|
+
{
|
|
372
|
+
label: "Organization",
|
|
373
|
+
value: login.orgId ?? "Personal"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
label: "User ID",
|
|
377
|
+
value: login.userId
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
label: "API URL",
|
|
381
|
+
value: login.apiUrl
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
label: "Default login",
|
|
385
|
+
value: login.isDefault ? "yes" : "no (run: runtype auth switch)"
|
|
386
|
+
}
|
|
387
|
+
];
|
|
388
|
+
}
|
|
389
|
+
function loginJson(login) {
|
|
390
|
+
const warning = orgMismatchWarning(login);
|
|
391
|
+
return {
|
|
392
|
+
status: "authenticated",
|
|
393
|
+
userId: login.userId,
|
|
394
|
+
orgId: login.orgId ?? null,
|
|
395
|
+
apiUrl: login.apiUrl,
|
|
396
|
+
accountId: login.account.id,
|
|
397
|
+
isDefault: login.isDefault,
|
|
398
|
+
secretStore: login.account.secretStore,
|
|
399
|
+
...warning ? { warning } : {}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function loginOutcome(login) {
|
|
403
|
+
return {
|
|
404
|
+
message: orgMismatchWarning(login) ?? `Logged in to ${login.orgId ?? "your personal account"}.`,
|
|
405
|
+
result: React.createElement(EntityCard, { fields: loginFields(login) })
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
authCommand.command("signup").description("Create a new Runtype account (alias for \"auth login\")").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
|
|
409
|
+
await loadEffectiveContext(options.apiUrl);
|
|
410
|
+
refuseForcedTtyLogin(options);
|
|
411
|
+
if (!isTTY(options)) {
|
|
412
|
+
console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
await renderTask("Opening browser for authentication...", async (setLabel) => loginOutcome(await browserLogin({
|
|
416
|
+
...options,
|
|
417
|
+
onProgress: setLabel
|
|
418
|
+
})));
|
|
419
|
+
});
|
|
420
|
+
function failApiKeyLogin(error) {
|
|
421
|
+
if (error instanceof ApiKeyValidationUnavailableError) {
|
|
422
|
+
console.error(JSON.stringify({
|
|
423
|
+
status: "error",
|
|
424
|
+
code: "api_unreachable",
|
|
425
|
+
error: error.message,
|
|
426
|
+
next: API_UNREACHABLE_NEXT
|
|
427
|
+
}, null, 2));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
431
|
+
console.error(JSON.stringify({
|
|
432
|
+
status: "error",
|
|
433
|
+
error: message
|
|
434
|
+
}, null, 2));
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
async function loginAction(options) {
|
|
438
|
+
await loadEffectiveContext(options.apiUrl);
|
|
439
|
+
if (options.apiKey) {
|
|
440
|
+
const apiKey = options.apiKey;
|
|
441
|
+
if (!isTTY(options)) {
|
|
442
|
+
let login;
|
|
443
|
+
try {
|
|
444
|
+
login = await apiKeyLogin(apiKey, { apiUrl: options.apiUrl });
|
|
445
|
+
} catch (error) {
|
|
446
|
+
failApiKeyLogin(error);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
console.log(JSON.stringify({
|
|
450
|
+
...loginJson(login),
|
|
451
|
+
next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami)"
|
|
452
|
+
}, null, 2));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
await renderTask("Validating API key...", async () => loginOutcome(await apiKeyLogin(apiKey, { apiUrl: options.apiUrl })));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
refuseForcedTtyLogin(options);
|
|
459
|
+
if (!isTTY(options)) {
|
|
460
|
+
console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
await renderTask("Initializing authentication...", async (setLabel) => loginOutcome(await browserLogin({
|
|
464
|
+
...options,
|
|
465
|
+
onProgress: setLabel
|
|
466
|
+
})));
|
|
467
|
+
}
|
|
468
|
+
const LOGIN_DESCRIPTION = "Log in to an organization (adds a login; the target org comes from --org, RUNTYPE_ORG, or .runtype/project.json)";
|
|
469
|
+
authCommand.command("login").description(LOGIN_DESCRIPTION).option("--api-url <url>", "Custom API URL").option("--api-key <key>", "Use API key directly").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(loginAction);
|
|
470
|
+
const loginCommand = new Command("login").description(LOGIN_DESCRIPTION).option("--api-url <url>", "Custom API URL").option("--api-key <key>", "Use API key directly").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(loginAction);
|
|
471
|
+
function describeAccountSource(context) {
|
|
472
|
+
switch (context.accountSource) {
|
|
473
|
+
case "flag": return "--org flag";
|
|
474
|
+
case "environment": return "RUNTYPE_ORG";
|
|
475
|
+
case "project": return context.projectLink ? `project link (${context.projectLink.path})` : "project link";
|
|
476
|
+
default: return "default login";
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
authCommand.command("status").description("Show which login this directory uses").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
|
|
480
|
+
const store = new CredentialStore();
|
|
481
|
+
if (!isTTY(options)) {
|
|
482
|
+
try {
|
|
483
|
+
const context = await loadEffectiveContext();
|
|
484
|
+
const storedLogins = (await store.listAccounts()).length;
|
|
485
|
+
const credentials = context.credentials;
|
|
486
|
+
if (!credentials) {
|
|
487
|
+
console.log(JSON.stringify({
|
|
488
|
+
status: "unauthenticated",
|
|
489
|
+
requestedOrgId: context.requestedOrgId ?? null,
|
|
490
|
+
accountSource: context.accountSource,
|
|
491
|
+
projectLink: context.projectLink?.path ?? null,
|
|
492
|
+
storedLogins,
|
|
493
|
+
next: context.requestedOrgId ? authRequiredMessage(context) : "New account: runtype auth register --email <email>. Existing account: runtype auth login --api-key <key>."
|
|
494
|
+
}, null, 2));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const pendingClaim = await store.getClaimState();
|
|
498
|
+
const claimState = pendingClaim?.accountId === credentials.accountId ? pendingClaim : null;
|
|
499
|
+
console.log(JSON.stringify({
|
|
500
|
+
status: "authenticated",
|
|
501
|
+
accountState: claimState ? "temporary" : "full",
|
|
502
|
+
userId: credentials.userId,
|
|
503
|
+
apiUrl: credentials.apiUrl,
|
|
504
|
+
orgId: credentials.orgId || null,
|
|
505
|
+
accountId: credentials.accountId,
|
|
506
|
+
accountSource: context.accountSource,
|
|
507
|
+
projectLink: context.projectLink?.path ?? null,
|
|
508
|
+
secretStore: context.account?.secretStore ?? null,
|
|
509
|
+
storedLogins,
|
|
510
|
+
...context.credentialSource === "environment" ? { note: "RUNTYPE_API_KEY is set and takes precedence over the stored login." } : {},
|
|
511
|
+
...claimState ? {
|
|
512
|
+
registrationId: claimState.registrationId,
|
|
513
|
+
credentialExpires: claimState.credentialExpires,
|
|
514
|
+
claimEmail: claimState.claimEmail ?? null,
|
|
515
|
+
next: claimState.claimEmail ? "Finish signup: ask the user for the 6-digit code from their email, then run: runtype auth verify <code>" : "Finish signup: ask the user for their email, then run: runtype auth claim <email>"
|
|
516
|
+
} : {}
|
|
517
|
+
}, null, 2));
|
|
518
|
+
} catch (error) {
|
|
519
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
520
|
+
console.error(JSON.stringify({
|
|
521
|
+
status: "error",
|
|
522
|
+
error: message
|
|
523
|
+
}, null, 2));
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
await renderTask("Checking authentication...", async () => {
|
|
528
|
+
const context = await loadEffectiveContext();
|
|
529
|
+
const storedLogins = (await store.listAccounts()).length;
|
|
530
|
+
const credentials = context.credentials;
|
|
531
|
+
if (!credentials) return {
|
|
532
|
+
message: "Auth Status",
|
|
533
|
+
result: React.createElement(EntityCard, { fields: [
|
|
534
|
+
{
|
|
535
|
+
label: "Status",
|
|
536
|
+
value: "Not authenticated",
|
|
537
|
+
color: "yellow"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
label: "Selected by",
|
|
541
|
+
value: describeAccountSource(context)
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
label: "Stored logins",
|
|
545
|
+
value: String(storedLogins)
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
label: "Hint",
|
|
549
|
+
value: context.requestedOrgId ? authRequiredMessage(context) : "Run runtype auth login"
|
|
550
|
+
}
|
|
551
|
+
] })
|
|
552
|
+
};
|
|
553
|
+
return {
|
|
554
|
+
message: "Auth Status",
|
|
555
|
+
result: React.createElement(EntityCard, { fields: [
|
|
556
|
+
{
|
|
557
|
+
label: "Status",
|
|
558
|
+
value: "Authenticated",
|
|
559
|
+
color: "green"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
label: "Organization",
|
|
563
|
+
value: credentials.orgId || "Personal"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
label: "User ID",
|
|
567
|
+
value: credentials.userId
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
label: "API URL",
|
|
571
|
+
value: credentials.apiUrl
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
label: "API Key",
|
|
575
|
+
value: credentials.apiKey.substring(0, 12) + "..."
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
label: "Selected by",
|
|
579
|
+
value: describeAccountSource(context)
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
label: "Stored in",
|
|
583
|
+
value: context.account?.secretStore === "file" ? "credential file" : "OS keychain"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
label: "Stored logins",
|
|
587
|
+
value: String(storedLogins)
|
|
588
|
+
},
|
|
589
|
+
...context.credentialSource === "environment" ? [{
|
|
590
|
+
label: "Note",
|
|
591
|
+
value: "RUNTYPE_API_KEY overrides the stored login",
|
|
592
|
+
color: "yellow"
|
|
593
|
+
}] : []
|
|
594
|
+
] })
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
authCommand.command("accounts").alias("list").description("List stored logins (one per organization and API URL)").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
|
|
599
|
+
const store = new CredentialStore();
|
|
600
|
+
const [accounts, defaultAccount, context] = await Promise.all([
|
|
601
|
+
store.listAccounts(),
|
|
602
|
+
store.getDefaultAccount(),
|
|
603
|
+
loadEffectiveContext()
|
|
604
|
+
]);
|
|
605
|
+
const rows = accounts.map((account) => ({
|
|
606
|
+
accountId: account.id,
|
|
607
|
+
orgId: account.orgId,
|
|
608
|
+
userId: account.userId,
|
|
609
|
+
apiUrl: account.apiUrl,
|
|
610
|
+
isDefault: account.id === defaultAccount?.id,
|
|
611
|
+
isActive: account.id === context.account?.id,
|
|
612
|
+
secretStore: account.secretStore,
|
|
613
|
+
pendingClaim: Boolean(account.claim),
|
|
614
|
+
lastUsedAt: account.lastUsedAt ?? null
|
|
615
|
+
}));
|
|
616
|
+
if (!isTTY(options) || options.json) {
|
|
617
|
+
console.log(JSON.stringify({ accounts: rows }, null, 2));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (rows.length === 0) {
|
|
621
|
+
console.log(chalk.yellow("No stored logins. Run: runtype login"));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
for (const row of rows) {
|
|
625
|
+
const marker = row.isActive ? chalk.green("*") : " ";
|
|
626
|
+
const tags = [row.isDefault ? "default" : null, row.pendingClaim ? "unclaimed" : null].filter(Boolean).join(", ");
|
|
627
|
+
console.log(`${marker} ${chalk.bold(row.orgId ?? "Personal")} ${chalk.gray(row.apiUrl)}${tags ? ` ${chalk.cyan(`(${tags})`)}` : ""}`);
|
|
628
|
+
}
|
|
629
|
+
console.log(chalk.gray("\n* = login used in this directory"));
|
|
630
|
+
});
|
|
631
|
+
/** Resolve an `auth switch`/`logout` argument (org id or account id) to exactly one stored login. */
|
|
632
|
+
async function findAccountArgument(store, value, apiUrl) {
|
|
633
|
+
const accounts = await store.listAccounts();
|
|
634
|
+
const byId = accounts.find((account) => account.id === value);
|
|
635
|
+
if (byId) return byId;
|
|
636
|
+
const target = apiUrl ? normalizeApiUrl(apiUrl) : void 0;
|
|
637
|
+
const matches = accounts.filter((account) => account.orgId === value && (!target || account.apiUrl === target));
|
|
638
|
+
if (matches.length === 1) return matches[0];
|
|
639
|
+
if (matches.length > 1) throw new CliContextError(`${value} has logins on several API URLs (${matches.map((m) => m.apiUrl).join(", ")}); pass --api-url to pick one.`);
|
|
640
|
+
throw new CliContextError(`No stored login for ${value}. Run: runtype login --org ${value}`);
|
|
641
|
+
}
|
|
642
|
+
authCommand.command("switch [org]").description("Set the default login used outside linked projects").option("--api-url <url>", "Disambiguate when the organization has logins on several API URLs").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (org, options) => {
|
|
643
|
+
const store = new CredentialStore();
|
|
644
|
+
const accounts = await store.listAccounts();
|
|
645
|
+
if (accounts.length === 0) {
|
|
646
|
+
console.error(chalk.yellow("No stored logins. Run: runtype login"));
|
|
647
|
+
process.exit(1);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
let account;
|
|
651
|
+
if (org) account = await findAccountArgument(store, org, options.apiUrl ?? getInvocationOptions().apiUrl);
|
|
652
|
+
else if (!isTTY(options)) {
|
|
653
|
+
console.error(JSON.stringify({
|
|
654
|
+
status: "error",
|
|
655
|
+
error: "Pass the organization to switch to.",
|
|
656
|
+
next: "runtype auth accounts --json, then: runtype auth switch <orgId>"
|
|
657
|
+
}));
|
|
658
|
+
process.exit(1);
|
|
659
|
+
return;
|
|
660
|
+
} else {
|
|
661
|
+
const { promptForAccount } = await import("./chunk-CGDQoqDC.js");
|
|
662
|
+
account = await promptForAccount(accounts);
|
|
663
|
+
}
|
|
664
|
+
if (!account) return;
|
|
665
|
+
await store.setDefaultAccount(account.id);
|
|
666
|
+
const context = await loadEffectiveContext();
|
|
667
|
+
const shadowed = context.accountSource !== "default" && context.account?.id !== account.id ? `This directory still uses ${context.requestedOrgId} (${describeAccountSource(context)}).` : null;
|
|
668
|
+
if (!isTTY(options)) {
|
|
669
|
+
console.log(JSON.stringify({
|
|
670
|
+
status: "switched",
|
|
671
|
+
accountId: account.id,
|
|
672
|
+
orgId: account.orgId,
|
|
673
|
+
apiUrl: account.apiUrl,
|
|
674
|
+
...shadowed ? { note: shadowed } : {}
|
|
675
|
+
}, null, 2));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
console.log(chalk.green(`Default login is now ${account.orgId ?? "Personal"} (${account.apiUrl}).`));
|
|
679
|
+
if (shadowed) console.log(chalk.yellow(shadowed));
|
|
680
|
+
});
|
|
681
|
+
authCommand.command("export-key").description("Print the active login's API key to stdout (for use in scripts and .env files)").action(async () => {
|
|
682
|
+
const apiKey = await new CredentialStore().getApiKey();
|
|
683
|
+
if (!apiKey) {
|
|
684
|
+
console.error("Not authenticated. Run `runtype auth login` first.");
|
|
685
|
+
process.exit(1);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
process.stdout.write(apiKey);
|
|
689
|
+
});
|
|
690
|
+
registerAgentAuthCommands(authCommand);
|
|
691
|
+
authCommand.command("logout [org]").description("Remove the login this directory uses (or the named org's login, or --all)").option("--all", "Remove every stored login").option("--api-url <url>", "Disambiguate when the organization has logins on several API URLs").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (org, options) => {
|
|
692
|
+
const store = new CredentialStore();
|
|
693
|
+
const logout = async () => {
|
|
694
|
+
if (options.all) {
|
|
695
|
+
const accounts = await store.listAccounts();
|
|
696
|
+
for (const account of accounts) await store.removeAccount(account.id);
|
|
697
|
+
return { removed: accounts };
|
|
698
|
+
}
|
|
699
|
+
if (org) {
|
|
700
|
+
const account = await findAccountArgument(store, org, options.apiUrl ?? getInvocationOptions().apiUrl);
|
|
701
|
+
await store.removeAccount(account.id);
|
|
702
|
+
return { removed: [account] };
|
|
703
|
+
}
|
|
704
|
+
const removed = await store.clearCredentials();
|
|
705
|
+
return { removed: removed ? [removed] : [] };
|
|
706
|
+
};
|
|
707
|
+
if (!isTTY(options)) {
|
|
708
|
+
const { removed } = await logout();
|
|
709
|
+
if (removed.length === 0) {
|
|
710
|
+
console.log(JSON.stringify({ status: "no_credentials" }, null, 2));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
console.log(JSON.stringify({
|
|
714
|
+
status: "logged_out",
|
|
715
|
+
removed: removed.map((account) => ({
|
|
716
|
+
orgId: account.orgId,
|
|
717
|
+
apiUrl: account.apiUrl
|
|
718
|
+
})),
|
|
719
|
+
remainingLogins: (await store.listAccounts()).length
|
|
720
|
+
}, null, 2));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
await renderTask("Logging out...", async () => {
|
|
724
|
+
const { removed } = await logout();
|
|
725
|
+
if (removed.length === 0) return { message: "No stored credentials found" };
|
|
726
|
+
return { message: `Logged out of ${removed.map((account) => account.orgId ?? "Personal").join(", ")}` };
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
authCommand.command("whoami").description("Display current user information").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
|
|
730
|
+
const apiKey = await ensureAuth({
|
|
731
|
+
interactive: false,
|
|
732
|
+
exitOnFailure: false
|
|
733
|
+
});
|
|
734
|
+
const context = await loadEffectiveContext();
|
|
735
|
+
const credentials = {
|
|
736
|
+
...context.credentialSource === "stored" ? context.credentials : {},
|
|
737
|
+
apiKey,
|
|
738
|
+
apiUrl: context.apiUrl
|
|
739
|
+
};
|
|
740
|
+
if (!credentials || !credentials.apiKey) {
|
|
741
|
+
if (!isTTY(options) || options.json) console.log(JSON.stringify({
|
|
742
|
+
status: "unauthenticated",
|
|
743
|
+
...context.requestedOrgId ? {
|
|
744
|
+
requestedOrgId: context.requestedOrgId,
|
|
745
|
+
next: authRequiredMessage(context)
|
|
746
|
+
} : {}
|
|
747
|
+
}, null, 2));
|
|
748
|
+
else {
|
|
749
|
+
console.log(chalk.yellow(context.requestedOrgId ? authRequiredMessage(context) : "Not logged in"));
|
|
750
|
+
if (!context.requestedOrgId) console.log("Run: runtype auth login");
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (!isTTY(options) || options.json) {
|
|
755
|
+
try {
|
|
756
|
+
const user = await getAuthenticatedUser();
|
|
757
|
+
const billingInfo = await getBillingInfo(credentials.apiKey, credentials.apiUrl);
|
|
758
|
+
console.log(JSON.stringify({
|
|
759
|
+
userId: user.userId,
|
|
760
|
+
orgId: user.orgId,
|
|
761
|
+
apiUrl: credentials.apiUrl || getApiUrl(),
|
|
762
|
+
credentialSource: context.credentialSource,
|
|
763
|
+
accountSource: context.accountSource,
|
|
764
|
+
projectLink: context.projectLink?.path ?? null,
|
|
765
|
+
billing: billingInfo
|
|
766
|
+
}, null, 2));
|
|
767
|
+
} catch (error) {
|
|
768
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
769
|
+
console.error(JSON.stringify({
|
|
770
|
+
status: "error",
|
|
771
|
+
error: message
|
|
772
|
+
}, null, 2));
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
await renderTask("Fetching user information...", async () => {
|
|
778
|
+
const user = await getAuthenticatedUser();
|
|
779
|
+
const billingInfo = await getBillingInfo(credentials.apiKey, credentials.apiUrl);
|
|
780
|
+
const fields = [
|
|
781
|
+
{
|
|
782
|
+
label: "User ID",
|
|
783
|
+
value: user.userId
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
label: "Organization",
|
|
787
|
+
value: user.orgId || "Personal"
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
label: "API Key",
|
|
791
|
+
value: credentials.apiKey.substring(0, 10) + "..."
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
label: "API URL",
|
|
795
|
+
value: credentials.apiUrl || getApiUrl()
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
label: "Selected by",
|
|
799
|
+
value: context.credentialSource === "environment" ? "RUNTYPE_API_KEY" : describeAccountSource(context)
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
label: "Created",
|
|
803
|
+
value: credentials.createdAt
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
label: "Last Used",
|
|
807
|
+
value: credentials.lastUsed
|
|
808
|
+
}
|
|
809
|
+
];
|
|
810
|
+
if (billingInfo) {
|
|
811
|
+
const plan = billingInfo.planName || billingInfo.plan;
|
|
812
|
+
if (plan) fields.push({
|
|
813
|
+
label: "Plan",
|
|
814
|
+
value: plan,
|
|
815
|
+
color: "green"
|
|
816
|
+
});
|
|
817
|
+
const usage = billingInfo.usage;
|
|
818
|
+
if (usage) {
|
|
819
|
+
const used = usage.executionsToday ?? usage.executionsThisMonth;
|
|
820
|
+
const value = usage.executionsThisMonthAvailable === false || typeof used !== "number" ? "unavailable" : used.toLocaleString();
|
|
821
|
+
fields.push({
|
|
822
|
+
label: "Executions today",
|
|
823
|
+
value
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
message: "Current User",
|
|
829
|
+
result: React.createElement(EntityCard, { fields })
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
export { authCommand, loginCommand };
|
|
834
|
+
|