@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
package/dist/cli.js ADDED
@@ -0,0 +1,1959 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+ import { handleError } from './output/errors.js';
7
+ import { isNonInteractive, markNonInteractive } from './auth/env.js';
8
+ import { loginCommand } from './commands/login.js';
9
+ import { whoamiCommand } from './commands/whoami.js';
10
+ import { listWorkspaces, currentWorkspace, useWorkspace, createWorkspace, deleteWorkspace, listMembers, updateMemberRole, workspaceInfo, updateWorkspaceName, workspaceGuests, createAccessLink, listRegions } from './resources/workspace.js';
11
+ import { getUser, updateUser, findUser } from './resources/user.js';
12
+ import { listProjects, getProject, createProject, updateProject, deleteProjects, listStatuses, listTargetPreferences, upsertTargetPreference } from './resources/project.js';
13
+ import { listIssues, getIssue, createIssue, updateIssue, deleteIssues, addIssueLabel, removeIssueLabel, addIssueRelation, removeIssueRelation, listIssueRelations, linkDocument, unlinkDocument, moveIssue, previewDelete, relatedTargets, setRelatedTarget } from './resources/issue.js';
14
+ import { listComponents, getComponent, createComponent, updateComponent, deleteComponents } from './resources/component.js';
15
+ import { listMilestones, getMilestone, createMilestone, updateMilestone, deleteMilestones } from './resources/milestone.js';
16
+ import { listIssueTemplates, getIssueTemplate, createIssueTemplate, updateIssueTemplate, deleteIssueTemplates, addTemplateChild, removeTemplateChild } from './resources/issue-template.js';
17
+ import { listComments, addComment, updateComment, deleteComments } from './resources/comment.js';
18
+ import { listCalendars, listSchedules, getSchedule, createSchedule, updateSchedule, deleteSchedules, listEvents, getEvent, createEvent, updateEvent, deleteEvents, listRecurringEvents, listRecurringInstances, createCalendar, deleteCalendar } from './resources/calendar.js';
19
+ import { listTimeEntries, logTime, deleteTimeEntries, timeReport } from './resources/time.js';
20
+ import { listCards, getCard, createCard, updateCard, deleteCards, listCardSpaces, getCardSpace, createCardSpace, deleteCardSpaces, listMasterTags } from './resources/card.js';
21
+ import { listDocuments, getDocument, createDocument, updateDocument, deleteDocuments, listSnapshots, getSnapshot, listInlineComments, listTeamspaces, getTeamspace, createTeamspace, updateTeamspace, deleteTeamspaces } from './resources/document.js';
22
+ import { listActions, getAction, createAction, updateAction, deleteActions, completeAction, reopenAction, scheduleAction, unscheduleAction } from './resources/todo.js';
23
+ import { listChannels, getChannel, createChannel, updateChannel, deleteChannels, archiveChannel, listChannelMembers, joinChannel, leaveChannel, addChannelMembers, removeChannelMembers, listChannelMessages, sendChannelMessage, updateChannelMessage, deleteChannelMessages, listThreadReplies, addThreadReply, updateThreadReply, deleteThreadReplies, listDms, createDm, listDmMessages, sendDmMessage } from './resources/channel.js';
24
+ import { apiCommand } from './raw/api.js';
25
+ import { wsCommand } from './raw/ws.js';
26
+ const __dirname = dirname(fileURLToPath(import.meta.url));
27
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
28
+ export function globalsFrom(cmd) {
29
+ return cmd.optsWithGlobals();
30
+ }
31
+ function attachGlobalOpts(cmd, opts = {}) {
32
+ let c = cmd
33
+ .option('--url <url>', 'Huly server URL')
34
+ .option('--workspace <name>', 'workspace URL name or UUID')
35
+ .option('--json', 'output JSON')
36
+ .option('--ci', 'CI mode (JSON output)')
37
+ .option('--markdown', 'output body as markdown')
38
+ .option('--dry-run', 'print intended tx, do not apply')
39
+ .option('--minimal', 'minimal payload (no smart defaults)')
40
+ .option('-y, --yes', 'skip confirmation prompts');
41
+ if (!opts.skipNonInteractive) {
42
+ c = c.option('--non-interactive', 'disable interactive prompts');
43
+ }
44
+ return c;
45
+ }
46
+ function attachToChildren(cmd) {
47
+ for (const child of cmd.commands) {
48
+ attachGlobalOpts(child);
49
+ attachToChildren(child);
50
+ }
51
+ }
52
+ const GLOBAL_OPTS_HELP = `
53
+ Global options (also available on parent commands):
54
+ --url <url> Huly server URL
55
+ --workspace <name> workspace URL name or UUID
56
+ --json / --ci output JSON
57
+ --markdown output body as markdown
58
+ --dry-run print intended tx, do not apply
59
+ --minimal minimal payload (no smart defaults)
60
+ -y, --yes skip confirmation prompts
61
+ --non-interactive disable interactive prompts
62
+ `;
63
+ function withGlobalHelp(cmd) {
64
+ cmd.addHelpText('after', GLOBAL_OPTS_HELP);
65
+ return cmd;
66
+ }
67
+ function preAction(cmd) {
68
+ const opts = cmd.optsWithGlobals();
69
+ if (opts.nonInteractive || opts.headless || opts.ci || isNonInteractive()) {
70
+ markNonInteractive();
71
+ }
72
+ }
73
+ export async function run(argv = process.argv) {
74
+ const program = new Command();
75
+ program
76
+ .name('huly')
77
+ .description('AI-agent-first CLI for self-hosted Huly')
78
+ .version(pkg.version)
79
+ .option('--non-interactive', 'disable interactive prompts')
80
+ .hook('preAction', (thisCmd) => preAction(thisCmd));
81
+ program
82
+ .command('login')
83
+ .description('Log in and cache credentials')
84
+ .option('--headless', 'use env vars only, no prompts')
85
+ .option('--email <email>')
86
+ .option('--password <pwd>')
87
+ .action(async (opts, cmd) => {
88
+ try {
89
+ const g = globalsFrom(cmd);
90
+ await loginCommand({
91
+ url: g.url,
92
+ workspace: g.workspace,
93
+ email: opts.email ?? g.email,
94
+ password: opts.password ?? g.password,
95
+ nonInteractive: g.nonInteractive,
96
+ headless: opts.headless ?? g.headless,
97
+ json: g.json ?? g.ci
98
+ });
99
+ }
100
+ catch (e) {
101
+ handleError(e);
102
+ }
103
+ });
104
+ program
105
+ .command('whoami')
106
+ .description('Show current account and workspace')
107
+ .action(async (_opts, cmd) => {
108
+ try {
109
+ const g = globalsFrom(cmd);
110
+ await whoamiCommand({ url: g.url, workspace: g.workspace, json: g.json ?? g.ci });
111
+ }
112
+ catch (e) {
113
+ handleError(e);
114
+ }
115
+ });
116
+ const ws = program.command('workspace').description('Manage workspaces');
117
+ withGlobalHelp(ws);
118
+ ws.command('list').description('List accessible workspaces')
119
+ .addHelpText('after', `
120
+ Examples:
121
+ $ huly workspace list
122
+ $ huly workspace list --json | jq -r '.[].name'`)
123
+ .action(async (_o, cmd) => {
124
+ try {
125
+ await listWorkspaces(globalsFrom(cmd));
126
+ }
127
+ catch (e) {
128
+ handleError(e);
129
+ }
130
+ });
131
+ ws.command('current').description('Show current workspace').action(async (_o, cmd) => {
132
+ try {
133
+ await currentWorkspace(globalsFrom(cmd));
134
+ }
135
+ catch (e) {
136
+ handleError(e);
137
+ }
138
+ });
139
+ ws.command('use <name>').description('Set active workspace')
140
+ .addHelpText('after', `
141
+ Examples:
142
+ $ huly workspace use production
143
+ $ huly workspace use life # switch workspace for subsequent commands
144
+ $ huly --workspace life issue list # one-off without switching`)
145
+ .action(async (name, _o, cmd) => {
146
+ try {
147
+ await useWorkspace(name, globalsFrom(cmd));
148
+ }
149
+ catch (e) {
150
+ handleError(e);
151
+ }
152
+ });
153
+ ws.command('create').description('Create a new workspace (requires --yes)')
154
+ .requiredOption('--name <name>')
155
+ .option('--region <region>')
156
+ .addHelpText('after', `
157
+ Examples:
158
+ $ huly workspace create --name "My new workspace" --yes
159
+ $ huly workspace create --name "EU workspace" --region eu-west --yes
160
+
161
+ Note: workspace creation runs the tracker migration. May take 30-60s.`)
162
+ .action(async (opts, cmd) => {
163
+ try {
164
+ await createWorkspace({ ...opts, ...globalsFrom(cmd) });
165
+ }
166
+ catch (e) {
167
+ handleError(e);
168
+ }
169
+ });
170
+ ws.command('delete [name]').description('Delete a workspace (DESTRUCTIVE; requires --yes; --force to delete the active workspace)')
171
+ .option('--force', 'delete even if this is the active workspace')
172
+ .addHelpText('after', `
173
+ Examples:
174
+ $ huly workspace delete my-old-workspace --yes
175
+ $ huly workspace delete life --yes --force # delete active workspace
176
+
177
+ WARNING: server-side hard-delete may take several minutes. Worker calls
178
+ doCleanup which drops all docs in all per-workspace tables.`)
179
+ .action(async (name, opts, cmd) => {
180
+ try {
181
+ await deleteWorkspace({ ...opts, ...globalsFrom(cmd), name });
182
+ }
183
+ catch (e) {
184
+ handleError(e);
185
+ }
186
+ });
187
+ ws.command('members').description('List workspace members')
188
+ .option('--role <r>', 'filter by role (Owner|Admin|Guest|ReadOnlyGuest|DocGuest)')
189
+ .addHelpText('after', `
190
+ Examples:
191
+ $ huly workspace members
192
+ $ huly workspace members --role Owner --json
193
+ $ huly workspace members --role Guest`)
194
+ .action(async (opts, cmd) => {
195
+ try {
196
+ await listMembers({ ...opts, ...globalsFrom(cmd) });
197
+ }
198
+ catch (e) {
199
+ handleError(e);
200
+ }
201
+ });
202
+ // N2: alias `member add` for consistency with channel.add-member.
203
+ // (No `member remove` — the SDK has no remove-member API; users must
204
+ // leave the workspace via huly workspace leave.)
205
+ const wsMember = ws.command('member').description('Manage a single member (alias for `workspace member`)');
206
+ wsMember.command('add <account>').description('Add or change a member\'s role (requires OWNER)')
207
+ .requiredOption('--role <r>', 'Owner|Admin|Guest|ReadOnlyGuest|DocGuest')
208
+ .addHelpText('after', `
209
+ Examples:
210
+ $ huly workspace member add alice@example.com --role MAINTAINER
211
+ $ huly workspace member add bob@example.com --role GUEST
212
+ $ huly workspace member add 86d46120-594e-4c10-8996-821ac2a7001a --role OWNER`)
213
+ .action(async (account, opts, cmd) => {
214
+ try {
215
+ await updateMemberRole({ ...opts, target: account, ...globalsFrom(cmd) });
216
+ }
217
+ catch (e) {
218
+ handleError(e);
219
+ }
220
+ });
221
+ ws.command('info').description('Show current workspace info (name, uuid, region, mode)')
222
+ .addHelpText('after', `
223
+ Examples:
224
+ $ huly workspace info
225
+ $ huly workspace info --json | jq -r .uuid`)
226
+ .action(async (_o, cmd) => {
227
+ try {
228
+ await workspaceInfo(globalsFrom(cmd));
229
+ }
230
+ catch (e) {
231
+ handleError(e);
232
+ }
233
+ });
234
+ ws.command('rename').description('Rename current workspace')
235
+ .requiredOption('--name <name>')
236
+ .action(async (opts, cmd) => {
237
+ try {
238
+ await updateWorkspaceName({ ...opts, ...globalsFrom(cmd) });
239
+ }
240
+ catch (e) {
241
+ handleError(e);
242
+ }
243
+ });
244
+ ws.command('guests').description('Update guest settings (--read-only and/or --sign-up, true|false)')
245
+ .option('--read-only <bool>')
246
+ .option('--sign-up <bool>')
247
+ .addHelpText('after', `
248
+ Examples:
249
+ $ huly workspace guests --read-only true
250
+ $ huly workspace guests --sign-up false --read-only true
251
+ $ huly workspace guests --sign-up true
252
+
253
+ Note: 'guests' (plural) is for workspace-level guest *settings*.
254
+ For individual guest role assignment, use \`huly workspace member add --role GUEST\`.`)
255
+ .action(async (opts, cmd) => {
256
+ try {
257
+ const readOnly = opts.readOnly === undefined ? undefined : opts.readOnly !== 'false' && opts.readOnly !== '0';
258
+ const signUp = opts.signUp === undefined ? undefined : opts.signUp !== 'false' && opts.signUp !== '0';
259
+ await workspaceGuests({ ...globalsFrom(cmd), readOnly, signUp });
260
+ }
261
+ catch (e) {
262
+ handleError(e);
263
+ }
264
+ });
265
+ ws.command('access-link').description('Create an access link (signup invite) for a role')
266
+ .requiredOption('--role <r>', 'Guest|ReadOnlyGuest|DocGuest|Admin|Owner')
267
+ .option('--exp-hours <n>', 'expiration in hours', (v) => parseInt(v, 10))
268
+ .option('--auto-join')
269
+ .option('--email <email>')
270
+ .addHelpText('after', `
271
+ Examples:
272
+ $ huly workspace access-link --role GUEST
273
+ $ huly workspace access-link --role MAINTAINER --exp-hours 48
274
+ $ huly workspace access-link --role GUEST --auto-join
275
+ $ huly workspace access-link --role GUEST --email alice@example.com`)
276
+ .action(async (opts, cmd) => {
277
+ try {
278
+ await createAccessLink({ ...opts, ...globalsFrom(cmd) });
279
+ }
280
+ catch (e) {
281
+ handleError(e);
282
+ }
283
+ });
284
+ ws.command('regions').description('List available regions')
285
+ .action(async (_o, cmd) => {
286
+ try {
287
+ await listRegions(globalsFrom(cmd));
288
+ }
289
+ catch (e) {
290
+ handleError(e);
291
+ }
292
+ });
293
+ const user = program.command('user').description('Manage user profile');
294
+ // N7: harmonize ref spec — accept positional <ref> OR --ref flag (matches project get).
295
+ user.command('get [ref]').description('Show user profile (current user by default, or by ref/uuid)')
296
+ .option('--ref <id>', 'account uuid (overrides positional ref)')
297
+ .addHelpText('after', `
298
+ Examples:
299
+ $ huly user get # current user profile
300
+ $ huly user get --ref 86d46120-594e-4c10-8996-821ac2a7001a
301
+ $ huly user get 86d46120-594e-4c10-8996-821ac2a7001a # positional form (N7)`)
302
+ .action(async (ref, opts, cmd) => {
303
+ try {
304
+ await getUser({ ...opts, ref, ...globalsFrom(cmd) });
305
+ }
306
+ catch (e) {
307
+ handleError(e);
308
+ }
309
+ });
310
+ user.command('update').description('Update current user profile')
311
+ .option('--name <name>')
312
+ .option('--bio <text>')
313
+ .option('--city <city>')
314
+ .option('--country <country>')
315
+ .addHelpText('after', `
316
+ Examples:
317
+ $ huly user update --city "Berlin"
318
+ $ huly user update --bio "New bio" --country "DE"`)
319
+ .action(async (opts, cmd) => {
320
+ try {
321
+ await updateUser({ ...opts, ...globalsFrom(cmd) });
322
+ }
323
+ catch (e) {
324
+ handleError(e);
325
+ }
326
+ });
327
+ user.command('find <email>').description('Look up a user by email (account-level or workspace-local)')
328
+ .addHelpText('after', `
329
+ Examples:
330
+ $ huly user find alice@example.com
331
+ $ huly user find alice@example.com --json
332
+
333
+ Resolution order: accountClient.findPersonBySocialKey → workspace-local
334
+ Person scan by name. Either may fail if the user is not in your workspace.`)
335
+ .action(async (email, _o, cmd) => {
336
+ try {
337
+ await findUser(email, globalsFrom(cmd));
338
+ }
339
+ catch (e) {
340
+ handleError(e);
341
+ }
342
+ });
343
+ const project = program.command('project').description('Manage tracker projects');
344
+ withGlobalHelp(project);
345
+ project.command('list').description('List projects').option('--limit <n>', 'limit', (v) => parseInt(v, 10)).option('--offset <n>', 'offset', (v) => parseInt(v, 10))
346
+ .addHelpText('after', `
347
+ Examples:
348
+ $ huly project list
349
+ $ huly project list --limit 10 --json
350
+ $ huly project list --offset 10 # pagination`)
351
+ .action(async (opts, cmd) => {
352
+ try {
353
+ await listProjects({ ...opts, ...globalsFrom(cmd) });
354
+ }
355
+ catch (e) {
356
+ handleError(e);
357
+ }
358
+ });
359
+ project.command('get <ref>').description('Get a project')
360
+ .addHelpText('after', `
361
+ Examples:
362
+ $ huly project get TSK
363
+ $ huly project get "Default project"
364
+ $ huly project get tracker:project:DefaultProject`)
365
+ .action(async (ref, opts, cmd) => {
366
+ try {
367
+ await getProject(ref, { ...opts, ...globalsFrom(cmd) });
368
+ }
369
+ catch (e) {
370
+ handleError(e);
371
+ }
372
+ });
373
+ project
374
+ .command('create')
375
+ .description('Create a project')
376
+ .requiredOption('--name <name>')
377
+ .requiredOption('--identifier <id>', 'short uppercase identifier (1-5 chars typical)')
378
+ .option('--description <text>')
379
+ .option('--private')
380
+ .addHelpText('after', `
381
+ Examples:
382
+ $ huly project create --name "Q3 Goals" --identifier Q3G
383
+ $ huly project create --name "Internal" --identifier INT --private \\
384
+ --description "Internal projects"
385
+
386
+ Required: --name, --identifier. Identifier must be uppercase letters/digits.
387
+ The CLI pre-checks for duplicate identifiers (server may not enforce).`)
388
+ .action(async (opts, cmd) => {
389
+ try {
390
+ await createProject({ ...opts, ...globalsFrom(cmd) });
391
+ }
392
+ catch (e) {
393
+ handleError(e);
394
+ }
395
+ });
396
+ project
397
+ .command('update <ref>')
398
+ .description('Update a project')
399
+ .option('--set <kv...>', 'set key=value (repeatable); value=null clears')
400
+ .option('--unset <key...>', 'unset key (repeatable)')
401
+ .addHelpText('after', `
402
+ Examples:
403
+ $ huly project update TSK --set description="Updated description"
404
+ $ huly project update TSK --set description=null # clear
405
+ $ huly project update TSK --set private=true
406
+ $ huly project update TSK --unset description`)
407
+ .action(async (ref, opts, cmd) => {
408
+ try {
409
+ await updateProject(ref, { ...opts, ...globalsFrom(cmd) });
410
+ }
411
+ catch (e) {
412
+ handleError(e);
413
+ }
414
+ });
415
+ project.command('delete <ref...>').description('Delete projects (DESTRUCTIVE; requires --yes)').action(async (refs, opts, cmd) => {
416
+ try {
417
+ await deleteProjects(refs, { ...opts, ...globalsFrom(cmd) });
418
+ }
419
+ catch (e) {
420
+ handleError(e);
421
+ }
422
+ });
423
+ project.command('statuses [ref]').description('List issue statuses for a project (defaults to $HULY_PROJECT or first arg)')
424
+ .option('--project <ref>')
425
+ .action(async (ref, opts, cmd) => {
426
+ try {
427
+ await listStatuses({ project: ref ?? opts.project, ...globalsFrom(cmd) });
428
+ }
429
+ catch (e) {
430
+ handleError(e);
431
+ }
432
+ });
433
+ project.command('target-preferences').description('List project target preferences (alias for `target-preference list`)')
434
+ .option('--project <ref>')
435
+ .action(async (opts, cmd) => {
436
+ try {
437
+ await listTargetPreferences({ ...opts, ...globalsFrom(cmd) });
438
+ }
439
+ catch (e) {
440
+ handleError(e);
441
+ }
442
+ });
443
+ const tgtPref = project.command('target-preference').description('Manage project target preferences');
444
+ tgtPref.command('list').description('List project target preferences')
445
+ .option('--project <ref>')
446
+ .action(async (opts, cmd) => {
447
+ try {
448
+ await listTargetPreferences({ ...opts, ...globalsFrom(cmd) });
449
+ }
450
+ catch (e) {
451
+ handleError(e);
452
+ }
453
+ });
454
+ tgtPref.command('upsert').description('Create or merge a project target preference')
455
+ .option('--project <ref>')
456
+ .option('--props <kv...>', 'key=value (repeatable)')
457
+ .action(async (opts, cmd) => {
458
+ try {
459
+ await upsertTargetPreference({ ...opts, ...globalsFrom(cmd) });
460
+ }
461
+ catch (e) {
462
+ handleError(e);
463
+ }
464
+ });
465
+ const issue = program.command('issue').description('Manage tracker issues');
466
+ withGlobalHelp(issue);
467
+ issue
468
+ .command('list')
469
+ .description('List issues')
470
+ .option('--project <id>')
471
+ .option('--status <name>')
472
+ .option('--status-category <c>', 'UnStarted|ToDo|Active|Won|Lost')
473
+ .option('--description-search <q>')
474
+ .option('--parent <ref|null>', 'filter by parent ref (literal "null" for top-level)')
475
+ .option('--assignee <email>')
476
+ .option('--label <l...>')
477
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
478
+ .option('--offset <n>', 'offset', (v) => parseInt(v, 10))
479
+ .addHelpText('after', `
480
+ Examples:
481
+ $ huly issue list --project TSK
482
+ $ huly issue list --status Backlog --assignee alice@example.com
483
+ $ huly issue list --status-category Active --limit 50 --json
484
+ $ huly issue list --description-search "smoke"
485
+ $ huly issue list --parent null # top-level only
486
+
487
+ Ref formats accepted by --project, --assignee, --label: name, identifier, or _id.`)
488
+ .action(async (opts, cmd) => {
489
+ try {
490
+ await listIssues({ ...opts, ...globalsFrom(cmd) });
491
+ }
492
+ catch (e) {
493
+ handleError(e);
494
+ }
495
+ });
496
+ issue
497
+ .command('get <ref>')
498
+ .description('Get an issue')
499
+ .addHelpText('after', `
500
+ Examples:
501
+ $ huly issue get TSK-1
502
+ $ huly issue get 1 # uses \$HULY_PROJECT
503
+ $ huly issue get TSK-1 --markdown
504
+ $ huly issue get tracker:issue:6a... # raw _id`)
505
+ .action(async (ref, opts, cmd) => {
506
+ try {
507
+ await getIssue(ref, { ...opts, ...globalsFrom(cmd) });
508
+ }
509
+ catch (e) {
510
+ handleError(e);
511
+ }
512
+ });
513
+ issue
514
+ .command('create')
515
+ .description('Create an issue')
516
+ .option('--project <id>', 'project identifier (defaults to $HULY_PROJECT or interactive selection)')
517
+ .requiredOption('--title <t>')
518
+ .option('--description <text>')
519
+ .option('--body <md>')
520
+ .option('--body-file <path>')
521
+ .option('--status <name>')
522
+ .option('--priority <p>', 'Urgent | High | Normal | Low | None')
523
+ .option('--assignee <email>', 'must be a workspace member')
524
+ .option('--label <l...>', 'repeatable: --label bug --label auth')
525
+ .option('--due <iso>', 'ISO 8601 e.g. 2026-07-01T14:00:00Z')
526
+ .option('--parent <ref>')
527
+ .option('--task-type <name|id>')
528
+ .addHelpText('after', `
529
+ Examples:
530
+ $ huly issue create --project TSK --title "Add OAuth login"
531
+ $ huly issue create --project TSK --title "Bug" --priority High \\
532
+ --assignee alice@example.com --label bug --label p1
533
+ $ huly issue create --project TSK --title "..." --body-file ./spec.md \\
534
+ --due 2026-08-01T00:00:00Z
535
+ $ huly issue create --project TSK --title "Sub-task" --parent TSK-5
536
+
537
+ Required: --project, --title. Valid priority values: Urgent, High, Normal,
538
+ Low, None. Assignee must be a workspace member.`)
539
+ .action(async (opts, cmd) => {
540
+ try {
541
+ await createIssue({ ...opts, ...globalsFrom(cmd) });
542
+ }
543
+ catch (e) {
544
+ handleError(e);
545
+ }
546
+ });
547
+ issue
548
+ .command('update <ref>')
549
+ .description('Update an issue')
550
+ .option('--set <kv...>', 'key=value (repeatable); key=null clears the field')
551
+ .option('--unset <key...>')
552
+ .option('--status <name>')
553
+ .option('--priority <p>', 'Urgent | High | Normal | Low | None')
554
+ .option('--assignee <email>')
555
+ .option('--title <t>')
556
+ .option('--description <text>')
557
+ .option('--task-type <name|id>')
558
+ .addHelpText('after', `
559
+ Examples:
560
+ $ huly issue update TSK-1 --status Done
561
+ $ huly issue update TSK-1 --description "Updated text"
562
+ $ huly issue update TSK-1 --set priority=High --set assignee=bob@example.com
563
+ $ huly issue update TSK-1 --set description=null # clear field
564
+
565
+ Pass any combination of --status/--priority/--assignee/--title/--description/--set.`)
566
+ .action(async (ref, opts, cmd) => {
567
+ try {
568
+ await updateIssue(ref, { ...opts, ...globalsFrom(cmd) });
569
+ }
570
+ catch (e) {
571
+ handleError(e);
572
+ }
573
+ });
574
+ issue.command('delete <ref...>').description('Delete issues (requires --yes for multiple)').action(async (refs, opts, cmd) => {
575
+ try {
576
+ await deleteIssues(refs, { ...opts, ...globalsFrom(cmd) });
577
+ }
578
+ catch (e) {
579
+ handleError(e);
580
+ }
581
+ });
582
+ const issueLabel = issue.command('label').description('Manage labels on an issue');
583
+ issueLabel.command('add <ref>').description('Add a label to an issue')
584
+ .requiredOption('--label <name>')
585
+ .addHelpText('after', `
586
+ Examples:
587
+ $ huly issue label add TSK-1 --label bug
588
+ $ huly issue label add TSK-1 --label auth --label backend`)
589
+ .action(async (ref, opts, cmd) => {
590
+ try {
591
+ await addIssueLabel(ref, opts.label, globalsFrom(cmd));
592
+ }
593
+ catch (e) {
594
+ handleError(e);
595
+ }
596
+ });
597
+ issueLabel.command('remove <ref>').description('Remove a label from an issue')
598
+ .requiredOption('--label <name>')
599
+ .addHelpText('after', `
600
+ Examples:
601
+ $ huly issue label remove TSK-1 --label bug`)
602
+ .action(async (ref, opts, cmd) => {
603
+ try {
604
+ await removeIssueLabel(ref, opts.label, globalsFrom(cmd));
605
+ }
606
+ catch (e) {
607
+ handleError(e);
608
+ }
609
+ });
610
+ const issueRel = issue.command('relation').description('Manage relations on an issue');
611
+ issueRel.command('add <ref>').description('Add a relation')
612
+ .requiredOption('--type <t>', 'blocks|isBlockedBy|relatesTo')
613
+ .requiredOption('--target <ref>')
614
+ .action(async (ref, opts, cmd) => {
615
+ try {
616
+ await addIssueRelation(ref, opts.type, opts.target, globalsFrom(cmd));
617
+ }
618
+ catch (e) {
619
+ handleError(e);
620
+ }
621
+ });
622
+ issueRel.command('remove <ref>').description('Remove a relation')
623
+ .requiredOption('--type <t>', 'blocks|isBlockedBy|relatesTo')
624
+ .requiredOption('--target <ref>')
625
+ .action(async (ref, opts, cmd) => {
626
+ try {
627
+ await removeIssueRelation(ref, opts.type, opts.target, globalsFrom(cmd));
628
+ }
629
+ catch (e) {
630
+ handleError(e);
631
+ }
632
+ });
633
+ issueRel.command('list <ref>').description('List relations on an issue')
634
+ .action(async (ref, _opts, cmd) => {
635
+ try {
636
+ await listIssueRelations(ref, globalsFrom(cmd));
637
+ }
638
+ catch (e) {
639
+ handleError(e);
640
+ }
641
+ });
642
+ issue.command('link-document <ref>').description('Link a document to an issue')
643
+ .requiredOption('--document <ref>')
644
+ .action(async (ref, opts, cmd) => {
645
+ try {
646
+ await linkDocument(ref, opts.document, globalsFrom(cmd));
647
+ }
648
+ catch (e) {
649
+ handleError(e);
650
+ }
651
+ });
652
+ issue.command('unlink-document <ref>').description('Unlink a document from an issue')
653
+ .requiredOption('--document <ref>')
654
+ .action(async (ref, opts, cmd) => {
655
+ try {
656
+ await unlinkDocument(ref, opts.document, globalsFrom(cmd));
657
+ }
658
+ catch (e) {
659
+ handleError(e);
660
+ }
661
+ });
662
+ issue.command('move <ref>').description('Move an issue (set/drop parent)')
663
+ .option('--parent <ref|null>', 'new parent ref, or literal "null" to drop')
664
+ .action(async (ref, opts, cmd) => {
665
+ try {
666
+ await moveIssue(ref, opts.parent ?? null, globalsFrom(cmd));
667
+ }
668
+ catch (e) {
669
+ handleError(e);
670
+ }
671
+ });
672
+ issue.command('preview-delete <ref...>').description('Preview the impact of deleting issues')
673
+ .action(async (refs, _opts, cmd) => {
674
+ try {
675
+ await previewDelete(refs, globalsFrom(cmd));
676
+ }
677
+ catch (e) {
678
+ handleError(e);
679
+ }
680
+ });
681
+ issue.command('related-targets').description('List related-issue-targets for a project')
682
+ .option('--project <ref>')
683
+ .action(async (opts, cmd) => {
684
+ try {
685
+ await relatedTargets('', { ...opts, ...globalsFrom(cmd) });
686
+ }
687
+ catch (e) {
688
+ handleError(e);
689
+ }
690
+ });
691
+ const relatedTarget = issue.command('related-target').description('Manage related-issue targets');
692
+ relatedTarget.command('set').description('Create a related-issue-target for a project')
693
+ .option('--project <ref>')
694
+ .requiredOption('--source <name>')
695
+ .requiredOption('--target <name>')
696
+ .action(async (opts, cmd) => {
697
+ try {
698
+ await setRelatedTarget({ ...opts, ...globalsFrom(cmd) });
699
+ }
700
+ catch (e) {
701
+ handleError(e);
702
+ }
703
+ });
704
+ const component = program.command('component').description('Manage tracker components');
705
+ withGlobalHelp(component);
706
+ component.command('list').description('List components')
707
+ .option('--project <ref>')
708
+ .action(async (opts, cmd) => {
709
+ try {
710
+ await listComponents({ ...opts, ...globalsFrom(cmd) });
711
+ }
712
+ catch (e) {
713
+ handleError(e);
714
+ }
715
+ });
716
+ component.command('get <ref>').description('Get a component').action(async (ref, opts, cmd) => {
717
+ try {
718
+ await getComponent(ref, { ...opts, ...globalsFrom(cmd) });
719
+ }
720
+ catch (e) {
721
+ handleError(e);
722
+ }
723
+ });
724
+ component.command('create').description('Create a component')
725
+ .option('--project <ref>')
726
+ .requiredOption('--label <name>')
727
+ .option('--description <text>')
728
+ .action(async (opts, cmd) => {
729
+ try {
730
+ await createComponent({ ...opts, ...globalsFrom(cmd) });
731
+ }
732
+ catch (e) {
733
+ handleError(e);
734
+ }
735
+ });
736
+ component.command('update <ref>').description('Update a component')
737
+ .option('--label <name>')
738
+ .option('--description <text>')
739
+ .action(async (ref, opts, cmd) => {
740
+ try {
741
+ await updateComponent(ref, { ...opts, ...globalsFrom(cmd) });
742
+ }
743
+ catch (e) {
744
+ handleError(e);
745
+ }
746
+ });
747
+ component.command('delete <ref...>').description('Delete components').action(async (refs, opts, cmd) => {
748
+ try {
749
+ await deleteComponents(refs, { ...opts, ...globalsFrom(cmd) });
750
+ }
751
+ catch (e) {
752
+ handleError(e);
753
+ }
754
+ });
755
+ const milestone = program.command('milestone').description('Manage tracker milestones');
756
+ withGlobalHelp(milestone);
757
+ milestone.command('list').description('List milestones')
758
+ .option('--project <ref>')
759
+ .action(async (opts, cmd) => {
760
+ try {
761
+ await listMilestones({ ...opts, ...globalsFrom(cmd) });
762
+ }
763
+ catch (e) {
764
+ handleError(e);
765
+ }
766
+ });
767
+ milestone.command('get <ref>').description('Get a milestone').action(async (ref, opts, cmd) => {
768
+ try {
769
+ await getMilestone(ref, { ...opts, ...globalsFrom(cmd) });
770
+ }
771
+ catch (e) {
772
+ handleError(e);
773
+ }
774
+ });
775
+ milestone.command('create').description('Create a milestone')
776
+ .option('--project <ref>')
777
+ .requiredOption('--label <name>')
778
+ .option('--description <text>')
779
+ .option('--target-date <iso>')
780
+ .action(async (opts, cmd) => {
781
+ try {
782
+ await createMilestone({ ...opts, ...globalsFrom(cmd) });
783
+ }
784
+ catch (e) {
785
+ handleError(e);
786
+ }
787
+ });
788
+ milestone.command('update <ref>').description('Update a milestone')
789
+ .option('--label <name>')
790
+ .option('--description <text>')
791
+ .option('--target-date <iso>')
792
+ .option('--status <s>')
793
+ .action(async (ref, opts, cmd) => {
794
+ try {
795
+ await updateMilestone(ref, { ...opts, ...globalsFrom(cmd) });
796
+ }
797
+ catch (e) {
798
+ handleError(e);
799
+ }
800
+ });
801
+ milestone.command('delete <ref...>').description('Delete milestones').action(async (refs, opts, cmd) => {
802
+ try {
803
+ await deleteMilestones(refs, { ...opts, ...globalsFrom(cmd) });
804
+ }
805
+ catch (e) {
806
+ handleError(e);
807
+ }
808
+ });
809
+ const tmpl = program.command('issue-template').description('Manage issue templates');
810
+ withGlobalHelp(tmpl);
811
+ tmpl.command('list').description('List templates')
812
+ .option('--project <ref>')
813
+ .action(async (opts, cmd) => {
814
+ try {
815
+ await listIssueTemplates({ ...opts, ...globalsFrom(cmd) });
816
+ }
817
+ catch (e) {
818
+ handleError(e);
819
+ }
820
+ });
821
+ tmpl.command('get <ref>').description('Get a template')
822
+ .action(async (ref, opts, cmd) => {
823
+ try {
824
+ await getIssueTemplate(ref, { ...opts, ...globalsFrom(cmd) });
825
+ }
826
+ catch (e) {
827
+ handleError(e);
828
+ }
829
+ });
830
+ tmpl.command('create').description('Create a template')
831
+ .option('--project <ref>')
832
+ .requiredOption('--title <t>')
833
+ .option('--description <text>')
834
+ .option('--body <md>')
835
+ .option('--body-file <path>')
836
+ .action(async (opts, cmd) => {
837
+ try {
838
+ await createIssueTemplate({ ...opts, ...globalsFrom(cmd) });
839
+ }
840
+ catch (e) {
841
+ handleError(e);
842
+ }
843
+ });
844
+ tmpl.command('update <ref>').description('Update a template')
845
+ .option('--title <t>')
846
+ .option('--description <text>')
847
+ .option('--body <md>')
848
+ .action(async (ref, opts, cmd) => {
849
+ try {
850
+ await updateIssueTemplate(ref, { ...opts, ...globalsFrom(cmd) });
851
+ }
852
+ catch (e) {
853
+ handleError(e);
854
+ }
855
+ });
856
+ tmpl.command('delete <ref...>').description('Delete templates').action(async (refs, opts, cmd) => {
857
+ try {
858
+ await deleteIssueTemplates(refs, { ...opts, ...globalsFrom(cmd) });
859
+ }
860
+ catch (e) {
861
+ handleError(e);
862
+ }
863
+ });
864
+ tmpl.command('add-child <template>').description('Add a child reference to a template')
865
+ .requiredOption('--child <ref>')
866
+ .action(async (template, opts, cmd) => {
867
+ try {
868
+ await addTemplateChild(template, opts.child, globalsFrom(cmd));
869
+ }
870
+ catch (e) {
871
+ handleError(e);
872
+ }
873
+ });
874
+ tmpl.command('remove-child <template>').description('Remove a child reference from a template')
875
+ .requiredOption('--child <ref>')
876
+ .action(async (template, opts, cmd) => {
877
+ try {
878
+ await removeTemplateChild(template, opts.child, globalsFrom(cmd));
879
+ }
880
+ catch (e) {
881
+ handleError(e);
882
+ }
883
+ });
884
+ const comment = program.command('comment').description('Manage comments (issue comments are ChatMessages)');
885
+ withGlobalHelp(comment);
886
+ comment.command('list').description('List comments on an issue')
887
+ .requiredOption('--issue <ref>')
888
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
889
+ .option('--offset <n>', 'offset', (v) => parseInt(v, 10))
890
+ .action(async (opts, cmd) => {
891
+ try {
892
+ await listComments({ ...opts, ...globalsFrom(cmd) });
893
+ }
894
+ catch (e) {
895
+ handleError(e);
896
+ }
897
+ });
898
+ comment.command('add').description('Add a comment to an issue')
899
+ .requiredOption('--issue <ref>')
900
+ .option('--body <md>')
901
+ .option('--body-file <path>')
902
+ .action(async (opts, cmd) => {
903
+ try {
904
+ await addComment({ ...opts, ...globalsFrom(cmd) });
905
+ }
906
+ catch (e) {
907
+ handleError(e);
908
+ }
909
+ });
910
+ comment.command('update <ref>').description('Update a comment\'s body')
911
+ .option('--body <md>')
912
+ .option('--body-file <path>')
913
+ .action(async (ref, opts, cmd) => {
914
+ try {
915
+ await updateComment(ref, { ...opts, ...globalsFrom(cmd) });
916
+ }
917
+ catch (e) {
918
+ handleError(e);
919
+ }
920
+ });
921
+ comment.command('delete <ref...>').description('Delete comments')
922
+ .action(async (refs, opts, cmd) => {
923
+ try {
924
+ await deleteComments(refs, { ...opts, ...globalsFrom(cmd) });
925
+ }
926
+ catch (e) {
927
+ handleError(e);
928
+ }
929
+ });
930
+ const channel = program.command('channel').description('Manage chunter channels (and their messages/threads)');
931
+ withGlobalHelp(channel);
932
+ channel.command('list').description('List channels')
933
+ .option('--archived <bool>', 'filter by archived state (true|false)', (v) => v !== 'false' && v !== '0')
934
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
935
+ .addHelpText('after', `
936
+ Examples:
937
+ $ huly channel list
938
+ $ huly channel list --archived false
939
+ $ huly channel list --json | jq -r '.[] | select(.topic != null) | .name'`)
940
+ .action(async (opts, cmd) => {
941
+ try {
942
+ await listChannels({ ...opts, ...globalsFrom(cmd) });
943
+ }
944
+ catch (e) {
945
+ handleError(e);
946
+ }
947
+ });
948
+ channel.command('get <ref>').description('Get a channel')
949
+ .addHelpText('after', `
950
+ Examples:
951
+ $ huly channel get engineering
952
+ $ huly channel get chunter:space.General # raw _id`)
953
+ .action(async (ref, opts, cmd) => {
954
+ try {
955
+ await getChannel(ref, { ...opts, ...globalsFrom(cmd) });
956
+ }
957
+ catch (e) {
958
+ handleError(e);
959
+ }
960
+ });
961
+ channel.command('create').description('Create a channel')
962
+ .requiredOption('--name <n>')
963
+ .option('--description <text>')
964
+ .option('--topic <text>')
965
+ .option('--private', 'private channel (members only)')
966
+ .option('--auto-join', 'new workspace members join automatically')
967
+ .option('--members <email...>', 'initial members (workspace members only)')
968
+ .addHelpText('after', `
969
+ Examples:
970
+ $ huly channel create --name engineering --topic "Eng discussions"
971
+ $ huly channel create --name leads --private --members alice@.. bob@..
972
+ $ huly channel create --name general --auto-join
973
+
974
+ Required: --name. Optional: --description, --topic, --private,
975
+ --auto-join, --members (space-separated emails).`)
976
+ .action(async (opts, cmd) => {
977
+ try {
978
+ await createChannel({ ...opts, ...globalsFrom(cmd) });
979
+ }
980
+ catch (e) {
981
+ handleError(e);
982
+ }
983
+ });
984
+ channel.command('update <ref>').description('Update a channel')
985
+ .option('--name <n>')
986
+ .option('--description <text>')
987
+ .option('--topic <text>')
988
+ .option('--private <bool>', 'true|false', (v) => v !== 'false' && v !== '0')
989
+ .option('--auto-join <bool>', 'true|false', (v) => v !== 'false' && v !== '0')
990
+ .addHelpText('after', `
991
+ Examples:
992
+ $ huly channel update engineering --topic "New topic"
993
+ $ huly channel update engineering --private true`)
994
+ .action(async (ref, opts, cmd) => {
995
+ try {
996
+ await updateChannel(ref, { ...opts, ...globalsFrom(cmd) });
997
+ }
998
+ catch (e) {
999
+ handleError(e);
1000
+ }
1001
+ });
1002
+ channel.command('delete <ref...>').description('Delete channels (DESTRUCTIVE; requires --yes)')
1003
+ .action(async (refs, opts, cmd) => {
1004
+ try {
1005
+ await deleteChannels(refs, { ...opts, ...globalsFrom(cmd) });
1006
+ }
1007
+ catch (e) {
1008
+ handleError(e);
1009
+ }
1010
+ });
1011
+ channel.command('archive <ref>').description('Archive a channel (--value false to unarchive)')
1012
+ .option('--value <bool>', 'true|false', (v) => v !== 'false' && v !== '0')
1013
+ .addHelpText('after', `
1014
+ Examples:
1015
+ $ huly channel archive engineering
1016
+ $ huly channel archive engineering --value false # unarchive`)
1017
+ .action(async (ref, opts, cmd) => {
1018
+ try {
1019
+ await archiveChannel(ref, { ...opts, ...globalsFrom(cmd) });
1020
+ }
1021
+ catch (e) {
1022
+ handleError(e);
1023
+ }
1024
+ });
1025
+ channel.command('unarchive <ref>').description('Unarchive a channel')
1026
+ .action(async (ref, opts, cmd) => {
1027
+ try {
1028
+ await archiveChannel(ref, { ...opts, value: false, ...globalsFrom(cmd) });
1029
+ }
1030
+ catch (e) {
1031
+ handleError(e);
1032
+ }
1033
+ });
1034
+ channel.command('members <ref>').description('List channel members')
1035
+ .addHelpText('after', `
1036
+ Examples:
1037
+ $ huly channel members engineering
1038
+ $ huly channel members engineering --json`)
1039
+ .action(async (ref, opts, cmd) => {
1040
+ try {
1041
+ await listChannelMembers(ref, { ...opts, ...globalsFrom(cmd) });
1042
+ }
1043
+ catch (e) {
1044
+ handleError(e);
1045
+ }
1046
+ });
1047
+ channel.command('join <ref>').description('Join a channel (--member <email> for a specific user)')
1048
+ .option('--member <email>', 'add a specific user (OWNER/admin only)')
1049
+ .addHelpText('after', `
1050
+ Examples:
1051
+ $ huly channel join engineering
1052
+ $ huly channel join engineering --member alice@example.com`)
1053
+ .action(async (ref, opts, cmd) => {
1054
+ try {
1055
+ await joinChannel(ref, { ...opts, ...globalsFrom(cmd) });
1056
+ }
1057
+ catch (e) {
1058
+ handleError(e);
1059
+ }
1060
+ });
1061
+ channel.command('leave <ref>').description('Leave a channel')
1062
+ .option('--member <email>', 'remove a specific user (OWNER/admin only)')
1063
+ .action(async (ref, opts, cmd) => {
1064
+ try {
1065
+ await leaveChannel(ref, { ...opts, ...globalsFrom(cmd) });
1066
+ }
1067
+ catch (e) {
1068
+ handleError(e);
1069
+ }
1070
+ });
1071
+ channel.command('add-member <ref>').description('Add one or more members')
1072
+ .requiredOption('--members <email...>', 'space-separated list')
1073
+ .addHelpText('after', `
1074
+ Examples:
1075
+ $ huly channel add-member engineering --members alice@example.com
1076
+ $ huly channel add-member engineering --members alice@.. bob@.. carol@..`)
1077
+ .action(async (ref, opts, cmd) => {
1078
+ try {
1079
+ await addChannelMembers(ref, opts.members, globalsFrom(cmd));
1080
+ }
1081
+ catch (e) {
1082
+ handleError(e);
1083
+ }
1084
+ });
1085
+ channel.command('remove-member <ref>').description('Remove one or more members')
1086
+ .requiredOption('--members <email...>', 'space-separated list')
1087
+ .addHelpText('after', `
1088
+ Examples:
1089
+ $ huly channel remove-member engineering --members alice@example.com
1090
+ $ huly channel remove-member engineering --members alice@.. bob@..`)
1091
+ .action(async (ref, opts, cmd) => {
1092
+ try {
1093
+ await removeChannelMembers(ref, opts.members, globalsFrom(cmd));
1094
+ }
1095
+ catch (e) {
1096
+ handleError(e);
1097
+ }
1098
+ });
1099
+ const cmsg = channel.command('message').description('Manage messages within a channel');
1100
+ cmsg.command('list <ref>').description('List messages in a channel')
1101
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1102
+ .addHelpText('after', `
1103
+ Examples:
1104
+ $ huly channel message list engineering
1105
+ $ huly channel message list engineering --limit 50
1106
+ $ huly channel message list engineering --json`)
1107
+ .action(async (ref, opts, cmd) => {
1108
+ try {
1109
+ await listChannelMessages(ref, { ...opts, ...globalsFrom(cmd) });
1110
+ }
1111
+ catch (e) {
1112
+ handleError(e);
1113
+ }
1114
+ });
1115
+ cmsg.command('send <ref>').description('Send a message to a channel')
1116
+ .option('--body <md>')
1117
+ .option('--body-file <path>')
1118
+ .action(async (ref, opts, cmd) => {
1119
+ try {
1120
+ await sendChannelMessage(ref, { ...opts, ...globalsFrom(cmd) });
1121
+ }
1122
+ catch (e) {
1123
+ handleError(e);
1124
+ }
1125
+ });
1126
+ cmsg.command('update <ref> <id>').description('Update a message')
1127
+ .option('--body <md>')
1128
+ .option('--body-file <path>')
1129
+ .action(async (ref, id, opts, cmd) => {
1130
+ try {
1131
+ await updateChannelMessage(ref, id, { ...opts, ...globalsFrom(cmd) });
1132
+ }
1133
+ catch (e) {
1134
+ handleError(e);
1135
+ }
1136
+ });
1137
+ cmsg.command('delete <ref> <messageIds...>').description('Delete one or more messages')
1138
+ .action(async (ref, messageIds, opts, cmd) => {
1139
+ try {
1140
+ await deleteChannelMessages(ref, messageIds, globalsFrom(cmd));
1141
+ }
1142
+ catch (e) {
1143
+ handleError(e);
1144
+ }
1145
+ });
1146
+ const dm = program.command('dm').description('Manage direct messages');
1147
+ withGlobalHelp(dm);
1148
+ dm.command('list').description('List DMs (spaces)')
1149
+ .action(async (opts, cmd) => {
1150
+ try {
1151
+ await listDms(globalsFrom(cmd));
1152
+ }
1153
+ catch (e) {
1154
+ handleError(e);
1155
+ }
1156
+ });
1157
+ dm.command('create').description('Create a DM (with --person or --members)')
1158
+ .option('--person <email>')
1159
+ .option('--members <email...>')
1160
+ .addHelpText('after', `
1161
+ Examples:
1162
+ $ huly dm create --person alice@example.com
1163
+ $ huly dm create --members alice@.. bob@.. # group DM`)
1164
+ .action(async (opts, cmd) => {
1165
+ try {
1166
+ await createDm({ ...opts, ...globalsFrom(cmd) });
1167
+ }
1168
+ catch (e) {
1169
+ handleError(e);
1170
+ }
1171
+ });
1172
+ // N1: dm.message mirrors channel.message (consistent nesting). The flat
1173
+ // 'dm messages' and 'dm send' commands remain as backward-compatible aliases.
1174
+ const dmMsg = dm.command('message').description('Manage messages within a DM (alias for `dm messages`/`dm send`)');
1175
+ dmMsg.command('list <dm>').description('List messages in a DM')
1176
+ .action(async (dm, opts, cmd) => {
1177
+ try {
1178
+ await listDmMessages(dm, { ...opts, ...globalsFrom(cmd) });
1179
+ }
1180
+ catch (e) {
1181
+ handleError(e);
1182
+ }
1183
+ });
1184
+ dmMsg.command('send <dm>').description('Send a DM message (or to <email> via --person)')
1185
+ .option('--body <md>')
1186
+ .option('--body-file <path>')
1187
+ .option('--person <email>', 'recipient email (auto-creates DM if needed)')
1188
+ .addHelpText('after', `
1189
+ Examples:
1190
+ $ huly dm message send <dmId> --body "hello"
1191
+ $ huly dm message send placeholder --person alice@.. --body "hi"
1192
+ $ huly dm message list <dmId>`)
1193
+ .action(async (dm, opts, cmd) => {
1194
+ try {
1195
+ await sendDmMessage(dm, { ...opts, ...globalsFrom(cmd) });
1196
+ }
1197
+ catch (e) {
1198
+ handleError(e);
1199
+ }
1200
+ });
1201
+ // Backward-compatible flat commands (deprecated; use dm message <verb>)
1202
+ dm.command('messages <dm>').description('[alias] List messages in a DM (use `dm message list`)')
1203
+ .action(async (dm, opts, cmd) => {
1204
+ try {
1205
+ await listDmMessages(dm, { ...opts, ...globalsFrom(cmd) });
1206
+ }
1207
+ catch (e) {
1208
+ handleError(e);
1209
+ }
1210
+ });
1211
+ dm.command('send <dm>').description('[alias] Send a DM message (use `dm message send`)')
1212
+ .option('--body <md>')
1213
+ .option('--body-file <path>')
1214
+ .option('--person <email>', 'recipient email (auto-creates DM if needed)')
1215
+ .action(async (dm, opts, cmd) => {
1216
+ try {
1217
+ await sendDmMessage(dm, { ...opts, ...globalsFrom(cmd) });
1218
+ }
1219
+ catch (e) {
1220
+ handleError(e);
1221
+ }
1222
+ });
1223
+ const thread = program.command('thread').description('Manage thread replies on a chat message');
1224
+ withGlobalHelp(thread);
1225
+ thread.command('list <target>').description('List replies on a target message')
1226
+ .action(async (target, opts, cmd) => {
1227
+ try {
1228
+ await listThreadReplies(target, globalsFrom(cmd));
1229
+ }
1230
+ catch (e) {
1231
+ handleError(e);
1232
+ }
1233
+ });
1234
+ thread.command('add <target>').description('Add a reply')
1235
+ .option('--body <md>')
1236
+ .option('--body-file <path>')
1237
+ .action(async (target, opts, cmd) => {
1238
+ try {
1239
+ await addThreadReply(target, { ...opts, ...globalsFrom(cmd) });
1240
+ }
1241
+ catch (e) {
1242
+ handleError(e);
1243
+ }
1244
+ });
1245
+ thread.command('update <replyId>').description('Update a reply')
1246
+ .option('--body <md>')
1247
+ .option('--body-file <path>')
1248
+ .action(async (replyId, opts, cmd) => {
1249
+ try {
1250
+ await updateThreadReply(replyId, { ...opts, ...globalsFrom(cmd) });
1251
+ }
1252
+ catch (e) {
1253
+ handleError(e);
1254
+ }
1255
+ });
1256
+ thread.command('delete <replyId...>').description('Delete replies')
1257
+ .action(async (replies, opts, cmd) => {
1258
+ try {
1259
+ await deleteThreadReplies(replies, globalsFrom(cmd));
1260
+ }
1261
+ catch (e) {
1262
+ handleError(e);
1263
+ }
1264
+ });
1265
+ // Note: the old `board:class:Card` (board module) commands were removed —
1266
+ // they're out of scope per the parity plan. The new `card:class:Card`
1267
+ // commands live below under the `card` command (Phase 12).
1268
+ const card = program.command('card').description('Manage Kanban cards (card module)');
1269
+ withGlobalHelp(card);
1270
+ card
1271
+ .command('list')
1272
+ .description('List cards')
1273
+ .option('--card-space <ref>')
1274
+ .option('--master-tag <ref>')
1275
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1276
+ .option('--offset <n>', 'offset', (v) => parseInt(v, 10))
1277
+ .action(async (opts, cmd) => {
1278
+ try {
1279
+ await listCards({ ...opts, ...globalsFrom(cmd) });
1280
+ }
1281
+ catch (e) {
1282
+ handleError(e);
1283
+ }
1284
+ });
1285
+ card.command('get <ref>').description('Get a card')
1286
+ .action(async (ref, opts, cmd) => {
1287
+ try {
1288
+ await getCard(ref, { ...opts, ...globalsFrom(cmd) });
1289
+ }
1290
+ catch (e) {
1291
+ handleError(e);
1292
+ }
1293
+ });
1294
+ card
1295
+ .command('create')
1296
+ .description('Create a card (requires --master-tag)')
1297
+ .requiredOption('--title <t>')
1298
+ .requiredOption('--master-tag <name|ref>', 'master-tag name (e.g. "Task") or _id')
1299
+ .option('--card-space <ref>', 'card space; defaults to card:space:Default')
1300
+ .option('--description <text>')
1301
+ .option('--body <md>')
1302
+ .option('--body-file <path>')
1303
+ .addHelpText('after', `
1304
+ Examples:
1305
+ $ huly card create --title "My card" --master-tag "Task"
1306
+ $ huly card create --title "..." --master-tag card:master-tag.Task \\
1307
+ --card-space card:space:Default --body-file ./spec.md
1308
+
1309
+ N9: --master-tag is REQUIRED. First-time setup usually requires creating a
1310
+ master-tag via the web UI (the CLI doesn't expose master-tag creation).
1311
+
1312
+ List available tags with \`huly master-tag list\`.`)
1313
+ .action(async (opts, cmd) => {
1314
+ try {
1315
+ await createCard({ ...opts, ...globalsFrom(cmd) });
1316
+ }
1317
+ catch (e) {
1318
+ handleError(e);
1319
+ }
1320
+ });
1321
+ card.command('update <ref>').description('Update a card')
1322
+ .option('--title <t>')
1323
+ .option('--description <text>')
1324
+ .option('--body <md>')
1325
+ .option('--body-file <path>')
1326
+ .action(async (ref, opts, cmd) => {
1327
+ try {
1328
+ await updateCard(ref, { ...opts, ...globalsFrom(cmd) });
1329
+ }
1330
+ catch (e) {
1331
+ handleError(e);
1332
+ }
1333
+ });
1334
+ card.command('delete <ref...>').description('Delete cards').action(async (refs, opts, cmd) => {
1335
+ try {
1336
+ await deleteCards(refs, { ...opts, ...globalsFrom(cmd) });
1337
+ }
1338
+ catch (e) {
1339
+ handleError(e);
1340
+ }
1341
+ });
1342
+ const cardSpace = program.command('card-space').description('Manage card spaces');
1343
+ withGlobalHelp(cardSpace);
1344
+ cardSpace.command('list').description('List card spaces')
1345
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1346
+ .action(async (opts, cmd) => {
1347
+ try {
1348
+ await listCardSpaces({ ...opts, ...globalsFrom(cmd) });
1349
+ }
1350
+ catch (e) {
1351
+ handleError(e);
1352
+ }
1353
+ });
1354
+ cardSpace.command('get <ref>').description('Get a card-space').action(async (ref, opts, cmd) => {
1355
+ try {
1356
+ await getCardSpace(ref, { ...opts, ...globalsFrom(cmd) });
1357
+ }
1358
+ catch (e) {
1359
+ handleError(e);
1360
+ }
1361
+ });
1362
+ cardSpace.command('create').description('Create a card-space')
1363
+ .requiredOption('--name <name>')
1364
+ .option('--description <text>')
1365
+ .action(async (opts, cmd) => {
1366
+ try {
1367
+ await createCardSpace({ ...opts, ...globalsFrom(cmd) });
1368
+ }
1369
+ catch (e) {
1370
+ handleError(e);
1371
+ }
1372
+ });
1373
+ cardSpace.command('delete <ref...>').description('Delete card-spaces')
1374
+ .action(async (refs, opts, cmd) => {
1375
+ try {
1376
+ await deleteCardSpaces(refs, { ...opts, ...globalsFrom(cmd) });
1377
+ }
1378
+ catch (e) {
1379
+ handleError(e);
1380
+ }
1381
+ });
1382
+ const mt = program.command('master-tag').description('Manage card master tags');
1383
+ withGlobalHelp(mt);
1384
+ mt.command('list').description('List master tags')
1385
+ .option('--card-space <ref>')
1386
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1387
+ .action(async (opts, cmd) => {
1388
+ try {
1389
+ await listMasterTags({ ...opts, ...globalsFrom(cmd) });
1390
+ }
1391
+ catch (e) {
1392
+ handleError(e);
1393
+ }
1394
+ });
1395
+ const action = program.command('action').description('Manage tasks (Planner ToDos)');
1396
+ withGlobalHelp(action);
1397
+ action
1398
+ .command('list')
1399
+ .description('List tasks')
1400
+ .option('--owner <email>')
1401
+ .option('--issue <ref>', 'filter to todos attached to an issue')
1402
+ .option('--title <q>', 'regex match on title')
1403
+ .option('--priority <p>', 'High|Medium|Low|NoPriority|Urgent')
1404
+ .option('--visibility <v>', 'public|busy|private')
1405
+ .option('--due-from <iso>')
1406
+ .option('--due-to <iso>')
1407
+ .option('--completed <bool>', 'true|false|all', (v) => v === 'all' ? 'all' : v !== 'false' && v !== '0')
1408
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1409
+ .option('--offset <n>', 'offset', (v) => parseInt(v, 10))
1410
+ .action(async (opts, cmd) => {
1411
+ try {
1412
+ await listActions({ ...opts, ...globalsFrom(cmd) });
1413
+ }
1414
+ catch (e) {
1415
+ handleError(e);
1416
+ }
1417
+ });
1418
+ action.command('get <ref>').description('Get a task')
1419
+ .action(async (ref, opts, cmd) => {
1420
+ try {
1421
+ await getAction(ref, { ...opts, ...globalsFrom(cmd) });
1422
+ }
1423
+ catch (e) {
1424
+ handleError(e);
1425
+ }
1426
+ });
1427
+ action
1428
+ .command('create')
1429
+ .description('Create a task')
1430
+ .requiredOption('--title <t>')
1431
+ .option('--description <text>')
1432
+ .option('--body <md>')
1433
+ .option('--body-file <path>')
1434
+ .option('--due <iso>')
1435
+ .option('--priority <p>')
1436
+ .option('--visibility <v>')
1437
+ .option('--owner <email>')
1438
+ .option('--attached-to <ref>')
1439
+ .option('--attached-to-class <class>')
1440
+ .action(async (opts, cmd) => {
1441
+ try {
1442
+ await createAction({ ...opts, ...globalsFrom(cmd) });
1443
+ }
1444
+ catch (e) {
1445
+ handleError(e);
1446
+ }
1447
+ });
1448
+ action.command('update <ref>').description('Update a task')
1449
+ .option('--title <t>')
1450
+ .option('--description <text>')
1451
+ .option('--body <md>')
1452
+ .option('--body-file <path>')
1453
+ .option('--due <iso>')
1454
+ .option('--priority <p>')
1455
+ .option('--visibility <v>')
1456
+ .option('--owner <email>')
1457
+ .action(async (ref, opts, cmd) => {
1458
+ try {
1459
+ await updateAction(ref, { ...opts, ...globalsFrom(cmd) });
1460
+ }
1461
+ catch (e) {
1462
+ handleError(e);
1463
+ }
1464
+ });
1465
+ action.command('complete <ref>').description('Mark a task done (sets doneOn=now)')
1466
+ .action(async (ref, opts, cmd) => {
1467
+ try {
1468
+ await completeAction(ref, globalsFrom(cmd));
1469
+ }
1470
+ catch (e) {
1471
+ handleError(e);
1472
+ }
1473
+ });
1474
+ action.command('reopen <ref>').description('Reopen a task (clears doneOn)')
1475
+ .action(async (ref, opts, cmd) => {
1476
+ try {
1477
+ await reopenAction(ref, globalsFrom(cmd));
1478
+ }
1479
+ catch (e) {
1480
+ handleError(e);
1481
+ }
1482
+ });
1483
+ action.command('schedule <ref>').description('Create a WorkSlot for the task')
1484
+ .requiredOption('--start <iso>')
1485
+ .requiredOption('--duration <minutes>', '', (v) => parseInt(v, 10))
1486
+ .option('--all-day')
1487
+ .action(async (ref, opts, cmd) => {
1488
+ try {
1489
+ await scheduleAction(ref, { ...opts, ...globalsFrom(cmd) });
1490
+ }
1491
+ catch (e) {
1492
+ handleError(e);
1493
+ }
1494
+ });
1495
+ action.command('unschedule <ref>').description('Remove WorkSlots for the task')
1496
+ .option('--slot-id <id>', 'remove a specific slot only')
1497
+ .action(async (ref, opts, cmd) => {
1498
+ try {
1499
+ await unscheduleAction(ref, { ...opts, ...globalsFrom(cmd) });
1500
+ }
1501
+ catch (e) {
1502
+ handleError(e);
1503
+ }
1504
+ });
1505
+ action.command('delete <ref...>').description('Delete tasks').action(async (refs, opts, cmd) => {
1506
+ try {
1507
+ await deleteActions(refs, { ...opts, ...globalsFrom(cmd) });
1508
+ }
1509
+ catch (e) {
1510
+ handleError(e);
1511
+ }
1512
+ });
1513
+ const doc = program.command('document').description('Manage documents (and their snapshots/inline comments)');
1514
+ withGlobalHelp(doc);
1515
+ doc
1516
+ .command('list')
1517
+ .description('List documents')
1518
+ .option('--teamspace <name|id>')
1519
+ .option('--title-search <q>', 'regex match on title')
1520
+ .option('--content-search <q>', 'best-effort regex match on content')
1521
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1522
+ .option('--offset <n>', 'offset', (v) => parseInt(v, 10))
1523
+ .action(async (opts, cmd) => {
1524
+ try {
1525
+ await listDocuments({ ...opts, ...globalsFrom(cmd) });
1526
+ }
1527
+ catch (e) {
1528
+ handleError(e);
1529
+ }
1530
+ });
1531
+ doc.command('get <ref>').description('Get a document (use --markdown to render the body)')
1532
+ .action(async (ref, opts, cmd) => {
1533
+ try {
1534
+ await getDocument(ref, { ...opts, ...globalsFrom(cmd) });
1535
+ }
1536
+ catch (e) {
1537
+ handleError(e);
1538
+ }
1539
+ });
1540
+ doc
1541
+ .command('create')
1542
+ .description('Create a document')
1543
+ .requiredOption('--title <t>')
1544
+ .option('--teamspace <name|id>', 'defaults to the first available teamspace')
1545
+ .option('--body <md>')
1546
+ .option('--body-file <path>')
1547
+ .option('--parent <ref|title>', 'parent ref or title (resolved within teamspace)')
1548
+ .action(async (opts, cmd) => {
1549
+ try {
1550
+ await createDocument({ ...opts, ...globalsFrom(cmd) });
1551
+ }
1552
+ catch (e) {
1553
+ handleError(e);
1554
+ }
1555
+ });
1556
+ doc
1557
+ .command('update <ref>')
1558
+ .description('Update a document (full body replace OR targeted --old-text/--new-text)')
1559
+ .option('--body <md>')
1560
+ .option('--body-file <path>')
1561
+ .option('--old-text <s>')
1562
+ .option('--new-text <s>')
1563
+ .option('--replace-all')
1564
+ .option('--title <t>')
1565
+ .option('--archived')
1566
+ .action(async (ref, opts, cmd) => {
1567
+ try {
1568
+ await updateDocument(ref, { ...opts, ...globalsFrom(cmd) });
1569
+ }
1570
+ catch (e) {
1571
+ handleError(e);
1572
+ }
1573
+ });
1574
+ doc.command('delete <ref...>').description('Delete documents').action(async (refs, opts, cmd) => {
1575
+ try {
1576
+ await deleteDocuments(refs, { ...opts, ...globalsFrom(cmd) });
1577
+ }
1578
+ catch (e) {
1579
+ handleError(e);
1580
+ }
1581
+ });
1582
+ doc.command('snapshots <ref>').description('List all snapshots for a document')
1583
+ .addHelpText('after', `
1584
+ Examples:
1585
+ $ huly document snapshots <docRef>
1586
+ $ huly document snapshots <docRef> --json
1587
+
1588
+ Use \`document snapshot --snapshot-id <id>\` to fetch a specific snapshot.`)
1589
+ .action(async (ref, opts, cmd) => {
1590
+ try {
1591
+ await listSnapshots(ref, { ...opts, ...globalsFrom(cmd) });
1592
+ }
1593
+ catch (e) {
1594
+ handleError(e);
1595
+ }
1596
+ });
1597
+ doc.command('snapshot <ref>').description('Get a specific snapshot (by --snapshot-id)')
1598
+ .requiredOption('--snapshot-id <id>')
1599
+ .addHelpText('after', `
1600
+ Examples:
1601
+ $ huly document snapshot <docRef> --snapshot-id 6a41527f12a078ec98cf64d5
1602
+ $ huly document snapshot <docRef> --snapshot-id 6a41527f12a078ec98cf64d5 --markdown
1603
+
1604
+ N4: \`snapshot\` (singular) gets one; \`snapshots\` (plural) lists all.`)
1605
+ .action(async (ref, opts, cmd) => {
1606
+ try {
1607
+ await getSnapshot(ref, { ...opts, ...globalsFrom(cmd) });
1608
+ }
1609
+ catch (e) {
1610
+ handleError(e);
1611
+ }
1612
+ });
1613
+ doc.command('inline-comments <ref>').description('List inline comments for a document')
1614
+ .action(async (ref, opts, cmd) => {
1615
+ try {
1616
+ await listInlineComments(ref, { ...opts, ...globalsFrom(cmd) });
1617
+ }
1618
+ catch (e) {
1619
+ handleError(e);
1620
+ }
1621
+ });
1622
+ const ts = program.command('teamspace').description('Manage document teamspaces');
1623
+ withGlobalHelp(ts);
1624
+ ts.command('list').description('List teamspaces')
1625
+ .action(async (_o, cmd) => {
1626
+ try {
1627
+ await listTeamspaces(globalsFrom(cmd));
1628
+ }
1629
+ catch (e) {
1630
+ handleError(e);
1631
+ }
1632
+ });
1633
+ ts.command('get <ref>').description('Get a teamspace')
1634
+ .action(async (ref, opts, cmd) => {
1635
+ try {
1636
+ await getTeamspace(ref, { ...opts, ...globalsFrom(cmd) });
1637
+ }
1638
+ catch (e) {
1639
+ handleError(e);
1640
+ }
1641
+ });
1642
+ ts.command('create').description('Create a teamspace')
1643
+ .requiredOption('--name <n>')
1644
+ .option('--description <text>')
1645
+ .option('--type <t>', 'public|private (default public)')
1646
+ .option('--private')
1647
+ .action(async (opts, cmd) => {
1648
+ try {
1649
+ await createTeamspace({ ...opts, ...globalsFrom(cmd) });
1650
+ }
1651
+ catch (e) {
1652
+ handleError(e);
1653
+ }
1654
+ });
1655
+ ts.command('update <ref>').description('Update a teamspace')
1656
+ .option('--name <n>')
1657
+ .option('--description <text>')
1658
+ .option('--archived <bool>', 'true|false', (v) => v !== 'false' && v !== '0')
1659
+ .action(async (ref, opts, cmd) => {
1660
+ try {
1661
+ await updateTeamspace(ref, { ...opts, ...globalsFrom(cmd) });
1662
+ }
1663
+ catch (e) {
1664
+ handleError(e);
1665
+ }
1666
+ });
1667
+ ts.command('delete <ref...>').description('Delete teamspaces').action(async (refs, opts, cmd) => {
1668
+ try {
1669
+ await deleteTeamspaces(refs, { ...opts, ...globalsFrom(cmd) });
1670
+ }
1671
+ catch (e) {
1672
+ handleError(e);
1673
+ }
1674
+ });
1675
+ const cal = program.command('calendar').description('Manage calendar events, schedules, calendars');
1676
+ withGlobalHelp(cal);
1677
+ // N5: 'calendars' (plural noun) lists CALENDAR OBJECTS; 'list' (verb) lists EVENTS.
1678
+ // 'get' (verb) gets an EVENT. To fetch a calendar's metadata, use 'calendars --json'.
1679
+ cal.command('calendars').description('List calendar objects (not events — see `calendar list` for events)')
1680
+ .addHelpText('after', `
1681
+ Examples:
1682
+ $ huly calendar calendars
1683
+ $ huly calendar calendars --json | jq -r '.[].name'`)
1684
+ .action(async (_o, cmd) => {
1685
+ try {
1686
+ await listCalendars(globalsFrom(cmd));
1687
+ }
1688
+ catch (e) {
1689
+ handleError(e);
1690
+ }
1691
+ });
1692
+ cal.command('create-calendar').description('Create a calendar')
1693
+ .requiredOption('--name <name>')
1694
+ .option('--description <text>')
1695
+ .option('--private', 'private calendar (members only)')
1696
+ .option('--access <a>', 'owner|team|public')
1697
+ .addHelpText('after', `
1698
+ Examples:
1699
+ $ huly calendar create-calendar --name "Work"
1700
+ $ huly calendar create-calendar --name "Personal" --private --access owner`)
1701
+ .action(async (opts, cmd) => {
1702
+ try {
1703
+ await createCalendar({ ...opts, ...globalsFrom(cmd) });
1704
+ }
1705
+ catch (e) {
1706
+ handleError(e);
1707
+ }
1708
+ });
1709
+ cal.command('delete-calendar <ref>').description('Delete a calendar')
1710
+ .action(async (ref, opts, cmd) => {
1711
+ try {
1712
+ await deleteCalendar(ref, { ...opts, ...globalsFrom(cmd) });
1713
+ }
1714
+ catch (e) {
1715
+ handleError(e);
1716
+ }
1717
+ });
1718
+ cal
1719
+ .command('list')
1720
+ .description('List EVENTS (not calendars — see `calendar calendars` for calendars)')
1721
+ .option('--start <iso>', 'ISO 8601 start date filter')
1722
+ .option('--end <iso>', 'ISO 8601 end date filter')
1723
+ .option('--calendar <id|name>', 'filter to a specific calendar')
1724
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1725
+ .addHelpText('after', `
1726
+ Examples:
1727
+ $ huly calendar list
1728
+ $ huly calendar list --start 2026-06-01 --end 2026-06-30
1729
+ $ huly calendar list --calendar "Work" --limit 20
1730
+ $ huly calendar list --json | jq -r '.[] | "\(.title): \(.date)"'`)
1731
+ .action(async (opts, cmd) => {
1732
+ try {
1733
+ await listEvents({ ...opts, ...globalsFrom(cmd) });
1734
+ }
1735
+ catch (e) {
1736
+ handleError(e);
1737
+ }
1738
+ });
1739
+ cal.command('get <ref>').description('Get an EVENT (not a calendar)')
1740
+ .addHelpText('after', `
1741
+ Examples:
1742
+ $ huly calendar get <eventRef>
1743
+ $ huly calendar get <eventRef> --markdown
1744
+
1745
+ To fetch a calendar (the container, not an event inside it), use
1746
+ \`calendar calendars --json\` and grep for the calendar id.`)
1747
+ .action(async (ref, opts, cmd) => {
1748
+ try {
1749
+ await getEvent(ref, { ...opts, ...globalsFrom(cmd) });
1750
+ }
1751
+ catch (e) {
1752
+ handleError(e);
1753
+ }
1754
+ });
1755
+ cal
1756
+ .command('create')
1757
+ .description('Create an event (or recurring event with --rrule)')
1758
+ .requiredOption('--title <t>')
1759
+ .requiredOption('--start <iso>')
1760
+ .requiredOption('--end <iso>')
1761
+ .option('--attendee <email>')
1762
+ .option('--location <text>')
1763
+ .option('--all-day')
1764
+ .option('--description <text>')
1765
+ .option('--body <md>')
1766
+ .option('--calendar-id <id>')
1767
+ .option('--rrule <string>', 'RRULE e.g. FREQ=DAILY;COUNT=3')
1768
+ .action(async (opts, cmd) => {
1769
+ try {
1770
+ await createEvent({ ...opts, ...globalsFrom(cmd) });
1771
+ }
1772
+ catch (e) {
1773
+ handleError(e);
1774
+ }
1775
+ });
1776
+ cal.command('update <ref>').description('Update an event')
1777
+ .option('--title <t>')
1778
+ .option('--description <text>')
1779
+ .option('--start <iso>')
1780
+ .option('--end <iso>')
1781
+ .option('--all-day')
1782
+ .option('--location <text>')
1783
+ .option('--attendee <email>')
1784
+ .action(async (ref, opts, cmd) => {
1785
+ try {
1786
+ await updateEvent(ref, { ...opts, ...globalsFrom(cmd) });
1787
+ }
1788
+ catch (e) {
1789
+ handleError(e);
1790
+ }
1791
+ });
1792
+ cal.command('delete <ref...>').description('Delete events').action(async (refs, opts, cmd) => {
1793
+ try {
1794
+ await deleteEvents(refs, { ...opts, ...globalsFrom(cmd) });
1795
+ }
1796
+ catch (e) {
1797
+ handleError(e);
1798
+ }
1799
+ });
1800
+ cal.command('recurring').description('List recurring events')
1801
+ .action(async (_o, cmd) => {
1802
+ try {
1803
+ await listRecurringEvents(globalsFrom(cmd));
1804
+ }
1805
+ catch (e) {
1806
+ handleError(e);
1807
+ }
1808
+ });
1809
+ cal.command('recurring-instances <ref>').description('List instances of a recurring event')
1810
+ .option('--start <iso>')
1811
+ .option('--end <iso>')
1812
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1813
+ .action(async (ref, opts, cmd) => {
1814
+ try {
1815
+ await listRecurringInstances(ref, { ...opts, ...globalsFrom(cmd) });
1816
+ }
1817
+ catch (e) {
1818
+ handleError(e);
1819
+ }
1820
+ });
1821
+ const schedule = program.command('schedule').description('Manage calendar schedules');
1822
+ withGlobalHelp(schedule);
1823
+ schedule.command('list').description('List schedules')
1824
+ .action(async (_o, cmd) => {
1825
+ try {
1826
+ await listSchedules(globalsFrom(cmd));
1827
+ }
1828
+ catch (e) {
1829
+ handleError(e);
1830
+ }
1831
+ });
1832
+ schedule.command('get <ref>').description('Get a schedule')
1833
+ .action(async (ref, opts, cmd) => {
1834
+ try {
1835
+ await getSchedule(ref, { ...opts, ...globalsFrom(cmd) });
1836
+ }
1837
+ catch (e) {
1838
+ handleError(e);
1839
+ }
1840
+ });
1841
+ schedule.command('create').description('Create a schedule')
1842
+ .requiredOption('--title <t>')
1843
+ .requiredOption('--owner <uuid>')
1844
+ .requiredOption('--time-zone <tz>')
1845
+ .option('--description <text>')
1846
+ .option('--duration <minutes>', 'meetingDuration', (v) => parseInt(v, 10))
1847
+ .option('--interval <minutes>', 'meetingInterval', (v) => parseInt(v, 10))
1848
+ .action(async (opts, cmd) => {
1849
+ try {
1850
+ await createSchedule({ ...opts, ...globalsFrom(cmd) });
1851
+ }
1852
+ catch (e) {
1853
+ handleError(e);
1854
+ }
1855
+ });
1856
+ schedule.command('update <ref>').description('Update a schedule')
1857
+ .option('--title <t>')
1858
+ .option('--description <text>')
1859
+ .option('--time-zone <tz>')
1860
+ .option('--duration <minutes>', '', (v) => parseInt(v, 10))
1861
+ .option('--interval <minutes>', '', (v) => parseInt(v, 10))
1862
+ .action(async (ref, opts, cmd) => {
1863
+ try {
1864
+ await updateSchedule(ref, { ...opts, ...globalsFrom(cmd) });
1865
+ }
1866
+ catch (e) {
1867
+ handleError(e);
1868
+ }
1869
+ });
1870
+ schedule.command('delete <ref...>').description('Delete schedules')
1871
+ .action(async (refs, opts, cmd) => {
1872
+ try {
1873
+ await deleteSchedules(refs, { ...opts, ...globalsFrom(cmd) });
1874
+ }
1875
+ catch (e) {
1876
+ handleError(e);
1877
+ }
1878
+ });
1879
+ const time = program.command('time').description('Time tracking on issues');
1880
+ withGlobalHelp(time);
1881
+ time.command('list').description('List time entries')
1882
+ .option('--issue <ref>')
1883
+ .option('--start <iso>')
1884
+ .option('--end <iso>')
1885
+ .option('--limit <n>', 'limit', (v) => parseInt(v, 10))
1886
+ .option('--offset <n>', 'offset', (v) => parseInt(v, 10))
1887
+ .action(async (opts, cmd) => {
1888
+ try {
1889
+ await listTimeEntries({ ...opts, ...globalsFrom(cmd) });
1890
+ }
1891
+ catch (e) {
1892
+ handleError(e);
1893
+ }
1894
+ });
1895
+ time.command('log').description('Log time on an issue')
1896
+ .requiredOption('--issue <ref>')
1897
+ .option('--minutes <n>', 'minutes spent', (v) => parseInt(v, 10))
1898
+ .option('--hours <n>', 'hours spent (decimal ok)', (v) => Number(v))
1899
+ .option('--description <text>')
1900
+ .option('--date <iso>', 'default now')
1901
+ .action(async (opts, cmd) => {
1902
+ try {
1903
+ await logTime({ ...opts, ...globalsFrom(cmd) });
1904
+ }
1905
+ catch (e) {
1906
+ handleError(e);
1907
+ }
1908
+ });
1909
+ time.command('report <issue>').description('Time report for a single issue')
1910
+ .action(async (issue, opts, cmd) => {
1911
+ try {
1912
+ await timeReport(issue, { ...opts, ...globalsFrom(cmd) });
1913
+ }
1914
+ catch (e) {
1915
+ handleError(e);
1916
+ }
1917
+ });
1918
+ time.command('delete <ref...>').description('Delete time entries')
1919
+ .action(async (refs, opts, cmd) => {
1920
+ try {
1921
+ await deleteTimeEntries(refs, { ...opts, ...globalsFrom(cmd) });
1922
+ }
1923
+ catch (e) {
1924
+ handleError(e);
1925
+ }
1926
+ });
1927
+ const raw = program.command('api').description('Raw HTTP escape hatch');
1928
+ raw
1929
+ .argument('<method>', 'HTTP method')
1930
+ .argument('<path>', 'URL path (e.g. /_accounts, /_transactor/...)')
1931
+ .option('--body <json>', 'request body')
1932
+ .option('--query <kv...>', 'query params k=v')
1933
+ .option('--header <kv...>', 'headers k=v')
1934
+ .action(async (method, path, opts) => {
1935
+ try {
1936
+ await apiCommand(method, path, opts);
1937
+ }
1938
+ catch (e) {
1939
+ handleError(e);
1940
+ }
1941
+ });
1942
+ const wsCmd = program.command('ws').description('Raw WebSocket escape hatch (text JSON only)');
1943
+ wsCmd
1944
+ .argument('<method>', 'RPC method (e.g. findAll, tx)')
1945
+ .argument('[params]', 'JSON-encoded array of positional params')
1946
+ .option('--no-ping', 'disable ping/pong')
1947
+ .action(async (method, params, opts) => {
1948
+ try {
1949
+ await wsCommand(method, params, opts);
1950
+ }
1951
+ catch (e) {
1952
+ handleError(e);
1953
+ }
1954
+ });
1955
+ attachToChildren(program);
1956
+ attachGlobalOpts(program, { skipNonInteractive: true });
1957
+ await program.parseAsync(argv);
1958
+ }
1959
+ //# sourceMappingURL=cli.js.map