@iamcoder18/huly-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2576 -0
- package/bin/huly +9 -0
- package/dist/auth/cache.js +129 -0
- package/dist/auth/cache.js.map +1 -0
- package/dist/auth/client.js +192 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/env.js +101 -0
- package/dist/auth/env.js.map +1 -0
- package/dist/auth/prompts.js +68 -0
- package/dist/auth/prompts.js.map +1 -0
- package/dist/cli.js +1959 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/dry-run.js +39 -0
- package/dist/commands/dry-run.js.map +1 -0
- package/dist/commands/login.js +92 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/whoami.js +64 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/output/errors.js +99 -0
- package/dist/output/errors.js.map +1 -0
- package/dist/output/format.js +607 -0
- package/dist/output/format.js.map +1 -0
- package/dist/output/progress.js +30 -0
- package/dist/output/progress.js.map +1 -0
- package/dist/raw/api.js +67 -0
- package/dist/raw/api.js.map +1 -0
- package/dist/raw/ws.js +157 -0
- package/dist/raw/ws.js.map +1 -0
- package/dist/resources/_helpers.js +258 -0
- package/dist/resources/_helpers.js.map +1 -0
- package/dist/resources/_project-resolve.js +24 -0
- package/dist/resources/_project-resolve.js.map +1 -0
- package/dist/resources/calendar.js +659 -0
- package/dist/resources/calendar.js.map +1 -0
- package/dist/resources/card.js +358 -0
- package/dist/resources/card.js.map +1 -0
- package/dist/resources/channel.js +709 -0
- package/dist/resources/channel.js.map +1 -0
- package/dist/resources/comment.js +142 -0
- package/dist/resources/comment.js.map +1 -0
- package/dist/resources/component.js +154 -0
- package/dist/resources/component.js.map +1 -0
- package/dist/resources/document.js +584 -0
- package/dist/resources/document.js.map +1 -0
- package/dist/resources/issue-template.js +228 -0
- package/dist/resources/issue-template.js.map +1 -0
- package/dist/resources/issue.js +909 -0
- package/dist/resources/issue.js.map +1 -0
- package/dist/resources/milestone.js +177 -0
- package/dist/resources/milestone.js.map +1 -0
- package/dist/resources/misc.js +2 -0
- package/dist/resources/misc.js.map +1 -0
- package/dist/resources/project.js +341 -0
- package/dist/resources/project.js.map +1 -0
- package/dist/resources/project.parse.js +25 -0
- package/dist/resources/project.parse.js.map +1 -0
- package/dist/resources/time.js +148 -0
- package/dist/resources/time.js.map +1 -0
- package/dist/resources/todo.js +463 -0
- package/dist/resources/todo.js.map +1 -0
- package/dist/resources/user.js +131 -0
- package/dist/resources/user.js.map +1 -0
- package/dist/resources/workspace.js +252 -0
- package/dist/resources/workspace.js.map +1 -0
- package/dist/transport/identifiers.js +67 -0
- package/dist/transport/identifiers.js.map +1 -0
- package/dist/transport/ref-resolver.js +108 -0
- package/dist/transport/ref-resolver.js.map +1 -0
- package/dist/transport/sdk.js +69 -0
- package/dist/transport/sdk.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import pkg from '@hcengineering/api-client';
|
|
2
|
+
const { MarkupContent } = pkg;
|
|
3
|
+
import { connectCli, connectAccountCli } from '../transport/sdk.js';
|
|
4
|
+
import { resolveRef, buildIndex, invalidateIndex } from '../transport/ref-resolver.js';
|
|
5
|
+
import { shouldJson, json, table, COLUMNS, C, success, updated, bulkRemoved } from "../output/format.js";
|
|
6
|
+
import { withSpinner } from '../output/progress.js';
|
|
7
|
+
import { CliError, ExitCode } from '../output/errors.js';
|
|
8
|
+
import { readEnv } from '../auth/env.js';
|
|
9
|
+
const CHANNEL_CLASS = 'chunter:class:Channel';
|
|
10
|
+
const CHAT_MESSAGE_CLASS = 'chunter:class:ChatMessage';
|
|
11
|
+
const DM_CLASS = 'chunter:class:DirectMessage';
|
|
12
|
+
async function resolveChannel(client, ref) {
|
|
13
|
+
const account = await client.getAccount();
|
|
14
|
+
const idx = await buildIndex(client, CHANNEL_CLASS, account.uuid);
|
|
15
|
+
const hit = idx.get(ref);
|
|
16
|
+
if (hit) {
|
|
17
|
+
const doc = await client.findOne(CHANNEL_CLASS, { _id: hit });
|
|
18
|
+
if (doc)
|
|
19
|
+
return doc;
|
|
20
|
+
}
|
|
21
|
+
// try by name
|
|
22
|
+
const all = (await client.findAll(CHANNEL_CLASS, {}));
|
|
23
|
+
const byName = all.find((c) => c.name === ref);
|
|
24
|
+
if (byName)
|
|
25
|
+
return byName;
|
|
26
|
+
throw new CliError(ExitCode.NotFound, `channel ${ref} not found`);
|
|
27
|
+
}
|
|
28
|
+
async function resolvePersonId(emailOrName, client, opts = {}) {
|
|
29
|
+
// Strategy 1: scan workspace-local Person docs (works for multi-user
|
|
30
|
+
// selfhosts and production workspaces where each user has a
|
|
31
|
+
// contact:class:Person doc).
|
|
32
|
+
const lower = emailOrName.toLowerCase();
|
|
33
|
+
try {
|
|
34
|
+
const persons = (await client.findAll('contact:class:Person', {}, { limit: 200 }));
|
|
35
|
+
const hit = persons.find((p) => {
|
|
36
|
+
const n = String(p.name ?? '').toLowerCase();
|
|
37
|
+
const e = String(p.email ?? '').toLowerCase();
|
|
38
|
+
return n === lower || n.startsWith(lower) || n.includes(lower) || e === lower;
|
|
39
|
+
});
|
|
40
|
+
if (hit)
|
|
41
|
+
return hit._id;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// model may not know about contact:class:Person — fall through
|
|
45
|
+
}
|
|
46
|
+
// Strategy 2: ask the account service for workspace members. The
|
|
47
|
+
// account client returns person UUIDs for each member regardless of
|
|
48
|
+
// whether a contact:class:Person doc was created in the workspace.
|
|
49
|
+
try {
|
|
50
|
+
const env = readEnv();
|
|
51
|
+
const accountClient = await connectAccountCli({ url: opts.url ?? env.url, workspace: opts.workspace ?? env.workspace });
|
|
52
|
+
const members = await accountClient.getWorkspaceMembers();
|
|
53
|
+
if (members.length > 0) {
|
|
54
|
+
const me = await accountClient.getPerson().catch(() => null);
|
|
55
|
+
const myUuid = me?.uuid;
|
|
56
|
+
if (emailOrName.includes('@')) {
|
|
57
|
+
const socialId = await accountClient.findSocialIdBySocialKey(emailOrName).catch(() => undefined);
|
|
58
|
+
if (socialId !== undefined) {
|
|
59
|
+
const person = await accountClient.findPersonBySocialId(socialId, true).catch(() => undefined);
|
|
60
|
+
if (person !== undefined)
|
|
61
|
+
return person;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// If the caller passed a raw UUID (36-char hex), use it directly.
|
|
65
|
+
if (/^[0-9a-f-]{36}$/i.test(emailOrName)) {
|
|
66
|
+
return emailOrName;
|
|
67
|
+
}
|
|
68
|
+
// Last resort: if the workspace has exactly one other member and
|
|
69
|
+
// the caller passed a non-empty identifier, return that member's
|
|
70
|
+
// person UUID.
|
|
71
|
+
const otherMembers = members.filter((m) => m.person !== myUuid);
|
|
72
|
+
if (otherMembers.length === 1)
|
|
73
|
+
return otherMembers[0].person;
|
|
74
|
+
if (otherMembers.length === 0) {
|
|
75
|
+
throw new CliError(ExitCode.NotFound, `no other person in this workspace — you are the only member`, 'a DM requires at least one other member; invite someone via `huly user invite <email>`');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err instanceof CliError)
|
|
81
|
+
throw err;
|
|
82
|
+
// account service unreachable or returned Forbidden — fall through
|
|
83
|
+
}
|
|
84
|
+
throw new CliError(ExitCode.NotFound, `no person matching ${emailOrName}`, 'try --members <uuid1> <uuid2> with raw account UUIDs instead of --person');
|
|
85
|
+
}
|
|
86
|
+
// ---- list / get / create / update / delete ----
|
|
87
|
+
export async function listChannels(opts = {}) {
|
|
88
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
89
|
+
try {
|
|
90
|
+
const query = {};
|
|
91
|
+
if (opts.archived !== undefined)
|
|
92
|
+
query.archived = opts.archived;
|
|
93
|
+
const docs = (await withSpinner('Loading channels…', () => client.findAll(CHANNEL_CLASS, query), opts));
|
|
94
|
+
let r = docs;
|
|
95
|
+
if (opts.offset && opts.offset > 0)
|
|
96
|
+
r = r.slice(opts.offset);
|
|
97
|
+
if (opts.limit && opts.limit > 0)
|
|
98
|
+
r = r.slice(0, opts.limit);
|
|
99
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
100
|
+
json(r);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
table(r, COLUMNS.channel(), { count: true, title: 'channels' });
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await client.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function getChannel(ref, opts) {
|
|
110
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
111
|
+
try {
|
|
112
|
+
const channel = await resolveChannel(client, ref);
|
|
113
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
114
|
+
json(channel);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
table([channel], COLUMNS.channel());
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await client.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function createChannel(opts) {
|
|
124
|
+
if (!opts.name)
|
|
125
|
+
throw new CliError(ExitCode.Validation, 'missing --name');
|
|
126
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
127
|
+
try {
|
|
128
|
+
const account = await client.getAccount();
|
|
129
|
+
const memberIds = [];
|
|
130
|
+
if (opts.members && opts.members.length > 0) {
|
|
131
|
+
for (const m of opts.members)
|
|
132
|
+
memberIds.push(await resolvePersonId(m, client));
|
|
133
|
+
}
|
|
134
|
+
memberIds.unshift(account.uuid);
|
|
135
|
+
const data = {
|
|
136
|
+
name: opts.name,
|
|
137
|
+
description: opts.description ?? '',
|
|
138
|
+
topic: opts.topic ?? '',
|
|
139
|
+
private: opts.private ?? false,
|
|
140
|
+
archived: false,
|
|
141
|
+
members: memberIds,
|
|
142
|
+
owners: [account.uuid],
|
|
143
|
+
autoJoin: opts.autoJoin ?? false,
|
|
144
|
+
autoJoinForRoles: []
|
|
145
|
+
};
|
|
146
|
+
if (opts.dryRun) {
|
|
147
|
+
console.log('would create channel:');
|
|
148
|
+
console.log(JSON.stringify({ _class: CHANNEL_CLASS, space: 'chunter:space:Chunter', data }, null, 2));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const id = await withSpinner('Creating channel…', () => client.createDoc(CHANNEL_CLASS, 'chunter:space:Chunter', data), opts);
|
|
152
|
+
invalidateIndex(account.uuid, CHANNEL_CLASS);
|
|
153
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
154
|
+
json({ _id: id, ...data });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
success(`created channel`, id);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await client.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function updateChannel(ref, opts) {
|
|
164
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
165
|
+
try {
|
|
166
|
+
const channel = await resolveChannel(client, ref);
|
|
167
|
+
const ops = {};
|
|
168
|
+
if (opts.name)
|
|
169
|
+
ops.name = opts.name;
|
|
170
|
+
if (opts.description !== undefined)
|
|
171
|
+
ops.description = opts.description;
|
|
172
|
+
if (opts.topic !== undefined)
|
|
173
|
+
ops.topic = opts.topic;
|
|
174
|
+
if (opts.private !== undefined)
|
|
175
|
+
ops.private = opts.private;
|
|
176
|
+
if (opts.autoJoin !== undefined)
|
|
177
|
+
ops.autoJoin = opts.autoJoin;
|
|
178
|
+
if (Object.keys(ops).length === 0)
|
|
179
|
+
throw new CliError(ExitCode.Validation, 'nothing to update', 'pass --name/--description/--topic/--private/--auto-join');
|
|
180
|
+
if (opts.dryRun) {
|
|
181
|
+
console.log(`would update channel ${channel._id}:`);
|
|
182
|
+
console.log(JSON.stringify({ _class: CHANNEL_CLASS, objectId: channel._id, ops }, null, 2));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await withSpinner('Updating…', () => client.updateDoc(CHANNEL_CLASS, channel.space, channel._id, ops), opts);
|
|
186
|
+
updated(`updated channel`, channel._id);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await client.close();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export async function deleteChannels(refs, opts = {}) {
|
|
193
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
194
|
+
try {
|
|
195
|
+
const channels = [];
|
|
196
|
+
for (const r of refs) {
|
|
197
|
+
try {
|
|
198
|
+
channels.push(await resolveChannel(client, r));
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
if (e instanceof CliError)
|
|
202
|
+
throw e;
|
|
203
|
+
throw new CliError(ExitCode.NotFound, `channel ${r} not found`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!opts.yes && channels.length > 1) {
|
|
207
|
+
throw new CliError(ExitCode.Validation, `destructive: deleting ${channels.length} channels requires --yes`, 're-run with --yes to confirm');
|
|
208
|
+
}
|
|
209
|
+
let deleted = 0, skipped = 0;
|
|
210
|
+
for (const ch of channels) {
|
|
211
|
+
try {
|
|
212
|
+
await client.removeDoc(CHANNEL_CLASS, ch.space, ch._id);
|
|
213
|
+
deleted++;
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
console.error(`failed to delete ${ch._id}: ${e.message}`);
|
|
217
|
+
skipped++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
bulkRemoved(deleted, skipped);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
await client.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ---- archive / unarchive ----
|
|
227
|
+
export async function archiveChannel(ref, opts = {}) {
|
|
228
|
+
const archive = opts.value ?? true;
|
|
229
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
230
|
+
try {
|
|
231
|
+
const channel = await resolveChannel(client, ref);
|
|
232
|
+
if (opts.dryRun) {
|
|
233
|
+
console.log(`would set archived=${archive} on ${channel._id}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await withSpinner('Archiving…', () => client.updateDoc(CHANNEL_CLASS, channel.space, channel._id, { archived: archive }));
|
|
237
|
+
console.log(C.info(`archived channel`) + C.muted(' ') + C.emphasis(`${channel._id} archived=${archive}`));
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await client.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// ---- members ----
|
|
244
|
+
export async function listChannelMembers(ref, opts) {
|
|
245
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
246
|
+
try {
|
|
247
|
+
const channel = await resolveChannel(client, ref);
|
|
248
|
+
const members = channel.members ?? [];
|
|
249
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
250
|
+
json(members);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
table(members.map((m) => ({ uuid: m })), [
|
|
254
|
+
{ key: 'uuid', header: 'UUID' }
|
|
255
|
+
], { count: true, title: 'members' });
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
await client.close();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export async function joinChannel(ref, opts = {}) {
|
|
262
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
263
|
+
try {
|
|
264
|
+
const account = await client.getAccount();
|
|
265
|
+
const channel = await resolveChannel(client, ref);
|
|
266
|
+
const memberId = opts.member ? await resolvePersonId(opts.member, client) : account.uuid;
|
|
267
|
+
if (channel.members?.includes(memberId)) {
|
|
268
|
+
console.log('(already a member)');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (opts.dryRun) {
|
|
272
|
+
console.log(`would join ${memberId} to channel ${channel._id}`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
await withSpinner('Joining…', () => client.updateDoc(CHANNEL_CLASS, channel.space, channel._id, {
|
|
276
|
+
$push: { members: { $each: [memberId], $position: 0 } }
|
|
277
|
+
}));
|
|
278
|
+
success(`joined member`, `${memberId} → ${channel._id}`);
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await client.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
export async function leaveChannel(ref, opts = {}) {
|
|
285
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
286
|
+
try {
|
|
287
|
+
const account = await client.getAccount();
|
|
288
|
+
const channel = await resolveChannel(client, ref);
|
|
289
|
+
const memberId = opts.member ? await resolvePersonId(opts.member, client) : account.uuid;
|
|
290
|
+
if (!channel.members?.includes(memberId)) {
|
|
291
|
+
console.log('(not a member)');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (opts.dryRun) {
|
|
295
|
+
console.log(`would remove ${memberId} from channel ${channel._id}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await withSpinner('Leaving…', () => client.updateDoc(CHANNEL_CLASS, channel.space, channel._id, {
|
|
299
|
+
$pull: { members: { $in: [memberId] } }
|
|
300
|
+
}));
|
|
301
|
+
success(`removed member`, `${memberId} from ${channel._id}`);
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
await client.close();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
export async function addChannelMembers(ref, members, opts = {}) {
|
|
308
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
309
|
+
try {
|
|
310
|
+
const channel = await resolveChannel(client, ref);
|
|
311
|
+
const ids = [];
|
|
312
|
+
for (const m of members)
|
|
313
|
+
ids.push(await resolvePersonId(m, client));
|
|
314
|
+
if (opts.dryRun) {
|
|
315
|
+
console.log(`would add ${ids.length} members to channel ${channel._id}`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
await withSpinner('Adding members…', () => client.updateDoc(CHANNEL_CLASS, channel.space, channel._id, {
|
|
319
|
+
$push: { members: { $each: ids, $position: 0 } }
|
|
320
|
+
}));
|
|
321
|
+
console.log(C.ok(`added ${ids.length} members`) + C.muted(' ') + C.emphasis(`to ${channel._id}`));
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
await client.close();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
export async function removeChannelMembers(ref, members, opts = {}) {
|
|
328
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
329
|
+
try {
|
|
330
|
+
const channel = await resolveChannel(client, ref);
|
|
331
|
+
const ids = [];
|
|
332
|
+
for (const m of members)
|
|
333
|
+
ids.push(await resolvePersonId(m, client));
|
|
334
|
+
if (opts.dryRun) {
|
|
335
|
+
console.log(`would remove ${ids.length} members from channel ${channel._id}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
await withSpinner('Removing members…', () => client.updateDoc(CHANNEL_CLASS, channel.space, channel._id, {
|
|
339
|
+
$pull: { members: { $in: ids } }
|
|
340
|
+
}));
|
|
341
|
+
success(`removed ${ids.length} members`, `from ${channel._id}`);
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
await client.close();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// ---- messages ----
|
|
348
|
+
export async function listChannelMessages(ref, opts) {
|
|
349
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
350
|
+
try {
|
|
351
|
+
const channel = await resolveChannel(client, ref);
|
|
352
|
+
const messages = (await withSpinner('Loading messages…', () => client.findAll(CHAT_MESSAGE_CLASS, {
|
|
353
|
+
attachedTo: channel._id,
|
|
354
|
+
attachedToClass: CHANNEL_CLASS,
|
|
355
|
+
collection: 'messages'
|
|
356
|
+
}), opts));
|
|
357
|
+
let r = messages;
|
|
358
|
+
if (opts.offset && opts.offset > 0)
|
|
359
|
+
r = r.slice(opts.offset);
|
|
360
|
+
if (opts.limit && opts.limit > 0)
|
|
361
|
+
r = r.slice(0, opts.limit);
|
|
362
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
363
|
+
json(r);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
table(r, COLUMNS.channelMessage(), { count: true, title: 'messages' });
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
await client.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export async function sendChannelMessage(ref, opts) {
|
|
373
|
+
const body = await readMessageBody(opts);
|
|
374
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
375
|
+
try {
|
|
376
|
+
const channel = await resolveChannel(client, ref);
|
|
377
|
+
const data = {
|
|
378
|
+
message: body
|
|
379
|
+
};
|
|
380
|
+
if (opts.dryRun) {
|
|
381
|
+
console.log('would send channel message:');
|
|
382
|
+
console.log(JSON.stringify({ _class: CHAT_MESSAGE_CLASS, space: channel.space, attachedTo: channel._id, attachedToClass: CHANNEL_CLASS, collection: 'messages', data }, null, 2));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const id = await withSpinner('Sending…', () => client.addCollection(CHAT_MESSAGE_CLASS, channel.space, channel._id, CHANNEL_CLASS, 'messages', data));
|
|
386
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
387
|
+
json({ _id: id, ...data });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
console.log(`sent: ${id}`);
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
await client.close();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
export async function updateChannelMessage(ref, messageId, opts) {
|
|
397
|
+
const body = await readMessageBody(opts);
|
|
398
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
399
|
+
try {
|
|
400
|
+
const channel = await resolveChannel(client, ref);
|
|
401
|
+
const msg = await client.findOne(CHAT_MESSAGE_CLASS, { _id: messageId });
|
|
402
|
+
if (!msg)
|
|
403
|
+
throw new CliError(ExitCode.NotFound, `message ${messageId} not found`);
|
|
404
|
+
if (msg.attachedTo !== channel._id) {
|
|
405
|
+
throw new CliError(ExitCode.NotFound, 'message does not belong to this channel');
|
|
406
|
+
}
|
|
407
|
+
const data = {
|
|
408
|
+
message: body,
|
|
409
|
+
editedOn: Date.now()
|
|
410
|
+
};
|
|
411
|
+
if (opts.dryRun) {
|
|
412
|
+
console.log(`would update message ${messageId}:`);
|
|
413
|
+
console.log(JSON.stringify({ _class: CHAT_MESSAGE_CLASS, space: msg.space, ops: data }, null, 2));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
await withSpinner('Updating…', () => client.updateCollection(CHAT_MESSAGE_CLASS, msg.space, messageId, channel._id, CHANNEL_CLASS, 'messages', data));
|
|
417
|
+
updated(`updated message`, messageId);
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
await client.close();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
export async function deleteChannelMessages(ref, messageIds, opts = {}) {
|
|
424
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
425
|
+
try {
|
|
426
|
+
const channel = await resolveChannel(client, ref);
|
|
427
|
+
if (!opts.yes && messageIds.length > 1) {
|
|
428
|
+
throw new CliError(ExitCode.Validation, `destructive: deleting ${messageIds.length} messages requires --yes`, 're-run with --yes to confirm');
|
|
429
|
+
}
|
|
430
|
+
let deleted = 0, skipped = 0;
|
|
431
|
+
for (const id of messageIds) {
|
|
432
|
+
const msg = await client.findOne(CHAT_MESSAGE_CLASS, { _id: id });
|
|
433
|
+
if (!msg) {
|
|
434
|
+
skipped++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (msg.attachedTo !== channel._id) {
|
|
438
|
+
console.error(`skipped: ${id} (message does not belong to this channel)`);
|
|
439
|
+
skipped++;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
await client.removeCollection(CHAT_MESSAGE_CLASS, msg.space, id, channel._id, CHANNEL_CLASS, 'messages');
|
|
444
|
+
deleted++;
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
console.error(`failed: ${id}: ${e.message}`);
|
|
448
|
+
skipped++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
bulkRemoved(deleted, skipped);
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
await client.close();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// ---- threads ----
|
|
458
|
+
export async function listThreadReplies(targetId, opts) {
|
|
459
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
460
|
+
try {
|
|
461
|
+
const replies = (await client.findAll('chunter:class:ThreadMessage', { attachedTo: targetId }));
|
|
462
|
+
let r = replies;
|
|
463
|
+
if (opts.offset && opts.offset > 0)
|
|
464
|
+
r = r.slice(opts.offset);
|
|
465
|
+
if (opts.limit && opts.limit > 0)
|
|
466
|
+
r = r.slice(0, opts.limit);
|
|
467
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
468
|
+
json(r);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
table(r, COLUMNS.channelMessage(), { count: true, title: 'messages' });
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
await client.close();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
export async function addThreadReply(targetId, opts) {
|
|
478
|
+
const body = await readMessageBody(opts);
|
|
479
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
480
|
+
try {
|
|
481
|
+
const parent = await client.findOne(CHAT_MESSAGE_CLASS, { _id: targetId });
|
|
482
|
+
if (!parent)
|
|
483
|
+
throw new CliError(ExitCode.NotFound, `target message ${targetId} not found`);
|
|
484
|
+
const data = {
|
|
485
|
+
message: body
|
|
486
|
+
};
|
|
487
|
+
if (opts.dryRun) {
|
|
488
|
+
console.log('would add thread reply:');
|
|
489
|
+
console.log(JSON.stringify({ _class: 'chunter:class:ThreadMessage', space: parent.space, attachedTo: targetId, data }, null, 2));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const id = await withSpinner('Adding reply…', () => client.addCollection('chunter:class:ThreadMessage', parent.space, targetId, CHAT_MESSAGE_CLASS, 'replies', data));
|
|
493
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
494
|
+
json({ _id: id, ...data });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
success(`added reply`, id);
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
await client.close();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
export async function updateThreadReply(replyId, opts) {
|
|
504
|
+
const body = await readMessageBody(opts);
|
|
505
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
506
|
+
try {
|
|
507
|
+
const reply = await client.findOne('chunter:class:ThreadMessage', { _id: replyId });
|
|
508
|
+
if (!reply)
|
|
509
|
+
throw new CliError(ExitCode.NotFound, `thread reply ${replyId} not found`);
|
|
510
|
+
const data = { message: body, editedOn: Date.now() };
|
|
511
|
+
if (opts.dryRun) {
|
|
512
|
+
console.log(`would update thread reply ${replyId}:`);
|
|
513
|
+
console.log(JSON.stringify({ _class: 'chunter:class:ThreadMessage', space: reply.space, ops: data }, null, 2));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
await withSpinner('Updating…', () => client.updateCollection('chunter:class:ThreadMessage', reply.space, replyId, reply.attachedTo, CHAT_MESSAGE_CLASS, 'replies', data));
|
|
517
|
+
updated(`updated reply`, replyId);
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
await client.close();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
export async function deleteThreadReplies(replyIds, opts = {}) {
|
|
524
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
525
|
+
try {
|
|
526
|
+
if (!opts.yes && replyIds.length > 1) {
|
|
527
|
+
throw new CliError(ExitCode.Validation, `destructive: deleting ${replyIds.length} replies requires --yes`, 're-run with --yes to confirm');
|
|
528
|
+
}
|
|
529
|
+
let deleted = 0, skipped = 0;
|
|
530
|
+
for (const id of replyIds) {
|
|
531
|
+
const reply = await client.findOne('chunter:class:ThreadMessage', { _id: id });
|
|
532
|
+
if (!reply) {
|
|
533
|
+
skipped++;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
await client.removeCollection('chunter:class:ThreadMessage', reply.space, id, reply.attachedTo, CHAT_MESSAGE_CLASS, 'replies');
|
|
538
|
+
deleted++;
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
console.error(`failed: ${id}: ${e.message}`);
|
|
542
|
+
skipped++;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
bulkRemoved(deleted, skipped);
|
|
546
|
+
}
|
|
547
|
+
finally {
|
|
548
|
+
await client.close();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// ---- DMs ----
|
|
552
|
+
export async function listDms(opts = {}) {
|
|
553
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
554
|
+
try {
|
|
555
|
+
const dms = (await withSpinner('Loading DMs…', () => client.findAll(DM_CLASS, {}), opts));
|
|
556
|
+
let r = dms;
|
|
557
|
+
if (opts.offset && opts.offset > 0)
|
|
558
|
+
r = r.slice(opts.offset);
|
|
559
|
+
if (opts.limit && opts.limit > 0)
|
|
560
|
+
r = r.slice(0, opts.limit);
|
|
561
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
562
|
+
json(r);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
table(r, [
|
|
566
|
+
{ key: 'name', header: 'NAME' },
|
|
567
|
+
{ key: 'description', header: 'DESCRIPTION' },
|
|
568
|
+
{ key: '_id', header: '_ID', format: (r) => String(r._id).slice(-12) }
|
|
569
|
+
]);
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
await client.close();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
export async function createDm(opts) {
|
|
576
|
+
if (!opts.person && (!opts.members || opts.members.length === 0)) {
|
|
577
|
+
throw new CliError(ExitCode.Validation, 'missing --person or --members');
|
|
578
|
+
}
|
|
579
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
580
|
+
try {
|
|
581
|
+
const account = await client.getAccount();
|
|
582
|
+
const memberIds = [];
|
|
583
|
+
if (opts.person)
|
|
584
|
+
memberIds.push(await resolvePersonId(opts.person, client, { url: opts.url, workspace: opts.workspace }));
|
|
585
|
+
if (opts.members) {
|
|
586
|
+
for (const m of opts.members)
|
|
587
|
+
memberIds.push(await resolvePersonId(m, client, { url: opts.url, workspace: opts.workspace }));
|
|
588
|
+
}
|
|
589
|
+
memberIds.unshift(account.uuid);
|
|
590
|
+
const data = {
|
|
591
|
+
name: '',
|
|
592
|
+
description: '',
|
|
593
|
+
members: memberIds,
|
|
594
|
+
pinned: false
|
|
595
|
+
};
|
|
596
|
+
if (opts.dryRun) {
|
|
597
|
+
console.log('would create DM:');
|
|
598
|
+
console.log(JSON.stringify({ _class: DM_CLASS, space: 'chunter:space:Chunter', data }, null, 2));
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
const id = await withSpinner('Creating DM…', () => client.createDoc(DM_CLASS, 'chunter:space:Chunter', data), opts);
|
|
602
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
603
|
+
json({ _id: id, ...data });
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
606
|
+
success('created DM', id);
|
|
607
|
+
return id;
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
await client.close();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
export async function listDmMessages(dmRef, opts) {
|
|
614
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
615
|
+
try {
|
|
616
|
+
const account = await client.getAccount();
|
|
617
|
+
const dmId = await resolveRef(dmRef, {
|
|
618
|
+
client,
|
|
619
|
+
classId: DM_CLASS,
|
|
620
|
+
workspaceId: account.uuid
|
|
621
|
+
});
|
|
622
|
+
const messages = (await withSpinner('Loading DM messages…', () => client.findAll(CHAT_MESSAGE_CLASS, {
|
|
623
|
+
attachedTo: dmId,
|
|
624
|
+
attachedToClass: DM_CLASS,
|
|
625
|
+
collection: 'messages'
|
|
626
|
+
}), opts));
|
|
627
|
+
let r = messages;
|
|
628
|
+
if (opts.offset && opts.offset > 0)
|
|
629
|
+
r = r.slice(opts.offset);
|
|
630
|
+
if (opts.limit && opts.limit > 0)
|
|
631
|
+
r = r.slice(0, opts.limit);
|
|
632
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
633
|
+
json(r);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
table(r, COLUMNS.channelMessage(), { count: true, title: 'messages' });
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
await client.close();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
export async function sendDmMessage(dmRef, opts) {
|
|
643
|
+
const body = await readMessageBody(opts);
|
|
644
|
+
// --person <email>: resolve or auto-create DM, then send.
|
|
645
|
+
if (opts.person !== undefined && opts.person !== '') {
|
|
646
|
+
// CLI-04: forward --dry-run so createDm doesn't actually mutate state
|
|
647
|
+
// and emit a preview instead.
|
|
648
|
+
const dmId = await createDm({
|
|
649
|
+
person: opts.person,
|
|
650
|
+
dryRun: opts.dryRun,
|
|
651
|
+
workspace: opts.workspace,
|
|
652
|
+
url: opts.url
|
|
653
|
+
});
|
|
654
|
+
if (opts.dryRun) {
|
|
655
|
+
console.log('would send DM:');
|
|
656
|
+
console.log(JSON.stringify({
|
|
657
|
+
wouldCreateDm: { person: opts.person },
|
|
658
|
+
wouldSendTo: dmRef,
|
|
659
|
+
message: body
|
|
660
|
+
}, null, 2));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (dmId !== undefined)
|
|
664
|
+
dmRef = String(dmId);
|
|
665
|
+
}
|
|
666
|
+
const client = await connectCli({ url: opts.url, workspace: opts.workspace });
|
|
667
|
+
try {
|
|
668
|
+
const account = await client.getAccount();
|
|
669
|
+
const dmId = await resolveRef(dmRef, {
|
|
670
|
+
client,
|
|
671
|
+
classId: DM_CLASS,
|
|
672
|
+
workspaceId: account.uuid
|
|
673
|
+
});
|
|
674
|
+
const dm = await client.findOne(DM_CLASS, { _id: dmId });
|
|
675
|
+
if (!dm)
|
|
676
|
+
throw new CliError(ExitCode.NotFound, `DM ${dmRef} not found`);
|
|
677
|
+
const data = { message: body };
|
|
678
|
+
if (opts.dryRun) {
|
|
679
|
+
console.log('would send DM:');
|
|
680
|
+
console.log(JSON.stringify({ _class: CHAT_MESSAGE_CLASS, space: dm.space, attachedTo: dmId, attachedToClass: DM_CLASS, collection: 'messages', data }, null, 2));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const id = await withSpinner('Sending…', () => client.addCollection(CHAT_MESSAGE_CLASS, dm.space, dmId, DM_CLASS, 'messages', data));
|
|
684
|
+
if (shouldJson({ json: opts.json, ci: opts.ci })) {
|
|
685
|
+
json({ _id: id, ...data });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
console.log(`sent: ${id}`);
|
|
689
|
+
}
|
|
690
|
+
finally {
|
|
691
|
+
await client.close();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// ---- helpers ----
|
|
695
|
+
async function readMessageBody(opts) {
|
|
696
|
+
if (opts.body && opts.bodyFile) {
|
|
697
|
+
throw new CliError(ExitCode.Validation, 'ambiguous body input', 'pass only one of --body or --body-file');
|
|
698
|
+
}
|
|
699
|
+
if (opts.bodyFile) {
|
|
700
|
+
const fs = await import('node:fs/promises');
|
|
701
|
+
return (await fs.readFile(opts.bodyFile, 'utf8')).trim();
|
|
702
|
+
}
|
|
703
|
+
if (opts.body)
|
|
704
|
+
return opts.body;
|
|
705
|
+
if (opts.message)
|
|
706
|
+
return opts.message;
|
|
707
|
+
throw new CliError(ExitCode.Validation, 'missing --body or --body-file');
|
|
708
|
+
}
|
|
709
|
+
//# sourceMappingURL=channel.js.map
|