@andocorp/cli 0.1.0 → 0.1.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/README.md +5 -51
- package/dist/index.js +13911 -0
- package/package.json +6 -11
- package/src/adapters.test.ts +0 -50
- package/src/adapters.ts +0 -215
- package/src/args.test.ts +0 -28
- package/src/args.ts +0 -98
- package/src/cli-helpers.test.ts +0 -82
- package/src/cli-helpers.ts +0 -149
- package/src/client.test.ts +0 -235
- package/src/client.ts +0 -378
- package/src/components/prompt-line.ts +0 -179
- package/src/components/transcript-pane.test.ts +0 -26
- package/src/components/transcript-pane.ts +0 -457
- package/src/config.ts +0 -53
- package/src/emoji-suggestions.ts +0 -152
- package/src/format.test.ts +0 -54
- package/src/format.ts +0 -611
- package/src/help.test.ts +0 -13
- package/src/help.ts +0 -48
- package/src/index.ts +0 -466
- package/src/interactive.ts +0 -832
- package/src/test-helpers.ts +0 -207
- package/src/types.ts +0 -24
package/package.json
CHANGED
|
@@ -1,36 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andocorp/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Ando CLI - A terminal client for humans and agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ando": "./
|
|
7
|
+
"ando": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --format esm && (echo '#!/usr/bin/env node'; tail -n +2 ./dist/index.js) > ./dist/index.tmp && mv ./dist/index.tmp ./dist/index.js && chmod +x ./dist/index.js",
|
|
10
11
|
"compile": "bun build --compile ./src/index.ts --outfile ./dist/ando && chmod +x ./dist/ando",
|
|
11
12
|
"compile:all": "node scripts/build-all-platforms.js",
|
|
12
13
|
"dev": "bun run ./src/index.ts",
|
|
13
14
|
"lint": "pnpm -w exec eslint apps/cli --no-warn-ignored",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
14
16
|
"test": "vitest run",
|
|
15
17
|
"typecheck": "tsc --noEmit"
|
|
16
18
|
},
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "https://github.com/ando-labs/ando.git",
|
|
20
|
-
"directory": "apps/cli"
|
|
21
|
-
},
|
|
22
19
|
"keywords": ["cli", "terminal", "chat", "collaboration"],
|
|
23
|
-
"author": "
|
|
20
|
+
"author": "Asari Inc.",
|
|
24
21
|
"license": "MIT",
|
|
25
22
|
"files": [
|
|
26
|
-
"
|
|
27
|
-
"src/**/*.js"
|
|
23
|
+
"dist/**/*.js"
|
|
28
24
|
],
|
|
29
25
|
"publishConfig": {
|
|
30
26
|
"access": "public"
|
|
31
27
|
},
|
|
32
28
|
"dependencies": {
|
|
33
|
-
"@ando/shared": "workspace:*",
|
|
34
29
|
"convex": "^1.34.1",
|
|
35
30
|
"emojibase-data": "^16.0.3"
|
|
36
31
|
},
|
package/src/adapters.test.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { adaptBootstrap, adaptConversationPage } from "./adapters.js";
|
|
3
|
-
import { createMe, createMembership, createMessage } from "./test-helpers.js";
|
|
4
|
-
|
|
5
|
-
describe("adapters", () => {
|
|
6
|
-
it("adapts bootstrap payloads into date-backed objects", () => {
|
|
7
|
-
const result = adaptBootstrap({
|
|
8
|
-
me: createMe({
|
|
9
|
-
created_at: 1_700_000_000_000 as unknown as Date,
|
|
10
|
-
}) as unknown as Record<string, unknown>,
|
|
11
|
-
memberships: [
|
|
12
|
-
createMembership({
|
|
13
|
-
created_at: 1_700_000_000_000 as unknown as Date,
|
|
14
|
-
}) as unknown as Record<string, unknown>,
|
|
15
|
-
],
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
expect(result.me?.created_at).toBeInstanceOf(Date);
|
|
19
|
-
expect(result.memberships[0]?.created_at).toBeInstanceOf(Date);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("reverses paginated conversation results into chronological order", () => {
|
|
23
|
-
const newest = createMessage({
|
|
24
|
-
id: "message-3",
|
|
25
|
-
created_at: 3 as unknown as Date,
|
|
26
|
-
});
|
|
27
|
-
const middle = createMessage({
|
|
28
|
-
id: "message-2",
|
|
29
|
-
created_at: 2 as unknown as Date,
|
|
30
|
-
});
|
|
31
|
-
const oldest = createMessage({
|
|
32
|
-
id: "message-1",
|
|
33
|
-
created_at: 1 as unknown as Date,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const page = adaptConversationPage({
|
|
37
|
-
page: [newest, middle, oldest],
|
|
38
|
-
isDone: false,
|
|
39
|
-
continueCursor: "cursor-2",
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
expect(page.items.map((item) => item.id)).toEqual([
|
|
43
|
-
"message-1",
|
|
44
|
-
"message-2",
|
|
45
|
-
"message-3",
|
|
46
|
-
]);
|
|
47
|
-
expect(page.cursor).toBe("cursor-2");
|
|
48
|
-
expect(page.hasMore).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
});
|
package/src/adapters.ts
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import * as Shared from "@ando/shared";
|
|
2
|
-
import { ConversationPage, DiscoveredWorkspace, Me, Membership, Message } from "./types.js";
|
|
3
|
-
|
|
4
|
-
type BootstrapResult = {
|
|
5
|
-
me: Record<string, unknown> | null;
|
|
6
|
-
memberships: Record<string, unknown>[];
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
type ConvexPaginatedResult<T> = {
|
|
10
|
-
page: T[];
|
|
11
|
-
isDone: boolean;
|
|
12
|
-
continueCursor: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function adaptTimestamp(value: Date | number | string | null | undefined): Date {
|
|
16
|
-
if (value instanceof Date) {
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (typeof value === "number") {
|
|
21
|
-
return new Date(value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (typeof value === "string") {
|
|
25
|
-
return new Date(value);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return new Date(0);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function adaptNullableTimestamp(value: Date | number | string | null | undefined) {
|
|
32
|
-
if (value == null) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return adaptTimestamp(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function adaptDiscoveredWorkspace(
|
|
40
|
-
workspace: Shared.Contracts.DiscoveredWorkspace
|
|
41
|
-
): DiscoveredWorkspace {
|
|
42
|
-
return {
|
|
43
|
-
...workspace,
|
|
44
|
-
created_at: adaptTimestamp(workspace.created_at),
|
|
45
|
-
updated_at: adaptTimestamp(workspace.updated_at),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function adaptFile(file: Shared.Contracts.FileExtended): Shared.Contracts.FileExtended {
|
|
50
|
-
return {
|
|
51
|
-
...file,
|
|
52
|
-
created_at: adaptTimestamp(file.created_at),
|
|
53
|
-
updated_at: adaptTimestamp(file.updated_at),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function adaptPreviewMember(member: Shared.Models.Member): Shared.Models.Member {
|
|
58
|
-
return {
|
|
59
|
-
...member,
|
|
60
|
-
created_at: adaptTimestamp(member.created_at),
|
|
61
|
-
updated_at: adaptTimestamp(member.updated_at),
|
|
62
|
-
last_active_at: adaptTimestamp(member.last_active_at),
|
|
63
|
-
last_caught_up_at: adaptTimestamp(member.last_caught_up_at),
|
|
64
|
-
archived_at: adaptNullableTimestamp(member.archived_at),
|
|
65
|
-
onboarding_completed_at: adaptNullableTimestamp(member.onboarding_completed_at),
|
|
66
|
-
last_summary_at: adaptNullableTimestamp(member.last_summary_at),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function adaptMember(member: Shared.Contracts.MemberExtended): Shared.Contracts.MemberExtended {
|
|
71
|
-
return {
|
|
72
|
-
...member,
|
|
73
|
-
created_at: adaptTimestamp(member.created_at),
|
|
74
|
-
updated_at: adaptTimestamp(member.updated_at),
|
|
75
|
-
last_active_at: adaptTimestamp(member.last_active_at),
|
|
76
|
-
last_caught_up_at: adaptTimestamp(member.last_caught_up_at),
|
|
77
|
-
archived_at: adaptNullableTimestamp(member.archived_at),
|
|
78
|
-
onboarding_completed_at: adaptNullableTimestamp(member.onboarding_completed_at),
|
|
79
|
-
last_summary_at: adaptNullableTimestamp(member.last_summary_at),
|
|
80
|
-
profile_image_file:
|
|
81
|
-
member.profile_image_file == null ? null : adaptFile(member.profile_image_file),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function adaptConversation(
|
|
86
|
-
conversation: Shared.Contracts.ConversationExtended
|
|
87
|
-
): Shared.Contracts.ConversationExtended {
|
|
88
|
-
return {
|
|
89
|
-
...conversation,
|
|
90
|
-
created_at: adaptTimestamp(conversation.created_at),
|
|
91
|
-
updated_at: adaptTimestamp(conversation.updated_at),
|
|
92
|
-
last_message_at: adaptNullableTimestamp(conversation.last_message_at),
|
|
93
|
-
last_summary_at: adaptNullableTimestamp(conversation.last_summary_at),
|
|
94
|
-
archived_at: adaptNullableTimestamp(conversation.archived_at),
|
|
95
|
-
preview_members: (conversation.preview_members ?? []).map(adaptPreviewMember),
|
|
96
|
-
creator_member:
|
|
97
|
-
conversation.creator_member == null
|
|
98
|
-
? null
|
|
99
|
-
: adaptPreviewMember(conversation.creator_member),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function adaptThreadSubscription(
|
|
104
|
-
subscription: Shared.Contracts.ThreadSubscriptionExtended
|
|
105
|
-
): Shared.Contracts.ThreadSubscriptionExtended {
|
|
106
|
-
return {
|
|
107
|
-
...subscription,
|
|
108
|
-
created_at: adaptTimestamp(subscription.created_at),
|
|
109
|
-
updated_at: adaptTimestamp(subscription.updated_at),
|
|
110
|
-
last_seen_at: adaptNullableTimestamp(subscription.last_seen_at),
|
|
111
|
-
thread_root: {
|
|
112
|
-
...subscription.thread_root,
|
|
113
|
-
created_at: adaptTimestamp(subscription.thread_root.created_at),
|
|
114
|
-
updated_at: adaptTimestamp(subscription.thread_root.updated_at),
|
|
115
|
-
edited_at: adaptNullableTimestamp(subscription.thread_root.edited_at),
|
|
116
|
-
pinned_at: adaptNullableTimestamp(subscription.thread_root.pinned_at),
|
|
117
|
-
last_reply_at: adaptNullableTimestamp(subscription.thread_root.last_reply_at),
|
|
118
|
-
archived_at: adaptNullableTimestamp(subscription.thread_root.archived_at),
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function adaptMembership(
|
|
124
|
-
membership: Shared.Contracts.MembershipExtended
|
|
125
|
-
): Membership {
|
|
126
|
-
return {
|
|
127
|
-
...membership,
|
|
128
|
-
created_at: adaptTimestamp(membership.created_at),
|
|
129
|
-
updated_at: adaptTimestamp(membership.updated_at),
|
|
130
|
-
last_seen_at: adaptTimestamp(membership.last_seen_at),
|
|
131
|
-
member: adaptMember(membership.member),
|
|
132
|
-
conversation: adaptConversation(membership.conversation),
|
|
133
|
-
thread_subscriptions: (membership.thread_subscriptions ?? []).map(
|
|
134
|
-
adaptThreadSubscription
|
|
135
|
-
),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function adaptMessage(
|
|
140
|
-
message: Shared.Contracts.ConversationMessageExtended
|
|
141
|
-
): Message {
|
|
142
|
-
return {
|
|
143
|
-
...message,
|
|
144
|
-
created_at: adaptTimestamp(message.created_at),
|
|
145
|
-
updated_at: adaptTimestamp(message.updated_at),
|
|
146
|
-
edited_at: adaptNullableTimestamp(message.edited_at),
|
|
147
|
-
pinned_at: adaptNullableTimestamp(message.pinned_at),
|
|
148
|
-
last_reply_at: adaptNullableTimestamp(message.last_reply_at),
|
|
149
|
-
archived_at: adaptNullableTimestamp(message.archived_at),
|
|
150
|
-
author: adaptMember(message.author),
|
|
151
|
-
message_reactions: (message.message_reactions ?? []).map((reaction) => ({
|
|
152
|
-
...reaction,
|
|
153
|
-
created_at: adaptTimestamp(reaction.created_at),
|
|
154
|
-
})),
|
|
155
|
-
replies_preview_members: (message.replies_preview_members ?? []).map(
|
|
156
|
-
adaptPreviewMember
|
|
157
|
-
),
|
|
158
|
-
files: (message.files ?? []).map(adaptFile),
|
|
159
|
-
thread_root_message:
|
|
160
|
-
message.thread_root_message == null
|
|
161
|
-
? null
|
|
162
|
-
: {
|
|
163
|
-
...message.thread_root_message,
|
|
164
|
-
created_at: adaptTimestamp(message.thread_root_message.created_at),
|
|
165
|
-
updated_at: adaptTimestamp(message.thread_root_message.updated_at),
|
|
166
|
-
edited_at: adaptNullableTimestamp(message.thread_root_message.edited_at),
|
|
167
|
-
pinned_at: adaptNullableTimestamp(message.thread_root_message.pinned_at),
|
|
168
|
-
last_reply_at: adaptNullableTimestamp(message.thread_root_message.last_reply_at),
|
|
169
|
-
archived_at: adaptNullableTimestamp(message.thread_root_message.archived_at),
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function adaptWorkspace(
|
|
175
|
-
workspace: Shared.Contracts.WorkspaceExtended
|
|
176
|
-
): Shared.Contracts.WorkspaceExtended {
|
|
177
|
-
return {
|
|
178
|
-
...workspace,
|
|
179
|
-
created_at: adaptTimestamp(workspace.created_at),
|
|
180
|
-
updated_at: adaptTimestamp(workspace.updated_at),
|
|
181
|
-
workspace_invite_link: {
|
|
182
|
-
...workspace.workspace_invite_link,
|
|
183
|
-
created_at: adaptTimestamp(workspace.workspace_invite_link.created_at),
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function adaptMe(member: Shared.Contracts.MeExtended): Me {
|
|
189
|
-
return {
|
|
190
|
-
...adaptMember(member),
|
|
191
|
-
workspace: adaptWorkspace(member.workspace),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function adaptBootstrap(result: BootstrapResult) {
|
|
196
|
-
return {
|
|
197
|
-
me:
|
|
198
|
-
result.me == null
|
|
199
|
-
? null
|
|
200
|
-
: adaptMe(result.me as unknown as Shared.Contracts.MeExtended),
|
|
201
|
-
memberships: result.memberships.map((membership) =>
|
|
202
|
-
adaptMembership(membership as unknown as Shared.Contracts.MembershipExtended)
|
|
203
|
-
),
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function adaptConversationPage(
|
|
208
|
-
result: ConvexPaginatedResult<Shared.Contracts.ConversationMessageExtended>
|
|
209
|
-
): ConversationPage {
|
|
210
|
-
return {
|
|
211
|
-
items: [...result.page].reverse().map(adaptMessage),
|
|
212
|
-
cursor: result.isDone ? null : result.continueCursor,
|
|
213
|
-
hasMore: !result.isDone,
|
|
214
|
-
};
|
|
215
|
-
}
|
package/src/args.test.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { getStringFlag, hasFlag, parseArgs } from "./args.js";
|
|
3
|
-
|
|
4
|
-
describe("args", () => {
|
|
5
|
-
it("parses long and short flags with values", () => {
|
|
6
|
-
const parsed = parseArgs([
|
|
7
|
-
"list-messages",
|
|
8
|
-
"--channel",
|
|
9
|
-
"engineering",
|
|
10
|
-
"-n",
|
|
11
|
-
"20",
|
|
12
|
-
"--json",
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
expect(parsed.positionals).toEqual(["list-messages"]);
|
|
16
|
-
expect(getStringFlag(parsed, "channel", "c")).toBe("engineering");
|
|
17
|
-
expect(getStringFlag(parsed, "limit", "n")).toBe("20");
|
|
18
|
-
expect(hasFlag(parsed, "json")).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("parses compact short flags", () => {
|
|
22
|
-
const parsed = parseArgs(["thread", "-vhm", "message-1"]);
|
|
23
|
-
|
|
24
|
-
expect(hasFlag(parsed, "verbose", "v")).toBe(true);
|
|
25
|
-
expect(hasFlag(parsed, "help", "h")).toBe(true);
|
|
26
|
-
expect(getStringFlag(parsed, "message-id", "m")).toBe("message-1");
|
|
27
|
-
});
|
|
28
|
-
});
|
package/src/args.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { ParsedArgs } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export function parseArgs(argv: string[]): ParsedArgs {
|
|
4
|
-
const flags = new Map<string, string | true>();
|
|
5
|
-
const positionals: string[] = [];
|
|
6
|
-
|
|
7
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
8
|
-
const value = argv[index];
|
|
9
|
-
if (value == null) {
|
|
10
|
-
continue;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (!value.startsWith("-")) {
|
|
14
|
-
positionals.push(value);
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (value.startsWith("--")) {
|
|
19
|
-
const [key, inlineValue] = value.slice(2).split("=", 2);
|
|
20
|
-
if (inlineValue != null) {
|
|
21
|
-
flags.set(key, inlineValue);
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const nextValue = argv[index + 1];
|
|
26
|
-
if (nextValue != null && !nextValue.startsWith("-")) {
|
|
27
|
-
flags.set(key, nextValue);
|
|
28
|
-
index += 1;
|
|
29
|
-
} else {
|
|
30
|
-
flags.set(key, true);
|
|
31
|
-
}
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const shortFlags = value.slice(1);
|
|
36
|
-
if (shortFlags.length > 1) {
|
|
37
|
-
for (let shortIndex = 0; shortIndex < shortFlags.length - 1; shortIndex += 1) {
|
|
38
|
-
const shortFlag = shortFlags[shortIndex];
|
|
39
|
-
if (shortFlag != null) {
|
|
40
|
-
flags.set(shortFlag, true);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const lastFlag = shortFlags[shortFlags.length - 1];
|
|
45
|
-
const nextValue = argv[index + 1];
|
|
46
|
-
if (lastFlag == null) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (nextValue != null && !nextValue.startsWith("-")) {
|
|
51
|
-
flags.set(lastFlag, nextValue);
|
|
52
|
-
index += 1;
|
|
53
|
-
} else {
|
|
54
|
-
flags.set(lastFlag, true);
|
|
55
|
-
}
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const nextValue = argv[index + 1];
|
|
60
|
-
if (nextValue != null && !nextValue.startsWith("-")) {
|
|
61
|
-
flags.set(shortFlags, nextValue);
|
|
62
|
-
index += 1;
|
|
63
|
-
} else {
|
|
64
|
-
flags.set(shortFlags, true);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
flags,
|
|
70
|
-
positionals,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function getStringFlag(
|
|
75
|
-
parsedArgs: ParsedArgs,
|
|
76
|
-
longName: string,
|
|
77
|
-
shortName?: string
|
|
78
|
-
) {
|
|
79
|
-
const longValue = parsedArgs.flags.get(longName);
|
|
80
|
-
if (typeof longValue === "string") {
|
|
81
|
-
return longValue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (shortName == null) {
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const shortValue = parsedArgs.flags.get(shortName);
|
|
89
|
-
return typeof shortValue === "string" ? shortValue : undefined;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function hasFlag(
|
|
93
|
-
parsedArgs: ParsedArgs,
|
|
94
|
-
longName: string,
|
|
95
|
-
shortName?: string
|
|
96
|
-
) {
|
|
97
|
-
return parsedArgs.flags.has(longName) || (shortName != null && parsedArgs.flags.has(shortName));
|
|
98
|
-
}
|
package/src/cli-helpers.test.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import * as Shared from "@ando/shared";
|
|
3
|
-
import { compareByRecentActivity, resolveConversation, resolveWorkspace } from "./cli-helpers.js";
|
|
4
|
-
import { createConversation, createMembership } from "./test-helpers.js";
|
|
5
|
-
|
|
6
|
-
describe("cli helpers", () => {
|
|
7
|
-
it("resolves a conversation by exact name", () => {
|
|
8
|
-
const memberships = [
|
|
9
|
-
createMembership({
|
|
10
|
-
id: "membership-a",
|
|
11
|
-
conversation: createConversation({ id: "conversation-a", name: "engineering" }),
|
|
12
|
-
}),
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const result = resolveConversation(memberships, {
|
|
16
|
-
allowChannels: true,
|
|
17
|
-
allowDms: true,
|
|
18
|
-
query: "engineering",
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
expect(result.id).toBe("membership-a");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("throws when a partial conversation match is ambiguous", () => {
|
|
25
|
-
const memberships = [
|
|
26
|
-
createMembership({
|
|
27
|
-
id: "membership-a",
|
|
28
|
-
conversation: createConversation({ id: "conversation-a", name: "eng-platform" }),
|
|
29
|
-
}),
|
|
30
|
-
createMembership({
|
|
31
|
-
id: "membership-b",
|
|
32
|
-
conversation: createConversation({ id: "conversation-b", name: "eng-mobile" }),
|
|
33
|
-
}),
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
expect(() =>
|
|
37
|
-
resolveConversation(memberships, {
|
|
38
|
-
allowChannels: true,
|
|
39
|
-
allowDms: true,
|
|
40
|
-
query: "eng",
|
|
41
|
-
})
|
|
42
|
-
).toThrow(/ambiguous/);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("sorts more recent memberships first", () => {
|
|
46
|
-
const oldMembership = createMembership({
|
|
47
|
-
id: "old",
|
|
48
|
-
conversation: createConversation({
|
|
49
|
-
id: "conversation-old",
|
|
50
|
-
last_message_at: new Date("2026-04-08T12:00:00.000Z"),
|
|
51
|
-
}),
|
|
52
|
-
});
|
|
53
|
-
const newMembership = createMembership({
|
|
54
|
-
id: "new",
|
|
55
|
-
conversation: createConversation({
|
|
56
|
-
id: "conversation-new",
|
|
57
|
-
last_message_at: new Date("2026-04-09T12:00:00.000Z"),
|
|
58
|
-
}),
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
expect(compareByRecentActivity(newMembership, oldMembership)).toBeLessThan(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("resolves workspaces by slug", () => {
|
|
65
|
-
const workspace = {
|
|
66
|
-
id: "workspace-1",
|
|
67
|
-
stytch_organization_id: "org-1",
|
|
68
|
-
name: "Ando",
|
|
69
|
-
slug: "ando",
|
|
70
|
-
image_url: null,
|
|
71
|
-
icon_image_file_id: null,
|
|
72
|
-
description: null,
|
|
73
|
-
invite_link_id: "invite-1",
|
|
74
|
-
created_at: new Date("2026-04-09T12:00:00.000Z"),
|
|
75
|
-
updated_at: new Date("2026-04-09T12:00:00.000Z"),
|
|
76
|
-
member_count: 3,
|
|
77
|
-
} satisfies Shared.Contracts.DiscoveredWorkspace;
|
|
78
|
-
|
|
79
|
-
const result = resolveWorkspace([workspace], "ando");
|
|
80
|
-
expect(result.id).toBe("workspace-1");
|
|
81
|
-
});
|
|
82
|
-
});
|
package/src/cli-helpers.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import * as Shared from "@ando/shared";
|
|
2
|
-
import { DiscoveredWorkspace, Membership } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export function isChannel(membership: Membership) {
|
|
5
|
-
return membership.conversation.type === Shared.Models.ConversationType.CHANNEL;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function normalizeForMatch(value: string) {
|
|
9
|
-
return value.trim().toLowerCase();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function compareByRecentActivity(left: Membership, right: Membership) {
|
|
13
|
-
const leftDate = new Date(
|
|
14
|
-
left.conversation.last_message_at ?? left.conversation.updated_at
|
|
15
|
-
).getTime();
|
|
16
|
-
const rightDate = new Date(
|
|
17
|
-
right.conversation.last_message_at ?? right.conversation.updated_at
|
|
18
|
-
).getTime();
|
|
19
|
-
|
|
20
|
-
return rightDate - leftDate;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getWorkspaceLabel(workspace: DiscoveredWorkspace) {
|
|
24
|
-
return `${workspace.name} (${workspace.slug})`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function resolveWorkspace(
|
|
28
|
-
workspaces: DiscoveredWorkspace[],
|
|
29
|
-
query: string | null
|
|
30
|
-
) {
|
|
31
|
-
if (workspaces.length === 0) {
|
|
32
|
-
throw new Error("No workspaces were returned for this account.");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (query == null || query.trim() === "") {
|
|
36
|
-
if (workspaces.length === 1) {
|
|
37
|
-
return workspaces[0] ?? null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
throw new Error("Multiple workspaces are available. Pass --workspace or choose one interactively.");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const normalizedQuery = normalizeForMatch(query);
|
|
44
|
-
const exactMatch =
|
|
45
|
-
workspaces.find((workspace) => {
|
|
46
|
-
return (
|
|
47
|
-
workspace.id === query ||
|
|
48
|
-
normalizeForMatch(workspace.slug) === normalizedQuery ||
|
|
49
|
-
normalizeForMatch(workspace.name) === normalizedQuery
|
|
50
|
-
);
|
|
51
|
-
}) ?? null;
|
|
52
|
-
|
|
53
|
-
if (exactMatch != null) {
|
|
54
|
-
return exactMatch;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const partialMatches = workspaces.filter((workspace) => {
|
|
58
|
-
return [workspace.id, workspace.slug, workspace.name].some((value) =>
|
|
59
|
-
normalizeForMatch(value).includes(normalizedQuery)
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (partialMatches.length === 1) {
|
|
64
|
-
return partialMatches[0] ?? null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (partialMatches.length === 0) {
|
|
68
|
-
throw new Error(`No workspace matched "${query}".`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
throw new Error(
|
|
72
|
-
`Workspace "${query}" is ambiguous: ${partialMatches
|
|
73
|
-
.slice(0, 8)
|
|
74
|
-
.map(getWorkspaceLabel)
|
|
75
|
-
.join(", ")}`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function resolveConversation(
|
|
80
|
-
memberships: Membership[],
|
|
81
|
-
options: {
|
|
82
|
-
allowDms: boolean;
|
|
83
|
-
allowChannels: boolean;
|
|
84
|
-
query: string;
|
|
85
|
-
}
|
|
86
|
-
) {
|
|
87
|
-
const filtered = memberships
|
|
88
|
-
.filter((membership) => {
|
|
89
|
-
if (!options.allowChannels && isChannel(membership)) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!options.allowDms && !isChannel(membership)) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return true;
|
|
98
|
-
})
|
|
99
|
-
.sort(compareByRecentActivity);
|
|
100
|
-
|
|
101
|
-
const normalizedQuery = normalizeForMatch(options.query);
|
|
102
|
-
const exactMatch =
|
|
103
|
-
filtered.find((membership) => {
|
|
104
|
-
return (
|
|
105
|
-
membership.conversation.id === options.query ||
|
|
106
|
-
normalizeForMatch(membership.conversation.name ?? "") === normalizedQuery
|
|
107
|
-
);
|
|
108
|
-
}) ?? null;
|
|
109
|
-
|
|
110
|
-
if (exactMatch != null) {
|
|
111
|
-
return exactMatch;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const partialMatches = filtered.filter((membership) => {
|
|
115
|
-
const fields = [
|
|
116
|
-
membership.conversation.id,
|
|
117
|
-
membership.conversation.name ?? "",
|
|
118
|
-
getConversationLabel(membership),
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
return fields.some((value) =>
|
|
122
|
-
normalizeForMatch(value).includes(normalizedQuery)
|
|
123
|
-
);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (partialMatches.length === 1) {
|
|
127
|
-
return partialMatches[0] ?? null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (partialMatches.length === 0) {
|
|
131
|
-
throw new Error(`No conversation matched "${options.query}".`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
throw new Error(
|
|
135
|
-
`Conversation "${options.query}" is ambiguous: ${partialMatches
|
|
136
|
-
.slice(0, 8)
|
|
137
|
-
.map((membership) => getConversationLabel(membership))
|
|
138
|
-
.join(", ")}`
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function getConversationLabel(membership: Membership) {
|
|
143
|
-
const prefix =
|
|
144
|
-
membership.conversation.type === Shared.Models.ConversationType.CHANNEL
|
|
145
|
-
? "#"
|
|
146
|
-
: "@";
|
|
147
|
-
|
|
148
|
-
return `${prefix}${membership.conversation.name || membership.conversation.id}`;
|
|
149
|
-
}
|