@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.
Files changed (75) hide show
  1. package/README.md +2576 -0
  2. package/bin/huly +9 -0
  3. package/dist/auth/cache.js +129 -0
  4. package/dist/auth/cache.js.map +1 -0
  5. package/dist/auth/client.js +192 -0
  6. package/dist/auth/client.js.map +1 -0
  7. package/dist/auth/env.js +101 -0
  8. package/dist/auth/env.js.map +1 -0
  9. package/dist/auth/prompts.js +68 -0
  10. package/dist/auth/prompts.js.map +1 -0
  11. package/dist/cli.js +1959 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/commands/dry-run.js +39 -0
  14. package/dist/commands/dry-run.js.map +1 -0
  15. package/dist/commands/login.js +92 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/commands/whoami.js +64 -0
  18. package/dist/commands/whoami.js.map +1 -0
  19. package/dist/index.js +59 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/output/errors.js +99 -0
  22. package/dist/output/errors.js.map +1 -0
  23. package/dist/output/format.js +607 -0
  24. package/dist/output/format.js.map +1 -0
  25. package/dist/output/progress.js +30 -0
  26. package/dist/output/progress.js.map +1 -0
  27. package/dist/raw/api.js +67 -0
  28. package/dist/raw/api.js.map +1 -0
  29. package/dist/raw/ws.js +157 -0
  30. package/dist/raw/ws.js.map +1 -0
  31. package/dist/resources/_helpers.js +258 -0
  32. package/dist/resources/_helpers.js.map +1 -0
  33. package/dist/resources/_project-resolve.js +24 -0
  34. package/dist/resources/_project-resolve.js.map +1 -0
  35. package/dist/resources/calendar.js +659 -0
  36. package/dist/resources/calendar.js.map +1 -0
  37. package/dist/resources/card.js +358 -0
  38. package/dist/resources/card.js.map +1 -0
  39. package/dist/resources/channel.js +709 -0
  40. package/dist/resources/channel.js.map +1 -0
  41. package/dist/resources/comment.js +142 -0
  42. package/dist/resources/comment.js.map +1 -0
  43. package/dist/resources/component.js +154 -0
  44. package/dist/resources/component.js.map +1 -0
  45. package/dist/resources/document.js +584 -0
  46. package/dist/resources/document.js.map +1 -0
  47. package/dist/resources/issue-template.js +228 -0
  48. package/dist/resources/issue-template.js.map +1 -0
  49. package/dist/resources/issue.js +909 -0
  50. package/dist/resources/issue.js.map +1 -0
  51. package/dist/resources/milestone.js +177 -0
  52. package/dist/resources/milestone.js.map +1 -0
  53. package/dist/resources/misc.js +2 -0
  54. package/dist/resources/misc.js.map +1 -0
  55. package/dist/resources/project.js +341 -0
  56. package/dist/resources/project.js.map +1 -0
  57. package/dist/resources/project.parse.js +25 -0
  58. package/dist/resources/project.parse.js.map +1 -0
  59. package/dist/resources/time.js +148 -0
  60. package/dist/resources/time.js.map +1 -0
  61. package/dist/resources/todo.js +463 -0
  62. package/dist/resources/todo.js.map +1 -0
  63. package/dist/resources/user.js +131 -0
  64. package/dist/resources/user.js.map +1 -0
  65. package/dist/resources/workspace.js +252 -0
  66. package/dist/resources/workspace.js.map +1 -0
  67. package/dist/transport/identifiers.js +67 -0
  68. package/dist/transport/identifiers.js.map +1 -0
  69. package/dist/transport/ref-resolver.js +108 -0
  70. package/dist/transport/ref-resolver.js.map +1 -0
  71. package/dist/transport/sdk.js +69 -0
  72. package/dist/transport/sdk.js.map +1 -0
  73. package/dist/types.js +2 -0
  74. package/dist/types.js.map +1 -0
  75. package/package.json +40 -0
