@andocorp/cli 0.1.3 → 0.3.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/LICENSE +21 -0
- package/README.md +95 -42
- package/dist/agent-commands.js +297 -0
- package/dist/api-command.js +187 -0
- package/dist/api-inputs.js +223 -0
- package/dist/api-operations.js +344 -0
- package/dist/args.js +71 -0
- package/dist/auth-commands.js +362 -0
- package/dist/cli-helpers.js +67 -0
- package/dist/cli-login-browser.js +60 -0
- package/dist/cli-login-errors.js +10 -0
- package/dist/cli-login-paths.js +8 -0
- package/dist/cli-login-revoke.js +100 -0
- package/dist/cli-login.js +335 -0
- package/dist/client.js +104 -0
- package/dist/commands.js +155 -0
- package/dist/config-credential-metadata.js +68 -0
- package/dist/config-keyring.js +61 -0
- package/dist/config-logout-credentials.js +171 -0
- package/dist/config-paths.js +41 -0
- package/dist/config-types.js +1 -0
- package/dist/config.js +333 -0
- package/dist/format.js +297 -0
- package/dist/help.js +70 -0
- package/dist/index.js +74 -11687
- package/dist/output.js +7 -0
- package/dist/session.js +58 -0
- package/dist/timeouts.js +1 -0
- package/dist/types.js +1 -0
- package/dist/watch-commands.js +120 -0
- package/package.json +24 -20
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { ColorTheme, DEFAULT_ANDO_BASE_URL, MemberType, SidebarFolderFilterMode, SidebarFolderSortMode, WorkspaceRole, } from "@andocorp/sdk";
|
|
2
|
+
import { getStringFlag } from "./args.js";
|
|
3
|
+
import { CliLoginRequestError, clearPendingCliLoginSession } from "./cli-login.js";
|
|
4
|
+
import { revokeCliLoginApiKey } from "./cli-login-revoke.js";
|
|
5
|
+
import { createAndoCliClient } from "./client.js";
|
|
6
|
+
import { clearSavedConfig, inspectSavedConfig, readSavedConfigMetadata, } from "./config.js";
|
|
7
|
+
import { readSavedConfigsForLogout, } from "./config-logout-credentials.js";
|
|
8
|
+
import { printJson } from "./output.js";
|
|
9
|
+
import { getConfiguredApiHost, getConfiguredBaseUrl, getConfiguredRealtimeHost, ensureConfig, } from "./session.js";
|
|
10
|
+
const PUBLIC_API_PROBE_DATE = new Date("2026-05-29T12:00:00.000Z");
|
|
11
|
+
function getEnvApiKey() {
|
|
12
|
+
const value = process.env["ANDO_API_KEY"];
|
|
13
|
+
return value == null || value.trim() === "" ? null : value.trim();
|
|
14
|
+
}
|
|
15
|
+
function getFlagApiKey(parsedArgs) {
|
|
16
|
+
const value = getStringFlag(parsedArgs, "api-key");
|
|
17
|
+
return value == null || value.trim() === "" ? null : value.trim();
|
|
18
|
+
}
|
|
19
|
+
function getExplicitApiKey(parsedArgs) {
|
|
20
|
+
return getFlagApiKey(parsedArgs) ?? getEnvApiKey();
|
|
21
|
+
}
|
|
22
|
+
function formatCheck(ok) {
|
|
23
|
+
return ok ? "OK" : "--";
|
|
24
|
+
}
|
|
25
|
+
function formatError(error) {
|
|
26
|
+
return error instanceof Error ? error.message : String(error);
|
|
27
|
+
}
|
|
28
|
+
function isLegacyGetMeUnavailable(error) {
|
|
29
|
+
return (error instanceof Error &&
|
|
30
|
+
error.message.includes("/auth/me failed") &&
|
|
31
|
+
error.message.includes("404"));
|
|
32
|
+
}
|
|
33
|
+
function createPublicApiProbeMe() {
|
|
34
|
+
return {
|
|
35
|
+
id: "public-api-key",
|
|
36
|
+
stytch_member_id: null,
|
|
37
|
+
email: null,
|
|
38
|
+
display_name: "Public API key",
|
|
39
|
+
profile_image_url: null,
|
|
40
|
+
profile_image_file_id: null,
|
|
41
|
+
top_level_sidebar_item_ids: [],
|
|
42
|
+
sidebar_folders: [],
|
|
43
|
+
sidebar_conversations_preferences: {
|
|
44
|
+
collapse_threads: "reading",
|
|
45
|
+
filter_mode: SidebarFolderFilterMode.ALL,
|
|
46
|
+
hide_read_thread_trees: false,
|
|
47
|
+
inactive_after: "never",
|
|
48
|
+
sort_mode: SidebarFolderSortMode.MANUAL,
|
|
49
|
+
},
|
|
50
|
+
title: null,
|
|
51
|
+
time_zone: null,
|
|
52
|
+
workspace_id: "public-api-workspace",
|
|
53
|
+
member_type: MemberType.HUMAN,
|
|
54
|
+
workspace_role: WorkspaceRole.MEMBER,
|
|
55
|
+
total_unread_count: 0,
|
|
56
|
+
e164: null,
|
|
57
|
+
from_e164: null,
|
|
58
|
+
linq_chat_id: null,
|
|
59
|
+
archived_at: null,
|
|
60
|
+
last_active_at: PUBLIC_API_PROBE_DATE,
|
|
61
|
+
last_caught_up_at: PUBLIC_API_PROBE_DATE,
|
|
62
|
+
onboarding_completed_at: null,
|
|
63
|
+
summary_text: null,
|
|
64
|
+
last_summary_at: null,
|
|
65
|
+
buffer_text: null,
|
|
66
|
+
updated_at: PUBLIC_API_PROBE_DATE,
|
|
67
|
+
created_at: PUBLIC_API_PROBE_DATE,
|
|
68
|
+
color_theme: ColorTheme.AUTO,
|
|
69
|
+
show_thread_trees: false,
|
|
70
|
+
profile_image_file: null,
|
|
71
|
+
workspace: {
|
|
72
|
+
id: "public-api-workspace",
|
|
73
|
+
stytch_organization_id: "public-api-organization",
|
|
74
|
+
name: "Public API workspace",
|
|
75
|
+
slug: "public-api-workspace",
|
|
76
|
+
image_url: null,
|
|
77
|
+
icon_image_file_id: null,
|
|
78
|
+
description: null,
|
|
79
|
+
invite_link_id: "public-api-invite",
|
|
80
|
+
created_at: PUBLIC_API_PROBE_DATE,
|
|
81
|
+
updated_at: PUBLIC_API_PROBE_DATE,
|
|
82
|
+
workspace_invite_link: {
|
|
83
|
+
id: "public-api-invite",
|
|
84
|
+
workspace_id: "public-api-workspace",
|
|
85
|
+
slug: "public-api-workspace",
|
|
86
|
+
code: "PUBLICAPI",
|
|
87
|
+
created_at: PUBLIC_API_PROBE_DATE,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function probePublicApiAuth(config) {
|
|
93
|
+
const client = createAndoCliClient(config);
|
|
94
|
+
const subscription = await client.realtime.subscribeMember();
|
|
95
|
+
await subscription.close();
|
|
96
|
+
return createPublicApiProbeMe();
|
|
97
|
+
}
|
|
98
|
+
function getAuthSource(parsedArgs, inspection) {
|
|
99
|
+
if (getFlagApiKey(parsedArgs) != null) {
|
|
100
|
+
return {
|
|
101
|
+
source: "flag",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (getEnvApiKey() != null) {
|
|
105
|
+
return {
|
|
106
|
+
source: "env",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (inspection.hasCredential && inspection.credentialStorage != null) {
|
|
110
|
+
return {
|
|
111
|
+
source: inspection.credentialStorage,
|
|
112
|
+
account: inspection.credentialAccount,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (inspection.hasLegacyCredential) {
|
|
116
|
+
return {
|
|
117
|
+
source: "legacy_config",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
source: "missing",
|
|
122
|
+
account: inspection.credentialAccount,
|
|
123
|
+
error: inspection.credentialError,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function defaultGetMe(config) {
|
|
127
|
+
const client = createAndoCliClient(config);
|
|
128
|
+
try {
|
|
129
|
+
return await client.getMe();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (isLegacyGetMeUnavailable(error)) {
|
|
133
|
+
return await probePublicApiAuth(config);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await client.close();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function loadDoctorConfig(parsedArgs, dependencies) {
|
|
142
|
+
if (dependencies.loadConfig != null) {
|
|
143
|
+
return await dependencies.loadConfig();
|
|
144
|
+
}
|
|
145
|
+
const explicitApiKey = getExplicitApiKey(parsedArgs);
|
|
146
|
+
if (explicitApiKey != null) {
|
|
147
|
+
return buildExplicitApiKeyConfig(parsedArgs, explicitApiKey, await readOptionalSavedConfigMetadata(dependencies));
|
|
148
|
+
}
|
|
149
|
+
return await ensureConfig({ command: "doctor", parsedArgs });
|
|
150
|
+
}
|
|
151
|
+
function buildExplicitApiKeyConfig(parsedArgs, apiKey, savedConfig) {
|
|
152
|
+
return {
|
|
153
|
+
apiKey,
|
|
154
|
+
apiHost: getConfiguredApiHost(parsedArgs, savedConfig?.apiHost ?? null),
|
|
155
|
+
baseUrl: getConfiguredBaseUrl(parsedArgs, savedConfig?.baseUrl ?? null),
|
|
156
|
+
realtimeHost: getConfiguredRealtimeHost(parsedArgs, savedConfig?.realtimeHost ?? null),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function readOptionalSavedConfigMetadata(dependencies) {
|
|
160
|
+
try {
|
|
161
|
+
return await (dependencies.readSavedConfigMetadata ?? readSavedConfigMetadata)();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function buildDoctorReport(parsedArgs, dependencies) {
|
|
168
|
+
const inspection = await (dependencies.inspectSavedConfig ?? inspectSavedConfig)();
|
|
169
|
+
const auth = getAuthSource(parsedArgs, inspection);
|
|
170
|
+
let api = {
|
|
171
|
+
status: "skipped",
|
|
172
|
+
message: "No API key found. Run `ando login` or set ANDO_API_KEY.",
|
|
173
|
+
};
|
|
174
|
+
if (auth.source !== "missing") {
|
|
175
|
+
try {
|
|
176
|
+
const config = await loadDoctorConfig(parsedArgs, dependencies);
|
|
177
|
+
const me = await (dependencies.getMe ?? defaultGetMe)(config);
|
|
178
|
+
api = {
|
|
179
|
+
status: "ok",
|
|
180
|
+
memberId: me.id,
|
|
181
|
+
memberLabel: me.display_name ?? me.email ?? me.id,
|
|
182
|
+
workspaceId: me.workspace.id,
|
|
183
|
+
workspaceLabel: me.workspace.name,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
api = {
|
|
188
|
+
status: "failed",
|
|
189
|
+
message: formatError(error),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
api,
|
|
195
|
+
auth,
|
|
196
|
+
config: {
|
|
197
|
+
error: inspection.configError,
|
|
198
|
+
path: inspection.configPath,
|
|
199
|
+
present: inspection.hasConfig,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function loadWhoamiContext(parsedArgs, dependencies) {
|
|
204
|
+
if (dependencies.loadConfig != null) {
|
|
205
|
+
const [config, savedConfig] = await Promise.all([
|
|
206
|
+
dependencies.loadConfig(),
|
|
207
|
+
(dependencies.readSavedConfigMetadata ?? readSavedConfigMetadata)(),
|
|
208
|
+
]);
|
|
209
|
+
return { config, savedConfig };
|
|
210
|
+
}
|
|
211
|
+
const explicitApiKey = getExplicitApiKey(parsedArgs);
|
|
212
|
+
if (explicitApiKey != null) {
|
|
213
|
+
const savedConfig = await readOptionalSavedConfigMetadata(dependencies);
|
|
214
|
+
return {
|
|
215
|
+
config: buildExplicitApiKeyConfig(parsedArgs, explicitApiKey, savedConfig),
|
|
216
|
+
savedConfig,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const [config, savedConfig] = await Promise.all([
|
|
220
|
+
ensureConfig({ command: "whoami", parsedArgs }),
|
|
221
|
+
(dependencies.readSavedConfigMetadata ?? readSavedConfigMetadata)(),
|
|
222
|
+
]);
|
|
223
|
+
return { config, savedConfig };
|
|
224
|
+
}
|
|
225
|
+
async function buildWhoamiReport(parsedArgs, dependencies) {
|
|
226
|
+
const { config, savedConfig } = await loadWhoamiContext(parsedArgs, dependencies);
|
|
227
|
+
const me = await (dependencies.getMe ?? defaultGetMe)(config);
|
|
228
|
+
return {
|
|
229
|
+
member: {
|
|
230
|
+
id: me.id,
|
|
231
|
+
displayName: me.display_name ?? undefined,
|
|
232
|
+
email: me.email ?? undefined,
|
|
233
|
+
},
|
|
234
|
+
saved: {
|
|
235
|
+
defaultWorkspaceId: savedConfig?.defaultWorkspaceId,
|
|
236
|
+
defaultWorkspaceMembershipId: savedConfig?.defaultWorkspaceMembershipId,
|
|
237
|
+
},
|
|
238
|
+
workspace: {
|
|
239
|
+
id: me.workspace.id,
|
|
240
|
+
name: me.workspace.name,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function formatAuthSource(auth) {
|
|
245
|
+
switch (auth.source) {
|
|
246
|
+
case "flag":
|
|
247
|
+
return "--api-key";
|
|
248
|
+
case "env":
|
|
249
|
+
return "ANDO_API_KEY";
|
|
250
|
+
case "file":
|
|
251
|
+
return `file${auth.account == null ? "" : ` (${auth.account})`}`;
|
|
252
|
+
case "keyring":
|
|
253
|
+
return `keyring${auth.account == null ? "" : ` (${auth.account})`}`;
|
|
254
|
+
case "legacy_config":
|
|
255
|
+
return "legacy config";
|
|
256
|
+
case "missing":
|
|
257
|
+
return auth.error == null ? "missing" : `missing (${auth.error})`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function formatMemberLabel(member) {
|
|
261
|
+
return member.displayName ?? member.email ?? member.id;
|
|
262
|
+
}
|
|
263
|
+
function writeDoctorReport(stdout, report) {
|
|
264
|
+
const configOk = report.config.present && report.config.error == null;
|
|
265
|
+
stdout.write(`Config ${formatCheck(configOk)} ${report.config.path}${report.config.error == null ? "" : ` (${report.config.error})`}\n`);
|
|
266
|
+
stdout.write(`Credential ${formatCheck(report.auth.source !== "missing")} ${formatAuthSource(report.auth)}\n`);
|
|
267
|
+
if (report.api.status === "ok") {
|
|
268
|
+
stdout.write(`API authenticated OK ${report.api.memberLabel} (${report.api.memberId})\n`);
|
|
269
|
+
stdout.write(`Workspace OK ${report.api.workspaceLabel} (${report.api.workspaceId})\n`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
stdout.write(`API authenticated -- ${report.api.message}\n`);
|
|
273
|
+
}
|
|
274
|
+
function writeWhoamiReport(stdout, report) {
|
|
275
|
+
stdout.write(`Member ${formatMemberLabel(report.member)} (${report.member.id})\n`);
|
|
276
|
+
if (report.member.email != null) {
|
|
277
|
+
stdout.write(`Email ${report.member.email}\n`);
|
|
278
|
+
}
|
|
279
|
+
stdout.write(`Workspace ${report.workspace.name} (${report.workspace.id})\n`);
|
|
280
|
+
if (report.saved.defaultWorkspaceId != null) {
|
|
281
|
+
stdout.write(`Saved workspace ${report.saved.defaultWorkspaceId}\n`);
|
|
282
|
+
}
|
|
283
|
+
if (report.saved.defaultWorkspaceMembershipId != null) {
|
|
284
|
+
stdout.write(`Saved membership ${report.saved.defaultWorkspaceMembershipId}\n`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function readSavedConfigsForLogoutCommand(dependencies) {
|
|
288
|
+
try {
|
|
289
|
+
if (dependencies.readSavedConfigsForLogout != null) {
|
|
290
|
+
return await dependencies.readSavedConfigsForLogout();
|
|
291
|
+
}
|
|
292
|
+
if (dependencies.readSavedConfig != null) {
|
|
293
|
+
const savedConfig = await dependencies.readSavedConfig();
|
|
294
|
+
return savedConfig == null
|
|
295
|
+
? []
|
|
296
|
+
: [
|
|
297
|
+
{
|
|
298
|
+
...savedConfig,
|
|
299
|
+
revokeOnLogout: savedConfig.credentialSource === "browser_login",
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
return await readSavedConfigsForLogout();
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function revokeSavedCliCredential(config, dependencies, stdout) {
|
|
310
|
+
if (!config.revokeOnLogout) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const result = await (dependencies.revokeCliLoginApiKey ?? revokeCliLoginApiKey)({
|
|
315
|
+
apiKey: config.apiKey,
|
|
316
|
+
baseUrl: config.baseUrl ?? DEFAULT_ANDO_BASE_URL,
|
|
317
|
+
});
|
|
318
|
+
stdout.write(result.revoked
|
|
319
|
+
? "Revoked remote CLI credential.\n"
|
|
320
|
+
: "Remote CLI credential was already revoked.\n");
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (error instanceof CliLoginRequestError) {
|
|
324
|
+
if (error.errorCode === "invalid_api_key") {
|
|
325
|
+
stdout.write("Remote CLI credential was already invalid.\n");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (error.errorCode === "unsupported_api_key_type") {
|
|
329
|
+
stdout.write("Remote CLI credential does not support revocation.\n");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
export async function runDoctor(parsedArgs, dependencies = {}) {
|
|
337
|
+
const report = await buildDoctorReport(parsedArgs, dependencies);
|
|
338
|
+
if (parsedArgs.flags.has("json")) {
|
|
339
|
+
printJson(report);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
writeDoctorReport(dependencies.stdout ?? process.stdout, report);
|
|
343
|
+
}
|
|
344
|
+
export async function runWhoami(parsedArgs, dependencies = {}) {
|
|
345
|
+
const report = await buildWhoamiReport(parsedArgs, dependencies);
|
|
346
|
+
if (parsedArgs.flags.has("json")) {
|
|
347
|
+
printJson(report);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
writeWhoamiReport(dependencies.stdout ?? process.stdout, report);
|
|
351
|
+
}
|
|
352
|
+
export async function runLogout(dependencies = {}) {
|
|
353
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
354
|
+
const savedConfigs = await readSavedConfigsForLogoutCommand(dependencies);
|
|
355
|
+
for (const savedConfig of savedConfigs) {
|
|
356
|
+
await revokeSavedCliCredential(savedConfig, dependencies, stdout);
|
|
357
|
+
}
|
|
358
|
+
const result = await (dependencies.clearSavedConfig ?? clearSavedConfig)();
|
|
359
|
+
await (dependencies.clearPendingCliLoginSession ?? clearPendingCliLoginSession)();
|
|
360
|
+
stdout.write(`Removed CLI auth config at ${result.configPath}\n`);
|
|
361
|
+
stdout.write("Logged out.\n");
|
|
362
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const CHANNEL_CONVERSATION_TYPE = 0;
|
|
2
|
+
function isChannel(membership) {
|
|
3
|
+
return membership.conversation.type === CHANNEL_CONVERSATION_TYPE;
|
|
4
|
+
}
|
|
5
|
+
function normalizeForMatch(value) {
|
|
6
|
+
return value.trim().toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
function getConversationName(membership) {
|
|
9
|
+
const conversation = membership.conversation;
|
|
10
|
+
return conversation.name ?? "";
|
|
11
|
+
}
|
|
12
|
+
export function compareByRecentActivity(left, right) {
|
|
13
|
+
const leftDate = new Date(left.conversation.last_message_at ?? left.conversation.updated_at).getTime();
|
|
14
|
+
const rightDate = new Date(right.conversation.last_message_at ?? right.conversation.updated_at).getTime();
|
|
15
|
+
return rightDate - leftDate;
|
|
16
|
+
}
|
|
17
|
+
export function resolveConversation(memberships, options) {
|
|
18
|
+
const filtered = memberships
|
|
19
|
+
.filter((membership) => {
|
|
20
|
+
if (!options.allowChannels && isChannel(membership)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (!options.allowDms && !isChannel(membership)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
})
|
|
28
|
+
.sort(compareByRecentActivity);
|
|
29
|
+
const normalizedQuery = normalizeForMatch(options.query);
|
|
30
|
+
const exactMatch = filtered.find((membership) => {
|
|
31
|
+
return (membership.conversation.id === options.query ||
|
|
32
|
+
normalizeForMatch(getConversationName(membership)) === normalizedQuery);
|
|
33
|
+
}) ?? null;
|
|
34
|
+
if (exactMatch != null) {
|
|
35
|
+
return exactMatch;
|
|
36
|
+
}
|
|
37
|
+
const partialMatches = filtered.filter((membership) => {
|
|
38
|
+
const fields = [
|
|
39
|
+
membership.conversation.id,
|
|
40
|
+
getConversationName(membership),
|
|
41
|
+
getConversationLabel(membership),
|
|
42
|
+
];
|
|
43
|
+
return fields.some((value) => normalizeForMatch(value).includes(normalizedQuery));
|
|
44
|
+
});
|
|
45
|
+
if (partialMatches.length === 1) {
|
|
46
|
+
const [partialMatch] = partialMatches;
|
|
47
|
+
if (partialMatch == null) {
|
|
48
|
+
throw new Error(`No conversation matched "${options.query}".`);
|
|
49
|
+
}
|
|
50
|
+
return partialMatch;
|
|
51
|
+
}
|
|
52
|
+
if (partialMatches.length === 0) {
|
|
53
|
+
throw new Error(`No conversation matched "${options.query}".`);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Conversation "${options.query}" is ambiguous: ${partialMatches
|
|
56
|
+
.slice(0, 8)
|
|
57
|
+
.map((membership) => getConversationLabel(membership))
|
|
58
|
+
.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
function getConversationLabel(membership) {
|
|
61
|
+
const prefix = membership.conversation.type === CHANNEL_CONVERSATION_TYPE ? "#" : "@";
|
|
62
|
+
const name = getConversationName(membership);
|
|
63
|
+
if (name.length > 0) {
|
|
64
|
+
return `${prefix}${name}`;
|
|
65
|
+
}
|
|
66
|
+
return `${prefix}${membership.conversation.id}`;
|
|
67
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const BROWSER_LAUNCH_TIMEOUT_MS = 5_000;
|
|
3
|
+
export async function sleep(ms) {
|
|
4
|
+
await new Promise((resolve) => {
|
|
5
|
+
setTimeout(resolve, Math.max(0, ms));
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export async function openBrowser(url, platform = process.platform, launchProcess = spawn) {
|
|
9
|
+
const { args, command } = getBrowserLauncher(platform, url);
|
|
10
|
+
await new Promise((resolve, reject) => {
|
|
11
|
+
const child = launchProcess(command, args, {
|
|
12
|
+
detached: true,
|
|
13
|
+
stdio: "ignore",
|
|
14
|
+
});
|
|
15
|
+
waitForBrowserLauncher(child, command, resolve, reject);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function getBrowserLauncher(platform, url) {
|
|
19
|
+
if (platform === "darwin") {
|
|
20
|
+
return { args: [url], command: "open" };
|
|
21
|
+
}
|
|
22
|
+
if (platform === "win32") {
|
|
23
|
+
return { args: ["url.dll,FileProtocolHandler", url], command: "rundll32" };
|
|
24
|
+
}
|
|
25
|
+
return { args: [url], command: "xdg-open" };
|
|
26
|
+
}
|
|
27
|
+
function waitForBrowserLauncher(child, command, resolve, reject) {
|
|
28
|
+
let settled = false;
|
|
29
|
+
const settle = (callback) => {
|
|
30
|
+
if (settled) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
settled = true;
|
|
34
|
+
clearTimeout(timeout);
|
|
35
|
+
child.off("error", onError);
|
|
36
|
+
child.off("close", onClose);
|
|
37
|
+
callback();
|
|
38
|
+
};
|
|
39
|
+
const onError = (error) => {
|
|
40
|
+
settle(() => reject(error));
|
|
41
|
+
};
|
|
42
|
+
const onClose = (code, signal) => {
|
|
43
|
+
settle(() => {
|
|
44
|
+
if (code === 0) {
|
|
45
|
+
resolve();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
reject(new Error(`${command} failed to open the browser` +
|
|
49
|
+
(code == null
|
|
50
|
+
? ` with signal ${signal ?? "unknown"}`
|
|
51
|
+
: ` with exit code ${code}`)));
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const timeout = setTimeout(() => {
|
|
55
|
+
settle(() => reject(new Error(`${command} did not finish opening the browser in time.`)));
|
|
56
|
+
}, BROWSER_LAUNCH_TIMEOUT_MS);
|
|
57
|
+
child.once("error", onError);
|
|
58
|
+
child.once("close", onClose);
|
|
59
|
+
child.unref();
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getConfigPath, getConfigReadPaths } from "./config-paths.js";
|
|
3
|
+
export function getPendingCliLoginPath(configPath = getConfigPath()) {
|
|
4
|
+
return path.join(path.dirname(configPath), "cli-login.json");
|
|
5
|
+
}
|
|
6
|
+
export function getPendingCliLoginPaths() {
|
|
7
|
+
return getConfigReadPaths().map(getPendingCliLoginPath);
|
|
8
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CliLoginRequestError } from "./cli-login-errors.js";
|
|
2
|
+
import { DEFAULT_CLI_REQUEST_TIMEOUT_MS } from "./timeouts.js";
|
|
3
|
+
const REVOKE_CLI_LOGIN_API_KEY_PATH = "/auth/cli-login/revoke";
|
|
4
|
+
export async function revokeCliLoginApiKey(params) {
|
|
5
|
+
const response = await postCliLoginRevokeJson({
|
|
6
|
+
apiKey: params.apiKey,
|
|
7
|
+
baseUrl: params.baseUrl,
|
|
8
|
+
fetchFn: params.fetchFn,
|
|
9
|
+
});
|
|
10
|
+
if (response.status === 200) {
|
|
11
|
+
return parseRevokeCliLoginApiKeyResponse(response.body);
|
|
12
|
+
}
|
|
13
|
+
throwRevokeResponseError(response.body);
|
|
14
|
+
}
|
|
15
|
+
function buildCliLoginRevokeUrl(baseUrl) {
|
|
16
|
+
return `${baseUrl.replace(/\/+$/, "")}${REVOKE_CLI_LOGIN_API_KEY_PATH}`;
|
|
17
|
+
}
|
|
18
|
+
async function postCliLoginRevokeJson(params) {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timeout = setTimeout(() => {
|
|
21
|
+
controller.abort();
|
|
22
|
+
}, DEFAULT_CLI_REQUEST_TIMEOUT_MS);
|
|
23
|
+
try {
|
|
24
|
+
const fetchFn = params.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
25
|
+
const response = await fetchFn(buildCliLoginRevokeUrl(params.baseUrl), {
|
|
26
|
+
headers: {
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
Authorization: `Bearer ${params.apiKey}`,
|
|
29
|
+
},
|
|
30
|
+
method: "POST",
|
|
31
|
+
signal: controller.signal,
|
|
32
|
+
});
|
|
33
|
+
const bodyText = await response.text();
|
|
34
|
+
return {
|
|
35
|
+
body: bodyText.trim() === "" ? null : parseRevokeJson(bodyText),
|
|
36
|
+
status: response.status,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
41
|
+
throw new Error(`[ando-cli] login request timed out after ${DEFAULT_CLI_REQUEST_TIMEOUT_MS}ms`, { cause: error });
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseRevokeJson(bodyText) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(bodyText);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new CliLoginRequestError("Malformed CLI login response: invalid JSON.", {
|
|
55
|
+
terminal: true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function parseRevokeCliLoginApiKeyResponse(body) {
|
|
60
|
+
const response = getRecord(body);
|
|
61
|
+
const data = getRecord(response["data"]);
|
|
62
|
+
if (response["success"] !== true) {
|
|
63
|
+
throwMalformedRevokeResponse("revoke response missing data");
|
|
64
|
+
}
|
|
65
|
+
const apiKeyId = getNonEmptyString(data, "api_key_id");
|
|
66
|
+
const revoked = data["revoked"];
|
|
67
|
+
if (apiKeyId == null || typeof revoked !== "boolean") {
|
|
68
|
+
throwMalformedRevokeResponse("revoke response missing required fields");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
apiKeyId,
|
|
72
|
+
revoked,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function throwRevokeResponseError(body) {
|
|
76
|
+
const response = getRecord(body);
|
|
77
|
+
const errorCode = getNonEmptyString(response, "error_code");
|
|
78
|
+
const errorMessage = getNonEmptyString(response, "error_message");
|
|
79
|
+
if (errorCode == null) {
|
|
80
|
+
throwMalformedRevokeResponse("error response missing error_code");
|
|
81
|
+
}
|
|
82
|
+
throw new CliLoginRequestError(`${errorMessage ?? "CLI login failed."} (${errorCode})`, {
|
|
83
|
+
errorCode,
|
|
84
|
+
terminal: errorCode === "invalid_api_key" || errorCode === "unsupported_api_key_type",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function throwMalformedRevokeResponse(reason) {
|
|
88
|
+
throw new CliLoginRequestError(`Malformed CLI login response: ${reason}.`, {
|
|
89
|
+
terminal: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function getRecord(value) {
|
|
93
|
+
return value != null && typeof value === "object" && !Array.isArray(value)
|
|
94
|
+
? value
|
|
95
|
+
: {};
|
|
96
|
+
}
|
|
97
|
+
function getNonEmptyString(record, key) {
|
|
98
|
+
const value = record[key];
|
|
99
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
100
|
+
}
|