@@ -0,0 +1,659 @@
1
+ import pkg from '@hcengineering/api-client';
2
+ const { MarkupContent } = pkg;
3
+ import { CLASS } from '../transport/identifiers.js';
4
+ import { connectCli } from '../transport/sdk.js';
5
+ import { resolveRef, resolveRefs, invalidateIndex } from '../transport/ref-resolver.js';
6
+ import { shouldJson, json, table, kv, header, COLUMNS, C, isoDate, relTime, withTimeout, success, updated, removed, bulkRemoved } from "../output/format.js";
7
+ import { withSpinner } from '../output/progress.js';
8
+ import { deleteDoc } from '../commands/dry-run.js';
9
+ import { CliError, ExitCode } from '../output/errors.js';
10
+ function parseDate(value, field) {
11
+ const t = new Date(value).getTime();
12
+ if (Number.isNaN(t))
13
+ throw new CliError(ExitCode.Validation, `invalid ${field}: ${value} (expected ISO date)`);
14
+ return t;
15
+ }
16
+ // ---- calendars ----
17
+ export async function listCalendars(g = {}) {
18
+ const client = await connectCli({ url: g.url, workspace: g.workspace });
19
+ try {
20
+ const docs = (await withSpinner('Loading calendars…', () => client.findAll(CLASS.Calendar, {}), g));
21
+ if (shouldJson({ json: g.json, ci: g.ci })) {
22
+ json(docs);
23
+ return;
24
+ }
25
+ table(docs, [
26
+ { key: 'name', header: 'NAME', format: (r) => C.emphasis(String(r.name ?? '')) },
27
+ { key: 'visibility', header: 'VISIBILITY', format: (r) => {
28
+ const v = String(r.visibility ?? '');
29
+ return v === 'public' ? C.green('public') : v === 'private' ? C.red('private') : C.muted(v);
30
+ } },
31
+ { key: 'access', header: 'ACCESS', format: (r) => {
32
+ const a = String(r.access ?? '');
33
+ return a === 'owner' ? C.cyan('owner') : a;
34
+ } },
35
+ { key: 'hidden', header: 'HIDDEN', width: 8, align: 'center', format: (r) => r.hidden ? C.yellow('yes') : C.muted('no') },
36
+ { key: '_id', header: '_ID', format: (r) => C.id(String(r._id).split(':').slice(-1)[0] ?? String(r._id)) }
37
+ ], { count: true, title: 'calendars' });
38
+ }
39
+ finally {
40
+ await client.close();
41
+ }
42
+ }
43
+ // ---- schedules ----
44
+ export async function createCalendar(opts) {
45
+ if (!opts.name)
46
+ throw new CliError(ExitCode.Validation, 'missing --name');
47
+ const data = {
48
+ name: opts.name,
49
+ description: opts.description ?? '',
50
+ visibility: opts.visibility ?? 'public',
51
+ access: opts.access ?? 'owner',
52
+ private: opts.private ?? false,
53
+ hidden: opts.hidden ?? false
54
+ };
55
+ if (opts.dryRun) {
56
+ console.log('would create calendar:');
57
+ console.log(JSON.stringify({ _class: CLASS.Calendar, space: 'core:space:Workspace', data }, null, 2));
58
+ return;
59
+ }
60
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
61
+ try {
62
+ const id = await withSpinner('Creating calendar…', () => client.createDoc(CLASS.Calendar, 'core:space:Workspace', data), opts);
63
+ if (shouldJson({ json: opts.json, ci: opts.ci })) {
64
+ json({ _id: id, ...data });
65
+ return;
66
+ }
67
+ success(`created calendar`, opts.name, id);
68
+ }
69
+ finally {
70
+ await client.close();
71
+ }
72
+ }
73
+ export async function deleteCalendar(ref, opts = {}) {
74
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
75
+ try {
76
+ const account = await client.getAccount();
77
+ const id = await resolveRef(ref, {
78
+ client,
79
+ classId: CLASS.Calendar,
80
+ workspaceId: account.uuid
81
+ });
82
+ const doc = await client.findOne(CLASS.Calendar, { _id: id });
83
+ if (!doc)
84
+ throw new CliError(ExitCode.NotFound, `calendar ${ref} not found`);
85
+ try {
86
+ await client.removeDoc(CLASS.Calendar, doc.space, id);
87
+ removed('deleted calendar', String(doc.name ?? ref), id);
88
+ }
89
+ catch (e) {
90
+ throw new CliError(ExitCode.Server, `delete failed: ${e.message}`);
91
+ }
92
+ }
93
+ finally {
94
+ await client.close();
95
+ }
96
+ }
97
+ export async function listSchedules(g = {}) {
98
+ const client = await connectCli({ url: g.url, workspace: g.workspace });
99
+ try {
100
+ const docs = (await withSpinner('Loading schedules…', () => client.findAll(CLASS.Schedule, {}), g));
101
+ if (shouldJson({ json: g.json, ci: g.ci })) {
102
+ json(docs);
103
+ return;
104
+ }
105
+ table(docs, [
106
+ { key: 'title', header: 'TITLE', format: (r) => C.emphasis(String(r.title ?? '')) },
107
+ { key: 'timeZone', header: 'TIMEZONE' },
108
+ { key: 'meetingDuration', header: 'DURATION' },
109
+ { key: 'meetingInterval', header: 'INTERVAL' },
110
+ { key: '_id', header: '_ID', format: (r) => C.id(String(r._id).slice(-12)) }
111
+ ], { count: true, title: 'schedules' });
112
+ }
113
+ finally {
114
+ await client.close();
115
+ }
116
+ }
117
+ export async function getSchedule(ref, opts = {}) {
118
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
119
+ try {
120
+ const account = await client.getAccount();
121
+ const id = await resolveRef(ref, {
122
+ client,
123
+ classId: CLASS.Schedule,
124
+ workspaceId: account.uuid
125
+ });
126
+ const doc = await client.findOne(CLASS.Schedule, { _id: id });
127
+ if (!doc)
128
+ throw new CliError(ExitCode.NotFound, `schedule ${ref} not found`);
129
+ if (shouldJson({ json: opts.json, ci: opts.ci })) {
130
+ json(doc);
131
+ return;
132
+ }
133
+ header(`Schedule — ${doc.title ?? doc.name ?? '(unnamed)'}`, { subtitle: `created ${relTime(doc.createdOn)}` });
134
+ kv([
135
+ ['ID', C.emphasis(String(doc._id))],
136
+ ['Title', String(doc.title ?? '—')],
137
+ ['Description', String(doc.description ?? '—')],
138
+ ['Time zone', String(doc.timeZone ?? '—')],
139
+ ['Meeting duration', doc.meetingDuration != null ? `${doc.meetingDuration} min` : C.muted('—')],
140
+ ['Meeting interval', doc.meetingInterval != null ? `${doc.meetingInterval} min` : C.muted('—')],
141
+ ['Owner', String(doc.owner ?? '—')],
142
+ ['Created', doc.createdOn != null ? `${isoDate(doc.createdOn)} (${relTime(doc.createdOn)})` : C.muted('—')],
143
+ ['Modified', doc.modifiedOn != null ? `${isoDate(doc.modifiedOn)} (${relTime(doc.modifiedOn)})` : C.muted('—')],
144
+ ['_class', C.id(String(doc._class))]
145
+ ]);
146
+ if (doc.description && doc.description !== '') {
147
+ console.log();
148
+ console.log(C.emphasis('Description'));
149
+ console.log(C.muted('─'.repeat(20)));
150
+ console.log(String(doc.description));
151
+ }
152
+ }
153
+ finally {
154
+ await client.close();
155
+ }
156
+ }
157
+ export async function createSchedule(opts) {
158
+ if (!opts.title)
159
+ throw new CliError(ExitCode.Validation, 'missing --title');
160
+ if (!opts.owner)
161
+ throw new CliError(ExitCode.Validation, 'missing --owner (person uuid)');
162
+ if (!opts.timeZone)
163
+ throw new CliError(ExitCode.Validation, 'missing --time-zone (e.g. UTC)');
164
+ const data = {
165
+ title: opts.title,
166
+ description: opts.description ?? '',
167
+ owner: opts.owner,
168
+ meetingDuration: opts.duration ?? 30,
169
+ meetingInterval: opts.interval ?? 15,
170
+ availability: {},
171
+ timeZone: opts.timeZone
172
+ };
173
+ if (opts.dryRun) {
174
+ console.log('would create schedule:');
175
+ console.log(JSON.stringify({ _class: CLASS.Schedule, space: '<self>', data }, null, 2));
176
+ return;
177
+ }
178
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
179
+ try {
180
+ const id = await withSpinner('Creating schedule…', () => client.createDoc(CLASS.Schedule, client.getHierarchy().getDomain(CLASS.Schedule), data), opts);
181
+ invalidateIndex((await client.getAccount()).uuid, CLASS.Schedule);
182
+ if (shouldJson({ json: opts.json, ci: opts.ci })) {
183
+ json({ _id: id, ...data });
184
+ }
185
+ else {
186
+ success(`created schedule`, opts.title, id);
187
+ }
188
+ }
189
+ finally {
190
+ await client.close();
191
+ }
192
+ }
193
+ export async function updateSchedule(ref, opts) {
194
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
195
+ try {
196
+ const account = await client.getAccount();
197
+ const id = await resolveRef(ref, {
198
+ client,
199
+ classId: CLASS.Schedule,
200
+ workspaceId: account.uuid
201
+ });
202
+ const doc = await client.findOne(CLASS.Schedule, { _id: id });
203
+ if (!doc)
204
+ throw new CliError(ExitCode.NotFound, `schedule ${ref} not found`);
205
+ const ops = {};
206
+ if (opts.title)
207
+ ops.title = opts.title;
208
+ if (opts.description !== undefined)
209
+ ops.description = opts.description;
210
+ if (opts.timeZone)
211
+ ops.timeZone = opts.timeZone;
212
+ if (opts.duration !== undefined)
213
+ ops.meetingDuration = opts.duration;
214
+ if (opts.interval !== undefined)
215
+ ops.meetingInterval = opts.interval;
216
+ if (Object.keys(ops).length === 0)
217
+ throw new CliError(ExitCode.Validation, 'nothing to update', 'pass --title, --time-zone, --duration, --interval, or --description');
218
+ if (opts.dryRun) {
219
+ console.log(`would update schedule ${id}:`);
220
+ console.log(JSON.stringify({ _class: CLASS.Schedule, objectId: id, ops }, null, 2));
221
+ return;
222
+ }
223
+ await withSpinner('Updating…', () => client.updateDoc(CLASS.Schedule, doc.space, id, ops), opts);
224
+ updated(`updated schedule`, id);
225
+ }
226
+ finally {
227
+ await client.close();
228
+ }
229
+ }
230
+ export async function deleteSchedules(refs, opts = {}) {
231
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
232
+ try {
233
+ const account = await client.getAccount();
234
+ const ids = await resolveRefs(refs, {
235
+ client,
236
+ classId: CLASS.Schedule,
237
+ workspaceId: account.uuid
238
+ });
239
+ if (!opts.yes && ids.length > 1)
240
+ throw new CliError(ExitCode.Validation, `destructive: deleting ${ids.length} schedules requires --yes`, 're-run with --yes to confirm');
241
+ let deleted = 0, skipped = 0;
242
+ for (const id of ids) {
243
+ const doc = await client.findOne(CLASS.Schedule, { _id: id });
244
+ if (!doc) {
245
+ skipped++;
246
+ continue;
247
+ }
248
+ const r = await deleteDoc(client, CLASS.Schedule, doc.space, id, opts);
249
+ if (r.skipped)
250
+ skipped++;
251
+ else {
252
+ deleted++;
253
+ await new Promise((res) => setTimeout(res, 100));
254
+ }
255
+ }
256
+ bulkRemoved(deleted, skipped);
257
+ }
258
+ finally {
259
+ await client.close();
260
+ }
261
+ }
262
+ // ---- events (existing surface, now expanded with --calendar-id, --rrule) ----
263
+ export async function listEvents(opts) {
264
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
265
+ try {
266
+ const query = {};
267
+ if (opts.start)
268
+ query.startDate = { $gte: parseDate(opts.start, '--start') };
269
+ if (opts.end)
270
+ query.dueDate = { $lte: parseDate(opts.end, '--end') };
271
+ if (opts.calendar) {
272
+ query.calendar = await resolveCalendarId(client, opts.calendar);
273
+ }
274
+ const docs = (await withSpinner('Loading events…', () => client.findAll(CLASS.Event, query), opts));
275
+ let r = docs;
276
+ if (opts.offset && opts.offset > 0)
277
+ r = r.slice(opts.offset);
278
+ if (opts.limit && opts.limit > 0)
279
+ r = r.slice(0, opts.limit);
280
+ if (shouldJson({ json: opts.json, ci: opts.ci })) {
281
+ json(r);
282
+ return;
283
+ }
284
+ table(r, COLUMNS.event(), { count: true, title: 'events' });
285
+ }
286
+ finally {
287
+ await client.close();
288
+ }
289
+ }
290
+ export async function getEvent(ref, opts = {}) {
291
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
292
+ try {
293
+ const account = await client.getAccount();
294
+ const id = await resolveRef(ref, {
295
+ client,
296
+ classId: CLASS.Event,
297
+ workspaceId: account.uuid
298
+ });
299
+ let doc = await client.findOne(CLASS.Event, { _id: id });
300
+ if (!doc) {
301
+ doc = await client.findOne(CLASS.ReccuringEvent, { _id: id });
302
+ }
303
+ if (!doc)
304
+ throw new CliError(ExitCode.NotFound, `event ${ref} not found`);
305
+ if (opts.markdown && doc.description) {
306
+ try {
307
+ const body = await withTimeout(client.fetchMarkup(CLASS.Event, doc._id, 'description', doc.description, 'markdown'), 5000, '(body fetch timed out)');
308
+ console.log(body);
309
+ return;
310
+ }
311
+ catch {
312
+ console.log(String(doc.description));
313
+ return;
314
+ }
315
+ }
316
+ if (shouldJson({ json: opts.json, ci: opts.ci })) {
317
+ json(doc);
318
+ return;
319
+ }
320
+ const start = typeof doc.date === 'number' ? isoDate(doc.date) : (doc.date ? String(doc.date) : '—');
321
+ const dur = doc.dueDate != null && doc.date != null
322
+ ? `${Math.round((doc.dueDate - doc.date) / 60000)} min`
323
+ : '—';
324
+ header(`Event — ${doc.title ?? '(untitled)'}`, { subtitle: `${start}${dur !== '—' ? ' · ' + dur : ''}` });
325
+ kv([
326
+ ['ID', C.emphasis(String(doc._id))],
327
+ ['Title', String(doc.title ?? '—')],
328
+ ['Calendar', String(doc.calendar ?? '—')],
329
+ ['Start', start],
330
+ ['Duration', dur],
331
+ ['All-day', doc.allDay ? C.warn('yes') : C.muted('no')],
332
+ ['Recurring', doc.recurring ? 'yes' : C.muted('no')],
333
+ ['Visibility', doc.visibility ?? C.muted('default')],
334
+ ['Status', String(doc.status ?? '—')],
335
+ ['Location', String(doc.location ?? '—')],
336
+ ['Participants', Array.isArray(doc.participants) && doc.participants.length > 0 ? C.muted(`${doc.participants.length} people`) : C.muted('none')],
337
+ ['External', Array.isArray(doc.externalParticipants) && doc.externalParticipants.length > 0 ? C.muted(`${doc.externalParticipants.length} external`) : C.muted('none')],
338
+ ['Created', doc.createdOn != null ? `${isoDate(doc.createdOn)} (${relTime(doc.createdOn)})` : C.muted('—')]
339
+ ]);
340
+ if (doc.description && doc.description !== '' && !opts.markdown) {
341
+ console.log();
342
+ console.log(C.emphasis('Description'));
343
+ console.log(C.muted('─'.repeat(20)));
344
+ const desc = String(doc.description);
345
+ console.log(desc.length > 500 ? desc.slice(0, 500) + '…' : desc);
346
+ }
347
+ }
348
+ finally {
349
+ await client.close();
350
+ }
351
+ }
352
+ export async function createEvent(opts) {
353
+ if (!opts.title)
354
+ throw new CliError(ExitCode.Validation, 'missing --title');
355
+ if (!opts.start)
356
+ throw new CliError(ExitCode.Validation, 'missing --start (ISO)');
357
+ if (!opts.end)
358
+ throw new CliError(ExitCode.Validation, 'missing --end (ISO)');
359
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
360
+ try {
361
+ const calendarId = await resolveCalendarId(client, opts.calendarId);
362
+ const startDate = parseDate(opts.start, '--start');
363
+ const dueDate = parseDate(opts.end, '--end');
364
+ const eventId = generateEventId();
365
+ const isRecurring = Boolean(opts.rrule);
366
+ const account = await client.getAccount();
367
+ // Pick attachedTo / attachedToClass: explicit arg → current user (by uuid).
368
+ let attachedTo;
369
+ let attachedToClass;
370
+ if (opts.attachedTo && opts.attachedToClass) {
371
+ attachedTo = await resolveRef(opts.attachedTo, {
372
+ client,
373
+ classId: opts.attachedToClass,
374
+ workspaceId: account.uuid
375
+ });
376
+ attachedToClass = opts.attachedToClass;
377
+ }
378
+ else {
379
+ // Default: the current user (so the event shows in their calendar).
380
+ attachedTo = account.uuid;
381
+ attachedToClass = CLASS.Person;
382
+ }
383
+ const data = {
384
+ title: opts.title,
385
+ description: opts.description ?? opts.body ?? '',
386
+ date: startDate,
387
+ startDate,
388
+ dueDate,
389
+ allDay: !!opts.allDay,
390
+ participants: opts.attendee ? [opts.attendee] : [],
391
+ location: opts.location ?? '',
392
+ calendar: calendarId,
393
+ eventId,
394
+ access: 'owner',
395
+ visibility: 'public',
396
+ blockTime: false,
397
+ user: account.primarySocialId
398
+ };
399
+ if (isRecurring) {
400
+ data.rules = [parseRRule(opts.rrule)];
401
+ data.exdate = [];
402
+ data.rdate = [];
403
+ data.originalStartTime = startDate;
404
+ data.timeZone = opts.timeZone ?? 'UTC';
405
+ }
406
+ const classId = isRecurring ? CLASS.ReccuringEvent : CLASS.Event;
407
+ if (opts.dryRun) {
408
+ console.log(`would create ${isRecurring ? 'recurring event' : 'event'}:`);
409
+ console.log(JSON.stringify({ _class: classId, space: 'calendar:space:Calendar', attachedTo, attachedToClass, collection: 'events', data }, null, 2));
410
+ return;
411
+ }
412
+ const id = await withSpinner(`Creating ${isRecurring ? 'recurring event' : 'event'}…`, () => client.addCollection(classId, 'calendar:space:Calendar', attachedTo, attachedToClass, 'events', data), opts);
413
+ invalidateIndex(account.uuid, CLASS.Event);
414
+ if (shouldJson({ json: opts.json, ci: opts.ci })) {
415
+ json({ _id: id, recurring: isRecurring, attachedTo, ...data });
416
+ }
417
+ else {
418
+ success(`created ${isRecurring ? `recurring event` : `event`}`, opts.title, id);
419
+ }
420
+ }
421
+ finally {
422
+ await client.close();
423
+ }
424
+ }
425
+ export async function updateEvent(ref, opts) {
426
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
427
+ try {
428
+ const account = await client.getAccount();
429
+ const id = await resolveRef(ref, {
430
+ client,
431
+ classId: CLASS.Event,
432
+ workspaceId: account.uuid
433
+ });
434
+ const doc = await client.findOne(CLASS.Event, { _id: id });
435
+ if (!doc)
436
+ throw new CliError(ExitCode.NotFound, `event ${ref} not found`);
437
+ const ops = {};
438
+ if (opts.title)
439
+ ops.title = opts.title;
440
+ if (opts.description !== undefined)
441
+ ops.description = opts.description;
442
+ if (opts.start) {
443
+ const sd = parseDate(opts.start, '--start');
444
+ ops.startDate = sd;
445
+ // CLI-16: the model stores BOTH `date` (display field) and `startDate`.
446
+ // Create writes both; update previously only wrote startDate. Keep them
447
+ // in sync so `calendar get`/`calendar list` show the updated start.
448
+ ops.date = sd;
449
+ }
450
+ if (opts.end)
451
+ ops.dueDate = parseDate(opts.end, '--end');
452
+ if (opts.allDay !== undefined)
453
+ ops.allDay = opts.allDay;
454
+ if (opts.location !== undefined)
455
+ ops.location = opts.location;
456
+ if (opts.attendee)
457
+ ops.participants = [opts.attendee];
458
+ if (Object.keys(ops).length === 0)
459
+ throw new CliError(ExitCode.Validation, 'nothing to update', 'pass --title/--description/--start/--end/--all-day/--location/--attendee');
460
+ if (opts.dryRun) {
461
+ console.log(`would update event ${id}:`);
462
+ console.log(JSON.stringify({ _class: CLASS.Event, objectId: id, space: doc.space, ops }, null, 2));
463
+ return;
464
+ }
465
+ await withSpinner('Updating…', () => client.updateDoc(CLASS.Event, doc.space, id, ops), opts);
466
+ updated(`updated event`, id);
467
+ }
468
+ finally {
469
+ await client.close();
470
+ }
471
+ }
472
+ export async function deleteEvents(refs, opts = {}) {
473
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
474
+ try {
475
+ const account = await client.getAccount();
476
+ if (!opts.yes && refs.length > 1) {
477
+ throw new CliError(ExitCode.Validation, `destructive: deleting ${refs.length} events requires --yes`, 're-run with --yes to confirm');
478
+ }
479
+ let deleted = 0, skipped = 0;
480
+ for (const ref of refs) {
481
+ // CLI-15: a CLI-created recurring event lives in CLASS.ReccuringEvent,
482
+ // not CLASS.Event. Try Event first, then fall back to ReccuringEvent.
483
+ const id = await resolveRef(ref, {
484
+ client,
485
+ classId: CLASS.Event,
486
+ workspaceId: account.uuid
487
+ });
488
+ let doc = await client.findOne(CLASS.Event, { _id: id });
489
+ let classId = CLASS.Event;
490
+ if (!doc) {
491
+ doc = await client.findOne(CLASS.ReccuringEvent, { _id: id });
492
+ classId = CLASS.ReccuringEvent;
493
+ }
494
+ if (!doc) {
495
+ skipped++;
496
+ continue;
497
+ }
498
+ const r = await deleteDoc(client, classId, doc.space, id, opts);
499
+ if (r.skipped)
500
+ skipped++;
501
+ else {
502
+ deleted++;
503
+ await new Promise((res) => setTimeout(res, 100));
504
+ }
505
+ }
506
+ bulkRemoved(deleted, skipped);
507
+ }
508
+ finally {
509
+ await client.close();
510
+ }
511
+ }
512
+ // ---- recurring events ----
513
+ export async function listRecurringEvents(opts = {}) {
514
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
515
+ try {
516
+ const docs = (await withSpinner('Loading recurring events…', () => client.findAll(CLASS.ReccuringEvent, {}), opts));
517
+ let r = docs;
518
+ if (opts.offset && opts.offset > 0)
519
+ r = r.slice(opts.offset);
520
+ if (opts.limit && opts.limit > 0)
521
+ r = r.slice(0, opts.limit);
522
+ if (shouldJson({ json: opts.json, ci: opts.ci })) {
523
+ json(r);
524
+ return;
525
+ }
526
+ table(r, [
527
+ { key: 'title', header: 'TITLE', format: (r) => C.emphasis(String(r.title ?? '').slice(0, 60)) },
528
+ { key: 'date', header: 'START', width: 17, format: (r) => {
529
+ const d = r.date ?? r.startDate;
530
+ return d != null ? isoDate(d) : C.muted('—');
531
+ } },
532
+ { key: 'dueDate', header: 'END', width: 17, format: (r) => {
533
+ const d = r.dueDate;
534
+ return d != null ? isoDate(d) : C.muted('—');
535
+ } },
536
+ { key: 'rules', header: 'RULES', format: (r) => {
537
+ const r2 = r.rules;
538
+ if (r2 == null)
539
+ return C.muted('—');
540
+ if (typeof r2 === 'string')
541
+ return C.muted(r2.slice(0, 50));
542
+ return C.muted(JSON.stringify(r2).slice(0, 50));
543
+ } },
544
+ { key: '_id', header: '_ID', format: (r) => C.id(String(r._id).slice(-12)) }
545
+ ], { count: true, title: 'reminders' });
546
+ }
547
+ finally {
548
+ await client.close();
549
+ }
550
+ }
551
+ export async function listRecurringInstances(ref, opts) {
552
+ const client = await connectCli({ url: opts.url, workspace: opts.workspace });
553
+ try {
554
+ const account = await client.getAccount();
555
+ const id = await resolveRef(ref, {
556
+ client,
557
+ classId: CLASS.ReccuringEvent,
558
+ workspaceId: account.uuid
559
+ });
560
+ const query = { recurringEventId: id };
561
+ if (opts.start || opts.end) {
562
+ const range = {};
563
+ if (opts.start)
564
+ range.$gte = parseDate(opts.start, '--start');
565
+ if (opts.end)
566
+ range.$lte = parseDate(opts.end, '--end');
567
+ query.date = range;
568
+ }
569
+ const instances = (await client.findAll(CLASS.ReccuringInstance, query));
570
+ let r = instances;
571
+ if (opts.limit)
572
+ r = r.slice(0, opts.limit);
573
+ if (shouldJson({ json: opts.json, ci: opts.ci })) {
574
+ json(r);
575
+ return;
576
+ }
577
+ table(r, [
578
+ { key: 'date', header: 'DATE', width: 17, format: (r) => {
579
+ const d = r.date;
580
+ return d != null ? isoDate(d) : C.muted('—');
581
+ } },
582
+ { key: 'originalStartTime', header: 'ORIGINAL', width: 17, format: (r) => {
583
+ const d = r.originalStartTime;
584
+ return d != null ? isoDate(d) : C.muted('—');
585
+ } },
586
+ { key: 'virtual', header: 'VIRTUAL', width: 10, align: 'center', format: (r) => r.virtual ? C.cyan('yes') : C.muted('no') },
587
+ { key: 'isCancelled', header: 'STATE', width: 11, align: 'center', format: (r) => r.isCancelled ? C.red('cancelled') : C.green('scheduled') },
588
+ { key: '_id', header: '_ID', format: (r) => C.id(String(r._id).slice(-12)) }
589
+ ], { count: true, title: 'instances' });
590
+ }
591
+ finally {
592
+ await client.close();
593
+ }
594
+ }
595
+ // ---- helpers ----
596
+ async function resolveCalendarId(client, arg) {
597
+ if (arg !== undefined) {
598
+ // Accept an id, an idx ref, or a calendar name.
599
+ if (arg.includes(':')) {
600
+ try {
601
+ return (await resolveRef(arg, {
602
+ client,
603
+ classId: CLASS.Calendar,
604
+ workspaceId: (await client.getAccount()).uuid
605
+ }));
606
+ }
607
+ catch {
608
+ // fall through to name lookup
609
+ }
610
+ }
611
+ const all = (await client.findAll(CLASS.Calendar, {}));
612
+ const hit = all.find((c) => String(c.name ?? '').toLowerCase() === arg.toLowerCase());
613
+ if (!hit)
614
+ throw new CliError(ExitCode.NotFound, `calendar ${arg} not found`);
615
+ return hit._id;
616
+ }
617
+ const primary = (await client.findAll('calendar:class:PrimaryCalendar', {}, { limit: 1 }))[0];
618
+ if (primary)
619
+ return primary.attachedTo;
620
+ const all = (await client.findAll(CLASS.Calendar, { hidden: false }, { limit: 1 }));
621
+ if (all.length === 0)
622
+ throw new CliError(ExitCode.NotFound, 'no calendars available — pass --calendar-id');
623
+ return all[0]._id;
624
+ }
625
+ async function resolveCalendarSpace(_arg) {
626
+ // Personal calendar events live in the workspace space. Reccuring in
627
+ // a dedicated space (set in plugin/model). For one-off events we use
628
+ // the personal calendar's space — typically the workspace itself.
629
+ return 'calendar:space:Personal';
630
+ }
631
+ function generateEventId() {
632
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
633
+ }
634
+ function parseRRule(rule) {
635
+ // Accept "FREQ=DAILY;COUNT=3" or "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE"
636
+ const out = {};
637
+ for (const part of rule.split(';')) {
638
+ const eq = part.indexOf('=');
639
+ if (eq < 0)
640
+ continue;
641
+ const key = part.slice(0, eq).trim();
642
+ const val = part.slice(eq + 1).trim();
643
+ if (key === 'BYDAY' || key === 'BYMONTH' || key === 'BYHOUR' || key === 'BYMINUTE' || key === 'BYSECOND' ||
644
+ key === 'BYMONTHDAY' || key === 'BYYEARDAY' || key === 'BYWEEKNO' || key === 'BYSETPOS') {
645
+ out[key] = val.split(',').map((s) => isNaN(Number(s)) ? s : Number(s));
646
+ }
647
+ else if (key === 'COUNT' || key === 'INTERVAL') {
648
+ out[key] = Number(val);
649
+ }
650
+ else if (key === 'UNTIL') {
651
+ out[key] = parseDate(val, 'UNTIL');
652
+ }
653
+ else {
654
+ out[key] = val;
655
+ }
656
+ }
657
+ return out;
658
+ }
659
+ //# sourceMappingURL=calendar.js.map