@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/README.md ADDED
@@ -0,0 +1,2576 @@
1
+ # huly-cli
2
+
3
+ AI-agent-first CLI for self-hosted Huly.
4
+
5
+ `huly` is a unified command-line interface for the Huly platform. It wraps
6
+ the Huly SDK into scriptable commands so you can automate workspace tasks,
7
+ integrate Huly into CI/CD pipelines, or operate Huly from agents without
8
+ a browser.
9
+
10
+ This README is the canonical reference. It is intentionally long so you can
11
+ find what you need without leaving the docs. The CLI's own `--help` output
12
+ is concise by design; this document expands it with examples, caveats, and
13
+ the rationale behind design decisions.
14
+
15
+ ---
16
+
17
+ ## Table of Contents
18
+
19
+ 1. [Why huly-cli](#why-huly-cli)
20
+ 2. [Installation](#installation)
21
+ 3. [Configuration](#configuration)
22
+ 4. [Authentication](#authentication)
23
+ 5. [Global flags](#global-flags)
24
+ 6. [Output modes](#output-modes)
25
+ 7. [Ref resolution](#ref-resolution)
26
+ 8. [Command reference](#command-reference)
27
+ - [login / whoami](#login--whoami)
28
+ - [workspace](#workspace)
29
+ - [user](#user)
30
+ - [project](#project)
31
+ - [issue](#issue)
32
+ - [component](#component)
33
+ - [milestone](#milestone)
34
+ - [issue-template](#issue-template)
35
+ - [comment](#comment)
36
+ - [channel](#channel)
37
+ - [dm](#dm)
38
+ - [thread](#thread)
39
+ - [card](#card)
40
+ - [card-space](#card-space)
41
+ - [master-tag](#master-tag)
42
+ - [action](#action)
43
+ - [document](#document)
44
+ - [teamspace](#teamspace)
45
+ - [calendar](#calendar)
46
+ - [schedule](#schedule)
47
+ - [time](#time)
48
+ 9. [Common workflows](#common-workflows)
49
+ 10. [Output mode reference](#output-mode-reference)
50
+ 11. [Class ID reference](#class-id-reference)
51
+ 12. [Plugin / model surface map](#plugin--model-surface-map)
52
+ 13. [Escape hatches](#escape-hatches)
53
+ 14. [Internal architecture](#internal-architecture)
54
+ 15. [Environment variables reference](#environment-variables-reference)
55
+ 16. [Troubleshooting](#troubleshooting)
56
+ 17. [Performance & limits](#performance--limits)
57
+ 18. [Security model](#security-model)
58
+ 19. [Compatibility matrix](#compatibility-matrix)
59
+ 20. [Development](#development)
60
+ 21. [Cross-references](#cross-references)
61
+ 22. [License](#license)
62
+
63
+ ---
64
+
65
+ ## Why huly-cli
66
+
67
+ Huly's web UI is great for interactive use. The SDK is great for programmatic
68
+ use. `huly-cli` bridges them for shell-and-script use cases:
69
+
70
+ - **Shell pipelines**: pipe `huly issue list --json` to `jq`, `xargs`, etc.
71
+ - **CI/CD**: log issues from CI failures, link commits, close on merge
72
+ - **Agents**: any LLM can drive the CLI; no browser, no Playwright
73
+ - **Cron/automation**: daily backups of comments, scheduled cleanup, audits
74
+ - **Cross-workspace ops**: bulk-move issues between workspaces
75
+ - **Offline scripting**: write ops as bash scripts, version them in git
76
+
77
+ The CLI mirrors the platform's domain model — projects, issues, channels,
78
+ calendar events — so there's a 1:1 mapping between `huly <surface> <verb>`
79
+ and the underlying SDK calls.
80
+
81
+ ---
82
+
83
+ ## Installation
84
+
85
+ ### From npm
86
+
87
+ ```bash
88
+ npm install -g huly-cli
89
+ huly --version
90
+ ```
91
+
92
+ ### From source
93
+
94
+ ```bash
95
+ git clone https://github.com/your-org/huly-cli.git
96
+ cd huly-cli
97
+ npm install
98
+ npm run build
99
+ node dist/index.js --version
100
+
101
+ # Optional: install `huly` on PATH
102
+ ln -s "$(pwd)/dist/index.js" /usr/local/bin/huly
103
+ ```
104
+
105
+ The repo's `bin/huly` script wraps `node dist/index.js "$@"`, so it's
106
+ also fine to add `bin/` to PATH directly.
107
+
108
+ ### Node version
109
+
110
+ Tested on Node 22.11 and Node 24. Node 20 lacks some `crypto` features
111
+ the SDK uses. Node 26 fails the rush build check (this repo's build chain
112
+ version-checks Node major).
113
+
114
+ If you must run Node 26, set:
115
+ ```bash
116
+ export RUSH_ALLOW_UNSUPPORTED_NODEJS=1
117
+ ```
118
+
119
+ ### Dependencies
120
+
121
+ - `node` >= 22.11
122
+ - `npm` >= 9
123
+ - A Huly server reachable from where you run the CLI
124
+ - Credentials for at least one Huly account
125
+
126
+ ---
127
+
128
+ ## Configuration
129
+
130
+ Configuration comes from (in precedence order):
131
+
132
+ 1. CLI flags (highest)
133
+ 2. Shell environment variables
134
+ 3. `~/.config/huly/.env` (loaded automatically if present)
135
+
136
+ ### Config files
137
+
138
+ | Path | Purpose |
139
+ |---|---|
140
+ | `~/.config/huly/.env` | Login + URL config (mode 0600 recommended) |
141
+ | `~/.config/huly/credentials.json` | Cached JWT tokens (mode 0600) |
142
+ | `~/.config/huly/active-workspace` | Last-used workspace name |
143
+
144
+ The CLI creates these on first run. Deleting `credentials.json` forces
145
+ re-login on the next invocation.
146
+
147
+ ### Minimal `.env`
148
+
149
+ ```bash
150
+ export HULY_URL=https://huly.example.com
151
+ export HULY_EMAIL=you@example.com
152
+ export HULY_PASSWORD=your-password
153
+ ```
154
+
155
+ ### Strict mode `.env` (CI-friendly)
156
+
157
+ ```bash
158
+ export HULY_URL=https://huly.example.com
159
+ export HULY_TOKEN=eyJ0eXAiOiJKV1Q... # pre-issued account JWT, skip login
160
+ export HULY_WORKSPACE=production
161
+ export HULY_PROJECT=BACKEND # for bare-number issue refs
162
+ export HULY_NONINTERACTIVE=1 # disable all prompts
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Authentication
168
+
169
+ The CLI supports three auth modes:
170
+
171
+ ### 1. Password login (interactive)
172
+
173
+ ```bash
174
+ huly login
175
+ # prompts for password if HULY_PASSWORD is unset
176
+ ```
177
+
178
+ ### 2. Password login (headless)
179
+
180
+ ```bash
181
+ huly login --headless
182
+ # reads HULY_EMAIL + HULY_PASSWORD from env only
183
+ # never prompts
184
+ ```
185
+
186
+ ### 3. Pre-issued token
187
+
188
+ ```bash
189
+ export HULY_TOKEN=eyJ0...
190
+ huly whoami
191
+ ```
192
+
193
+ Useful for service accounts and CI where you don't want a stored password.
194
+
195
+ ### Token caching
196
+
197
+ After login, the CLI stores the **account token** and **workspace tokens**
198
+ in `~/.config/huly/credentials.json`. Each subsequent invocation reuses
199
+ the cache until tokens expire.
200
+
201
+ ```bash
202
+ # clear cache
203
+ rm ~/.config/huly/credentials.json
204
+
205
+ # verify cache contents
206
+ cat ~/.config/huly/credentials.json | jq .
207
+ ```
208
+
209
+ ### Logout
210
+
211
+ There's no `huly logout` command. Either:
212
+
213
+ ```bash
214
+ rm ~/.config/huly/credentials.json
215
+ ```
216
+
217
+ Or unset the tokens in the file. Logout is intentionally manual so you
218
+ don't accidentally drop credentials during a long automation run.
219
+
220
+ ### Signup
221
+
222
+ `huly signup` does not exist as a CLI command (the platform requires
223
+ email-confirmation UX the CLI can't provide). Sign up via:
224
+
225
+ - The web UI
226
+ - The `accountClient.signUp` SDK call directly
227
+ - An admin's invite link (`huly workspace access-link --role GUEST`)
228
+
229
+ After signup, `huly login` works normally.
230
+
231
+ ---
232
+
233
+ ## Global flags
234
+
235
+ These flags work on every command. They may be placed before or after
236
+ the subcommand:
237
+
238
+ ```bash
239
+ huly --workspace prod issue list
240
+ huly issue list --workspace prod # equivalent
241
+ ```
242
+
243
+ | Flag | Description |
244
+ |---|---|
245
+ | `--url <url>` | Server URL (overrides `HULY_URL`) |
246
+ | `--workspace <name>` | Active workspace (overrides `HULY_WORKSPACE`). Name or UUID. |
247
+ | `--json` | Output machine-readable JSON |
248
+ | `--ci` | Alias for `--json`. Same effect; signals non-interactive intent. |
249
+ | `--markdown` | Output body content as raw markdown (skips markup resolution) |
250
+ | `--dry-run` | Print the tx that would be applied, do not apply |
251
+ | `--minimal` | Skip smart defaults (no auto-Teamspace, no auto-IssueStatus) |
252
+ | `-y, --yes` | Skip confirmation prompts (required for destructive ops) |
253
+ | `--non-interactive` | Same as `--yes` + disable any interactive prompts |
254
+
255
+ ### Precedence rules
256
+
257
+ - A flag on the subcommand overrides the flag on the parent
258
+ - A flag after the subcommand overrides the flag before
259
+ - `--workspace prod issue list` ≡ `issue list --workspace prod`
260
+ - `huly login --workspace prod` is a no-op — login is workspace-independent
261
+
262
+ ### Exit codes
263
+
264
+ | Code | Meaning |
265
+ |---|---|
266
+ | 0 | Success |
267
+ | 1 | Generic error (uncaught exception, network failure, etc.) |
268
+ | 2 | Validation error (missing required arg, invalid ref, etc.) |
269
+ | 3 | Not found (ref doesn't exist) |
270
+ | 4 | Forbidden (insufficient permissions) |
271
+ | 64 | Usage error (no command given, unknown subcommand) |
272
+
273
+ All errors are exit-coded; pipe-friendly. `set -e` works as expected.
274
+
275
+ ---
276
+
277
+ ## Output modes
278
+
279
+ ### Table (default)
280
+
281
+ Designed for humans. Auto-sizes columns, truncates long fields, hides
282
+ uninteresting ones:
283
+
284
+ ```
285
+ ID NAME DESCRIPTION _ID
286
+ ──── ───────── ─────────────────────── ────────────
287
+ TSK Default Default project faultProject
288
+ DEMO Demo Demo project emoProject
289
+ ```
290
+
291
+ ### JSON (`--json` / `--ci`)
292
+
293
+ Full objects, arrays for lists. Designed for `jq` / `xargs`:
294
+
295
+ ```json
296
+ [
297
+ {
298
+ "_id": "tracker:project:DefaultProject",
299
+ "_class": "tracker:class:Project",
300
+ "name": "Default",
301
+ "identifier": "TSK",
302
+ "description": "Default project",
303
+ "private": false,
304
+ "archived": false,
305
+ "members": [],
306
+ "modifiedBy": "core:account:System",
307
+ "modifiedOn": 1782697470759
308
+ }
309
+ ]
310
+ ```
311
+
312
+ ### CI mode (`--ci`)
313
+
314
+ Identical to `--json`. Use `--ci` in shell scripts to signal "I expect
315
+ machine-readable output, do not prompt for input" — helps future maintainers
316
+ understand intent. (Currently no behavioral difference; reserved for future
317
+ strict-mode behavior.)
318
+
319
+ ### Markdown body (`--markdown`)
320
+
321
+ For resources that have body content (documents, comments, channel messages,
322
+ issue descriptions), `--markdown` returns the raw markdown text instead
323
+ of a table:
324
+
325
+ ```bash
326
+ huly document get <ref> --markdown
327
+ # prints: # Hello\nThis is the document body in markdown.
328
+ ```
329
+
330
+ The CLI's read path catches markup resolution failures and falls back to
331
+ returning the raw body string. If a doc was created with the CLI (which
332
+ stores bodies as raw strings), `--markdown` returns that string verbatim.
333
+
334
+ ---
335
+
336
+ ## Ref resolution
337
+
338
+ References to documents can be specified in several ways. The CLI tries
339
+ each in order:
340
+
341
+ ### 1. Raw `_id`
342
+
343
+ The full class-prefixed ID. Always works, slowest:
344
+
345
+ ```bash
346
+ huly issue get tracker:issue:6a41527f12a078ec98cf64d5
347
+ ```
348
+
349
+ ### 2. Prefixed form
350
+
351
+ For issues: `<PROJECT_IDENTIFIER>-<NUMBER>`. Resolved via the local index
352
+ of issues:
353
+
354
+ ```bash
355
+ huly issue get TSK-1
356
+ ```
357
+
358
+ ### 3. Bare number
359
+
360
+ If `HULY_PROJECT` is set, bare numbers resolve against that project's
361
+ issues:
362
+
363
+ ```bash
364
+ export HULY_PROJECT=TSK
365
+ huly issue get 1 # equivalent to TSK-1
366
+ ```
367
+
368
+ ### 4. Title match
369
+
370
+ Case-insensitive match on the document's title. Used for documents,
371
+ teamspaces, projects, etc. (not issues):
372
+
373
+ ```bash
374
+ huly document get "My design doc"
375
+ ```
376
+
377
+ ### Resolution algorithm
378
+
379
+ 1. Check if it matches `_id` regex (`<prefix>:<prefix>:<id>`)
380
+ 2. Check if it matches prefixed issue form (`[A-Z]+-\d+`)
381
+ 3. Check if it's a bare number with `HULY_PROJECT` set
382
+ 4. Look up in the local class index (built from prior `findAll`)
383
+ 5. Try `findOne` by name/title
384
+ 6. Throw `NotFound` with candidate suggestions
385
+
386
+ The local index is **invalidated automatically after writes** to the same
387
+ class. Cross-class writes (e.g. updating an issue doesn't invalidate the
388
+ project index) require a fresh process.
389
+
390
+ ---
391
+
392
+ ## Command reference
393
+
394
+ This section documents every command in detail. Commands are grouped by
395
+ top-level resource.
396
+
397
+ ### login / whoami
398
+
399
+ ```bash
400
+ huly login # interactive
401
+ huly login --headless # env-only
402
+ huly whoami # show current account + workspace
403
+ huly whoami --json # machine-readable
404
+ ```
405
+
406
+ `whoami` output:
407
+
408
+ ```
409
+ URL: https://huly.example.com
410
+ Account: you@example.com
411
+ Workspace: production (uuid=..., mode=active)
412
+ ```
413
+
414
+ ---
415
+
416
+ ### workspace
417
+
418
+ Workspace-level operations.
419
+
420
+ ```bash
421
+ huly workspace list # list all accessible workspaces
422
+ huly workspace current # show current workspace
423
+ huly workspace use <name> # set active workspace
424
+ huly workspace create --name X # create (requires --yes)
425
+ huly workspace delete --yes # delete current (requires --yes)
426
+ huly workspace delete --yes --force # delete active workspace
427
+ huly workspace info # show uuid, region, mode
428
+ huly workspace members # list members (OWNER role required)
429
+ huly workspace member <uuid> --role MAINTAINER # change role
430
+ huly workspace rename <new-name> # rename current
431
+ huly workspace guests --read-only true # toggle guest read-only
432
+ huly workspace guests --sign-up true # toggle guest sign-up
433
+ huly workspace access-link --role GUEST # create invite link
434
+ huly workspace regions # list available regions
435
+ ```
436
+
437
+ **Destructive:** `delete` requires `--yes`. Deleting the active workspace
438
+ additionally requires `--force`.
439
+
440
+ **Permissions:** `delete`, `member`, `rename`, `guests`, `access-link`
441
+ require OWNER role. `info`, `members`, `list`, `use`, `current`, `regions`
442
+ require membership.
443
+
444
+ ---
445
+
446
+ ### user
447
+
448
+ Account-level identity operations.
449
+
450
+ ```bash
451
+ huly user get # current user profile
452
+ huly user get --ref <uuid> # by account uuid
453
+ huly user update --city "Berlin" # update profile fields
454
+ huly user find <email> # look up by email (returns personUuid)
455
+ ```
456
+
457
+ `user find` resolution order:
458
+ 1. Try `accountClient.findPersonBySocialKey` (account-level)
459
+ 2. Fall back to workspace-local `Person` scan (name match)
460
+
461
+ Both paths may fail if the user is not in your workspace.
462
+
463
+ ---
464
+
465
+ ### project
466
+
467
+ Tracker project operations.
468
+
469
+ ```bash
470
+ huly project list [--limit N] [--offset N]
471
+ huly project get <ref> # by identifier, name, or _id
472
+ huly project create --name X --identifier BACKEND [--description] [--private]
473
+ huly project update <ref> --set description="..."
474
+ huly project update <ref> --set description=null # clear field
475
+ huly project update <ref> --set private=true
476
+ huly project delete <ref...> [--yes]
477
+ huly project statuses --project TSK
478
+ huly project target-preferences --project TSK
479
+ huly project target-preference upsert --project TSK --key ... --value ...
480
+ ```
481
+
482
+ **Identifier rules:**
483
+ - Must be uppercase letters and digits only
484
+ - 1-5 characters typical
485
+ - Unique per workspace (CLI pre-checks for duplicates; this selfhost's
486
+ server does not enforce uniqueness server-side)
487
+
488
+ **`--set` semantics:** Pass `key=value` to set, `key=null` to clear.
489
+ Anything else is left unchanged.
490
+
491
+ ---
492
+
493
+ ### issue
494
+
495
+ Tracker issue operations — the most-used surface.
496
+
497
+ ```bash
498
+ huly issue list [--project TSK] [--status <name>] [--status-category Active]
499
+ [--assignee <email>] [--label bug] [--parent <ref>|null]
500
+ [--description-search <q>] [--limit N] [--offset N]
501
+
502
+ huly issue get <ref> [--markdown] # by prefixed (TSK-1), bare (1), or _id
503
+ huly issue create --project TSK --title "..." [--description] [--body <md>]
504
+ [--body-file <path>] [--status <name>] [--priority <p>]
505
+ [--assignee <email>] [--label bug --label auth]
506
+ [--due 2026-07-01T14:00:00Z] [--parent <ref>]
507
+ [--task-type <name>]
508
+ huly issue update <ref> --title "..." # any combination of updatable fields
509
+ huly issue delete <ref...> [--yes]
510
+ huly issue preview-delete <ref...> # show what delete would affect
511
+
512
+ huly issue label <ref> add <name>
513
+ huly issue label <ref> remove <name>
514
+
515
+ huly issue relation <ref> add <type> <targetRef> # type: blocks|isBlockedBy|relatesTo
516
+ huly issue relation <ref> remove <type> <targetRef>
517
+ huly issue relation <ref> list
518
+
519
+ huly issue link-document <ref> <docRef>
520
+ huly issue unlink-document <ref> <docRef>
521
+
522
+ huly issue move <ref> --parent <parentRef> # set parent
523
+ huly issue move <ref> --parent null # clear parent
524
+
525
+ huly issue related-targets --project TSK
526
+ huly issue related-target set --project TSK --source <ref> --target <ref>
527
+ ```
528
+
529
+ **Status categories:** `UnStarted | ToDo | Active | Won | Lost`. Used by
530
+ the kanban-style status filters.
531
+
532
+ **Priorities:** `Urgent | High | Normal | Low | None`. Server-side enum;
533
+ the CLI auto-matches case-insensitively.
534
+
535
+ **Body format:** Markdown. Stored as raw string (the SDK's
536
+ `MarkupContent` upload is bypassed because the collaborator's
537
+ `createMarkup` RPC throws on this selfhost).
538
+
539
+ **`--markdown` on get:** returns raw body string. For CLI-created
540
+ documents (which store raw strings), this works correctly. For web-UI-created
541
+ documents with markup refs to y-docs, the ref string is returned instead.
542
+
543
+ **Known issue:** Issue create requires the project to have at least one
544
+ IssueStatus. On workspaces where the tracker migration didn't seed statuses,
545
+ issue create fails with "no IssueStatus in workspace". Workaround: create
546
+ a status manually via the web UI first.
547
+
548
+ ---
549
+
550
+ ### component
551
+
552
+ ```bash
553
+ huly component list --project TSK
554
+ huly component get <ref>
555
+ huly component create --project TSK --label "Backend"
556
+ huly component update <ref> --label "New Name"
557
+ huly component delete <ref...> [--yes]
558
+ ```
559
+
560
+ **Known issue:** `component list` returns 0 results after a successful
561
+ `create` on this selfhost. The create succeeds (returns an `_id`) but the
562
+ list doesn't find it. Tracked as C2 in `docs/open-issues.md`. Same issue
563
+ affects milestone, issue-template, and time-entry lists.
564
+
565
+ ---
566
+
567
+ ### milestone
568
+
569
+ ```bash
570
+ huly milestone list --project TSK
571
+ huly milestone get <ref>
572
+ huly milestone create --project TSK --label "v1.0" [--due 2026-08-01]
573
+ huly milestone update <ref> --label "v1.0 Final" --due 2026-08-15
574
+ huly milestone delete <ref...> [--yes]
575
+ ```
576
+
577
+ ---
578
+
579
+ ### issue-template
580
+
581
+ ```bash
582
+ huly issue-template list --project TSK
583
+ huly issue-template get <ref>
584
+ huly issue-template create --project TSK --title "Bug template"
585
+ huly issue-template update <ref> --title "..."
586
+ huly issue-template delete <ref...> [--yes]
587
+ huly issue-template add-child <templateRef> <childRef> # template refs can include other templates
588
+ huly issue-template remove-child <templateRef> <childRef>
589
+ ```
590
+
591
+ ---
592
+
593
+ ### comment
594
+
595
+ ```bash
596
+ huly comment list --issue <ref> # issue can be TSK-1 or full _id
597
+ huly comment add --issue TSK-1 --body "Looking into this"
598
+ huly comment add --issue TSK-1 --body-file ./comment.md
599
+ huly comment update <commentRef> --body "Updated text"
600
+ huly comment delete <ref...> [--yes]
601
+ ```
602
+
603
+ ---
604
+
605
+ ### channel
606
+
607
+ ```bash
608
+ huly channel list [--archived]
609
+ huly channel get <ref>
610
+ huly channel create --name "engineering" [--topic "..."] [--private]
611
+ huly channel update <ref> --topic "..."
612
+ huly channel delete <ref...> [--yes]
613
+ huly channel archive <ref> [--value false] # value=false to unarchive
614
+ huly channel members <ref>
615
+ huly channel join <ref> # join self
616
+ huly channel join <ref> --member alice@... # join specific user
617
+ huly channel leave <ref>
618
+ huly channel add-member <ref> alice@... # one or more members
619
+ huly channel remove-member <ref> alice@...
620
+
621
+ huly channel message list <channelRef>
622
+ huly channel message get <channelRef> <messageRef> [--markdown]
623
+ huly channel message create <channelRef> --body "hello" [--body-file <path>]
624
+ huly channel message update <channelRef> <messageRef> --body "edited"
625
+ huly channel message delete <channelRef> <messageRef...> [--yes]
626
+ ```
627
+
628
+ ---
629
+
630
+ ### dm
631
+
632
+ Direct messages.
633
+
634
+ ```bash
635
+ huly dm list # list DM spaces
636
+ huly dm create --person alice@example.com # create 1:1 DM
637
+ huly dm messages <dmRef>
638
+ huly dm send <dmRef> --body "hi"
639
+ huly dm send --person alice@example.com --body "hi" # auto-creates DM
640
+ ```
641
+
642
+ ---
643
+
644
+ ### thread
645
+
646
+ Replies to chat messages (channel messages or DM messages).
647
+
648
+ ```bash
649
+ huly thread list <targetRef> # target = channel + message _id, or just message _id
650
+ huly thread add <targetRef> --body "reply" [--body-file <path>]
651
+ huly thread update <replyRef> --body "edited"
652
+ huly thread delete <replyRef...> [--yes]
653
+ ```
654
+
655
+ ---
656
+
657
+ ### card
658
+
659
+ Card module (separate from tracker issues).
660
+
661
+ ```bash
662
+ huly card list
663
+ huly card get <ref> [--markdown]
664
+ huly card create --master-tag <name|id> --title "..." [--body <md>] [--body-file <path>]
665
+ huly card update <ref> [--title] [--description] [--body] [--body-file]
666
+ huly card delete <ref...> [--yes]
667
+ ```
668
+
669
+ **Master-tag:** cards MUST have a master-tag. The CLI resolves name or
670
+ ID. Use `huly master-tag list` to see available tags. First-card setup
671
+ typically requires using the web UI once to create a master-tag, since
672
+ the CLI doesn't expose master-tag creation.
673
+
674
+ ---
675
+
676
+ ### card-space
677
+
678
+ ```bash
679
+ huly card-space list
680
+ huly card-space get <ref>
681
+ huly card-space create --name "Engineering" [--description] [--private]
682
+ huly card-space delete <ref...> [--yes]
683
+ ```
684
+
685
+ ---
686
+
687
+ ### master-tag
688
+
689
+ ```bash
690
+ huly master-tag list # read-only on CLI
691
+ ```
692
+
693
+ ---
694
+
695
+ ### action (Planner tasks / ToDos)
696
+
697
+ ```bash
698
+ huly action list [--completed all|open|done] [--priority High] [--owner email@...]
699
+ huly action get <ref>
700
+ huly action create --title "..." [--description] [--body] [--body-file]
701
+ [--due 2026-07-01T14:00:00Z] [--priority High]
702
+ [--owner email@...] [--attached-to <ref>] [--attached-to-class <classId>]
703
+ huly action update <ref> [--title] [--description] [--body] [--body-file]
704
+ huly action complete <ref> # sets doneOn=now
705
+ huly action reopen <ref> # clears doneOn
706
+ huly action schedule <ref> # creates a WorkSlot for the task
707
+ huly action unschedule <ref> # removes WorkSlots for the task
708
+ huly action delete <ref...> [--yes]
709
+ ```
710
+
711
+ **`--completed` filter:** `all` (default) shows all, `open` excludes done,
712
+ `done` shows only done.
713
+
714
+ **Priority:** accepts any of `Urgent | High | Normal | Low | None`. Match
715
+ is case-insensitive. Unknown priorities throw NotFound.
716
+
717
+ ---
718
+
719
+ ### document
720
+
721
+ ```bash
722
+ huly document list
723
+ huly document create --title "..." [--body <md>] [--body-file <path>]
724
+ [--teamspace <name>] [--parent <ref>] [--description] [--archived]
725
+ huly document update <ref> [--title] [--body] [--body-file] [--old-text] [--new-text]
726
+ [--replace-all]
727
+ huly document delete <ref...> [--yes]
728
+ huly document snapshots <ref> # list version snapshots
729
+ huly document snapshot <ref> # get a specific snapshot (by ID)
730
+ huly document inline-comments <ref>
731
+ ```
732
+
733
+ **`--body` vs `--old-text/--new-text`:** These are mutually exclusive.
734
+ Full body replace with `--body`; targeted substitution with `--old-text`
735
+ + `--new-text`. The substitution throws if `--old-text` appears 0 times
736
+ (unless `--replace-all`).
737
+
738
+ **Auto-teamspace:** On first document create in a workspace with no
739
+ teamspaces, the CLI auto-creates a default `General` teamspace.
740
+
741
+ ---
742
+
743
+ ### teamspace
744
+
745
+ Document teamspaces.
746
+
747
+ ```bash
748
+ huly teamspace list
749
+ huly teamspace get <ref>
750
+ huly teamspace create --name "Engineering" [--description] [--private]
751
+ huly teamspace delete <ref...> [--yes]
752
+ ```
753
+
754
+ ---
755
+
756
+ ### calendar
757
+
758
+ Calendar events, recurring events, and calendars.
759
+
760
+ ```bash
761
+ huly calendar calendars # list calendars (NOT events)
762
+ huly calendar create-calendar --name "Work" [--description] [--private] [--access owner|team|public]
763
+ huly calendar delete-calendar <ref>
764
+
765
+ huly calendar list # list events
766
+ huly calendar get <eventRef> [--markdown] # events have --markdown body
767
+ huly calendar create --title "..." [--start ISO] [--end ISO] [--attendee email@...]
768
+ [--location] [--all-day] [--description] [--body <md>]
769
+ [--calendar-id <ref>] [--rrule "FREQ=DAILY;COUNT=3"]
770
+ huly calendar update <eventRef> [--title] [--start] [--end] [--attendee]
771
+ huly calendar delete <eventRef...> [--yes]
772
+
773
+ huly calendar recurring # list recurring event definitions
774
+ huly calendar recurring-instances <recRef> # list materialized instances
775
+ ```
776
+
777
+ **Date format:** ISO 8601 with timezone, e.g. `2026-07-01T14:00:00Z`.
778
+ The CLI does not parse natural-language dates — use `date -u -d "..."`
779
+ or similar to generate.
780
+
781
+ **RRULE format:** iCalendar RFC 5545, e.g. `FREQ=DAILY;COUNT=3`,
782
+ `FREQ=WEEKLY;BYDAY=MO,WE,FR`. Use `recurring-instances` to see what got
783
+ materialized.
784
+
785
+ **`calendars` vs `get`:** confusingly, `calendar get <ref>` returns
786
+ EVENTS (not calendars). To fetch a calendar's metadata, use
787
+ `calendar calendars --json` and grep for `_id`.
788
+
789
+ ---
790
+
791
+ ### schedule
792
+
793
+ Calendar schedules (owner availability).
794
+
795
+ ```bash
796
+ huly schedule list
797
+ huly schedule create --owner <userUuid> [--time-zone UTC] [--description]
798
+ [--duration 30] [--interval 30]
799
+ huly schedule update <ref> [...]
800
+ huly schedule delete <ref...> [--yes]
801
+ ```
802
+
803
+ **`--owner`:** UUID of the account that owns the schedule (typically the
804
+ current user). Resolve via `huly user get --json | jq -r '._id'`.
805
+
806
+ ---
807
+
808
+ ### time
809
+
810
+ Time tracking on issues.
811
+
812
+ ```bash
813
+ huly time log --issue TSK-1 --minutes 30 --description "did thing"
814
+ huly time log --issue TSK-1 --hours 2 --description "pair programming"
815
+ huly time report --from 2026-06-01 --to 2026-06-30 [--user email@...] [--project TSK]
816
+ huly time delete <entryRef...> [--yes]
817
+ ```
818
+
819
+ ---
820
+
821
+ ## Common workflows
822
+
823
+ ### Bootstrap a new project
824
+
825
+ ```bash
826
+ # Create the project
827
+ huly project create --name "Q3 Initiative" --identifier Q3I --description "Q3 goals"
828
+
829
+ # Add statuses (web UI recommended — CLI doesn't expose status creation)
830
+ # Add components
831
+ huly component create --project Q3I --label "API"
832
+ huly component create --project Q3I --label "Web"
833
+
834
+ # Add milestones
835
+ huly milestone create --project Q3I --label "v1.0" --due 2026-09-30
836
+
837
+ # Create the first issue
838
+ huly issue create --project Q3I --title "Set up CI pipeline" --priority High \
839
+ --assignee alice@example.com --label backend
840
+ ```
841
+
842
+ ### Bulk-archive old issues
843
+
844
+ ```bash
845
+ huly issue list --status-category Won --limit 1000 --json \
846
+ | jq -r '.[]._id' \
847
+ | xargs -I{} huly issue move {} --parent null --yes
848
+ ```
849
+
850
+ ### Daily activity report
851
+
852
+ ```bash
853
+ # Issues created today
854
+ huly issue list --limit 100 --json | \
855
+ jq -r '.[] | select(.createdOn > (now - 86400) * 1000) | "\(.identifier): \(.title)"'
856
+
857
+ # Time logged today
858
+ huly time report --from $(date -u +%Y-%m-%d) --to $(date -u +%Y-%m-%d)
859
+ ```
860
+
861
+ ### Migration: move issues between projects
862
+
863
+ ```bash
864
+ # Get all issue IDs in old project
865
+ IDS=$(huly issue list --project OLD --json | jq -r '.[]._id')
866
+
867
+ # Move each to new project (cannot bulk — CLI moves one at a time)
868
+ for id in $IDS; do
869
+ huly issue move "$id" --project NEW --yes 2>&1 | head -1
870
+ done
871
+ ```
872
+
873
+ ### Find and fix orphan docs
874
+
875
+ ```bash
876
+ # Documents whose teamspace was deleted
877
+ huly document list --json | \
878
+ jq -r '.[] | select(.space == null) | ._id' \
879
+ | xargs -I{} huly document delete {} --yes
880
+ ```
881
+
882
+ ---
883
+
884
+ ## Output mode reference
885
+
886
+ | Command category | Default | `--json` | `--markdown` |
887
+ |---|---|---|---|
888
+ | `list` | Table | Array of full objects | N/A |
889
+ | `get <ref>` | Table (key fields) | Full object | Body as markdown text |
890
+ | `create` / `update` / `delete` | One-line confirmation | `{ _id, created: bool, ... }` | N/A |
891
+ | `whoami` | Multi-line | Object | N/A |
892
+ | `login` | One-line | One-line | N/A |
893
+
894
+ ### When to use `--json`
895
+
896
+ Use `--json` whenever:
897
+ - You're piping to `jq`, `xargs`, or another tool
898
+ - You're writing a script that needs the `_id` field
899
+ - You want to assert specific fields in CI
900
+ - You want full objects instead of truncated table rows
901
+
902
+ Avoid `--json` when:
903
+ - You're interactively exploring (tables are more readable)
904
+ - You want body content (use `--markdown` instead)
905
+
906
+ ---
907
+
908
+ ## Class ID reference
909
+
910
+ The platform's class hierarchy. Used as `_class` in JSON, as class IDs in
911
+ escape-hatch calls, and as class filters in queries.
912
+
913
+ | Plugin | Class ID pattern | Examples |
914
+ |---|---|---|
915
+ | `core` | `core:class:*` | `Account`, `Space`, `Type`, `Doc`, `Obj` |
916
+ | `contact` | `contact:class:*` | `Person` |
917
+ | `tracker` | `tracker:class:*` | `Project`, `Issue`, `IssueStatus`, `Component`, `Milestone`, `IssueTemplate`, `TimeSpendReport`, `TypeIssuePriority` |
918
+ | `task` | `task:class:*` | `Task` |
919
+ | `board` | `board:class:*` | `Card` |
920
+ | `card` | `card:class:*` | `CardSpace`, `MasterTag` |
921
+ | `calendar` | `calendar:class:*` | `Event`, `ReccuringEvent`, `ReccuringInstance`, `Calendar`, `Schedule` |
922
+ | `document` | `document:class:*` | `Document`, `DocumentSnapshot`, `DocumentEmbedding`, `Teamspace` |
923
+ | `chunter` | `chunter:class:*` | `Channel`, `ChatMessage`, `DirectMessage`, `Message`, `ThreadMessage` |
924
+ | `time` | `time:class:*` | `ToDo`, `WorkSlot` |
925
+ | `notification` | `notification:class:*` | `Notification`, `NotificationContext`, `InboxNotification` (Phase 15 — not yet in CLI) |
926
+ | `activity` | `activity:class:*` | `ActivityMessage`, `Reaction`, `SavedMessage` (Phase 14 — not yet in CLI) |
927
+ | `approval` | `approval:class:*` | `ApprovalRequest`, `Approval` (Phase 16 — not yet in CLI) |
928
+
929
+ The CLI's class IDs are in `src/transport/identifiers.ts`. They're the
930
+ canonical reference for escape-hatch use.
931
+
932
+ ---
933
+
934
+ ## Plugin / model surface map
935
+
936
+ For each plugin, what classes the CLI exposes and which are read-only:
937
+
938
+ | Plugin | CLI surface | Read | Write |
939
+ |---|---|---|---|
940
+ | core | (used internally) | — | — |
941
+ | contact | `user` | `get`, `find` | — |
942
+ | tracker | `project`, `issue`, `component`, `milestone`, `issue-template`, `time` | All | All |
943
+ | task | `action` (alias for `todo`) | All | All |
944
+ | board | `card` | All | All |
945
+ | card | `card-space`, `master-tag` | All | `card-space` only (master-tags are read-only) |
946
+ | calendar | `calendar`, `schedule` | All | All |
947
+ | document | `document`, `teamspace` | All | All |
948
+ | chunter | `channel`, `dm`, `thread` | All | All |
949
+ | time | (used by `time` commands) | — | — |
950
+ | notification | (Phase 15 — not implemented) | — | — |
951
+ | activity | (Phase 14 — not implemented) | — | — |
952
+ | approval | (Phase 16 — not implemented) | — | — |
953
+
954
+ ---
955
+
956
+ ## Escape hatches
957
+
958
+ When a CLI command doesn't exist for what you need, use the raw RPC
959
+ escape hatches. These pass through directly to the server.
960
+
961
+ ### HTTP (`huly api`)
962
+
963
+ ```bash
964
+ huly api GET /api/v1/version
965
+ huly api GET /config.json
966
+ huly api POST /api/v1/something --body '{"key":"value"}'
967
+ huly api GET /api/v1/things --query foo=bar --query baz=qux
968
+ huly api GET /api/v1/things --header "Authorization: Bearer ..."
969
+ ```
970
+
971
+ Available methods: `GET | POST | PUT | PATCH | DELETE`. The path is
972
+ appended to the workspace's API URL.
973
+
974
+ ### WebSocket (`huly ws`)
975
+
976
+ The Huly RPC protocol uses WebSocket. Use the `ws` command for direct
977
+ method calls:
978
+
979
+ ```bash
980
+ # findAll
981
+ huly ws findAll '{"_class":"tracker:class:Project"}' '{}'
982
+
983
+ # findOne
984
+ huly ws findOne '{"_class":"tracker:class:Project"}' '{"identifier":"TSK"}'
985
+
986
+ # createDoc
987
+ huly ws createDoc 'tracker:class:Project' 'core:space:Space' \
988
+ '{"identifier":"NEW","name":"New project"}'
989
+
990
+ # tx (raw transaction)
991
+ haly ws tx '{"_class":"core:class:TxCreateDoc",...}'
992
+ ```
993
+
994
+ Method names mirror the SDK's `PlatformClient` interface. See
995
+ `node_modules/@hcengineering/api-client/lib/client.js` for the full list.
996
+
997
+ ### When to use escape hatches
998
+
999
+ - A command exists but doesn't expose the flag you need (rare)
1000
+ - A command exists but operates on a wrong sub-resource
1001
+ - You're doing batch operations and need to skip validation
1002
+ - You're debugging and need to see the raw server response
1003
+ - The CLI doesn't support the surface you need (use the SDK instead)
1004
+
1005
+ ---
1006
+
1007
+ ## Internal architecture
1008
+
1009
+ ### Layout
1010
+
1011
+ ```
1012
+ src/
1013
+ cli.ts # top-level command registration (1000+ LOC)
1014
+ index.ts # entry point + Node shims (window, localStorage)
1015
+ auth/
1016
+ client.ts # login, accountClient, connectPlatform
1017
+ cache.ts # token cache (credentials.json)
1018
+ env.ts # env var loading
1019
+ resources/
1020
+ _helpers.ts # shared command helpers
1021
+ _project-resolve.ts
1022
+ project.ts # project CRUD
1023
+ issue.ts # issue CRUD + relations + labels + moves
1024
+ component.ts # component CRUD
1025
+ milestone.ts # milestone CRUD
1026
+ issue-template.ts
1027
+ comment.ts
1028
+ channel.ts # channel CRUD + members + messages
1029
+ dm.ts # (in channel.ts)
1030
+ thread.ts # (in channel.ts)
1031
+ card.ts # card module
1032
+ card-space.ts # (in card.ts)
1033
+ master-tag.ts # (in card.ts)
1034
+ action.ts # planner tasks
1035
+ document.ts # documents + teamspaces + snapshots
1036
+ teamspace.ts # (in document.ts)
1037
+ calendar.ts # events + recurring + calendars + schedules
1038
+ schedule.ts # (in calendar.ts)
1039
+ time.ts # time tracking
1040
+ user.ts # profile + person lookup
1041
+ workspace.ts # workspace ops
1042
+ todo.ts # (legacy todo; replaced by action)
1043
+ project.parse.ts # project parsing helpers
1044
+ misc.ts # misc utilities
1045
+ transport/
1046
+ sdk.ts # connectCli, connectAccountCli, resolveWorkspace
1047
+ identifiers.ts # CLASS, CLASS_ICON, ref helpers
1048
+ ref-resolver.ts # ref → Ref<Doc> resolution
1049
+ output/
1050
+ format.ts # table, json, kv, withTimeout
1051
+ progress.ts # withSpinner
1052
+ errors.ts # CliError, ExitCode
1053
+ commands/
1054
+ dry-run.ts # dry-run helpers
1055
+ scripts/
1056
+ smoke.sh # phase-based smoke test (13 phases)
1057
+ docs/
1058
+ HANDOVER.md # session handover
1059
+ issues.md # historical issue inventory
1060
+ learnings.md # detailed learnings
1061
+ open-issues.md # currently-open issues (excluding verified fixes)
1062
+ ```
1063
+
1064
+ ### Connection flow
1065
+
1066
+ 1. `huly --workspace prod issue list`
1067
+ 2. `globalsFrom(cmd)` extracts `--workspace prod` from the parsed Command
1068
+ 3. `connectCli({ workspace: 'prod' })` resolves workspace name → URL/UUID
1069
+ 4. `connectPlatform(...)` reads token from cache, falls back to env login
1070
+ 5. SDK opens WebSocket to transactor, loads model
1071
+ 6. `client.findAll(CLASS.Issue, { ... })` issues server-side query
1072
+ 7. CLI formats result as table/JSON
1073
+
1074
+ ### Markup handling
1075
+
1076
+ The SDK's `processMarkup` calls the collaborator's `uploadMarkup` RPC
1077
+ on every `MarkupContent` instance. This throws on this selfhost because
1078
+ the collaborator's `createMarkup` has a hocuspocus hang.
1079
+
1080
+ **Workaround:** the CLI passes body content as raw strings instead of
1081
+ `new MarkupContent(body, 'markdown')`. The SDK's else branch passes
1082
+ strings through unchanged. The read path (`get --markdown`) falls back
1083
+ to returning the raw body string when markup resolution fails.
1084
+
1085
+ This means `get --markdown` returns the raw body for CLI-created docs
1086
+ (always correct) and the ref string for web-UI-created docs (rare on
1087
+ this selfhost, since only CLI creates docs).
1088
+
1089
+ ---
1090
+
1091
+ ## Environment variables reference
1092
+
1093
+ | Variable | Default | Description |
1094
+ |---|---|---|
1095
+ | `HULY_URL` | (none) | Base URL of your Huly server |
1096
+ | `HULY_EMAIL` | (none) | Account email for password login |
1097
+ | `HULY_PASSWORD` | (none) | Account password for password login |
1098
+ | `HULY_TOKEN` | (none) | Pre-issued account JWT (skips login) |
1099
+ | `HULY_WORKSPACE` | (none) | Default workspace (URL name or UUID) |
1100
+ | `HULY_PROJECT` | (none) | Default project for bare-number issue refs |
1101
+ | `HULY_TEAMSPACE` | (none) | Default teamspace for document creation |
1102
+ | `HULY_NONINTERACTIVE` | 0 | Set to 1 to disable all prompts |
1103
+ | `HULY_LOG` | (none) | Log level: `debug | info | warn | error` |
1104
+ | `HULY_LOCALSTORAGE` | (none) | Path for SDK's localStorage shim (rarely needed) |
1105
+ | `NO_COLOR` | (none) | Set to 1 to disable colored output |
1106
+
1107
+ ---
1108
+
1109
+ ## Troubleshooting
1110
+
1111
+ ### "permission denied to create schema" on account pod startup
1112
+
1113
+ The cockroach `selfhost` user lacks the `CREATE` privilege on `defaultdb`.
1114
+ This happens after `docker compose down -v` (which wipes the volume and
1115
+ recreates the user without privileges).
1116
+
1117
+ **Fix:**
1118
+ ```bash
1119
+ docker exec -e PGPASSWORD=... huly_v7-cockroach-1 \
1120
+ /cockroach/cockroach sql --insecure -d defaultdb -u root \
1121
+ -e "GRANT CREATE ON DATABASE defaultdb TO selfhost"
1122
+ ```
1123
+
1124
+ The password is `CR_USER_PASSWORD` from `~/huly-selfhost/.env`.
1125
+
1126
+ ### "Forbidden" on workspace delete / members / region operations
1127
+
1128
+ The deployed `account:local-fix` image uses MongoDB code paths even
1129
+ though the selfhost runs Postgres. This is a build-artifact mismatch —
1130
+ the deployed bundle has the wrong collection implementation.
1131
+
1132
+ **Fix:** rebuild `account` from the `fix/server-issues-2026-06` branch:
1133
+ ```bash
1134
+ cd ~/platform
1135
+ PATH=/tmp/node22/bin:$PATH ./scripts/docker.sh --tool rush build --to-version-only account
1136
+ docker build -t hardcoreeng/account:local-fix -f docker/images/account/Dockerfile .
1137
+ docker compose -f ~/huly-selfhost/compose.yml up -d --force-recreate account
1138
+ ```
1139
+
1140
+ ### "no IssueStatus in workspace" on issue create
1141
+
1142
+ The workspace's tracker migration didn't seed IssueStatuses. The CLI
1143
+ cannot auto-seed on workspaces with incomplete local model state.
1144
+
1145
+ **Fix:** create statuses via the web UI once, or run the tracker
1146
+ migration manually:
1147
+ ```bash
1148
+ # Manual SQL seed (use with caution)
1149
+ docker exec -u root huly_v7-cockroach-1 /cockroach/cockroach sql \
1150
+ --url 'postgresql://root@127.0.0.1:26257/defaultdb?sslcert=...' \
1151
+ -e "INSERT INTO global_tracker.class_xxx ..."
1152
+ ```
1153
+
1154
+ Or recreate the workspace (the seed runs on workspace creation).
1155
+
1156
+ ### "no document found, failed to apply model transaction" warnings
1157
+
1158
+ These appear in transactor logs on every CLI command. They are the
1159
+ workspace pod's model-upgrade loop retrying update txes whose target
1160
+ class doesn't exist yet. Cosmetic only — does not affect functionality.
1161
+
1162
+ **Tracking:** Fix #3 (model-upgrade retry) helps but doesn't fully clear
1163
+ the warnings. A deeper fix requires N-pass retries or tx re-ordering.
1164
+
1165
+ ### Component/milestone create succeeds but list returns 0
1166
+
1167
+ Sub-resource create→list roundtrip is broken on this selfhost.
1168
+ Tracked as C2 in `docs/open-issues.md`.
1169
+
1170
+ **Workaround:** the doc was created (CLI returned an _id). Just don't
1171
+ rely on the list query. Future CLI versions will track created IDs
1172
+ in a local index instead of relying on server-side findAll.
1173
+
1174
+ ### "Error: connect ECONNREFUSED" on first call after server restart
1175
+
1176
+ The CLI caches connections. After the server restarts, the next call
1177
+ will fail with a connection error. Run any command again — the CLI
1178
+ reconnects transparently.
1179
+
1180
+ ### Token expired errors
1181
+
1182
+ JWTs expire after a server-configured TTL (default ~7 days). When
1183
+ expired, you get `Unauthorized`:
1184
+
1185
+ ```bash
1186
+ rm ~/.config/huly/credentials.json
1187
+ huly login --headless
1188
+ ```
1189
+
1190
+ ### "Cannot add option '--non-interactive' to command 'huly'"
1191
+
1192
+ This error occurs if you have a custom CLI script that re-attaches
1193
+ global options without skipping `--non-interactive`. The fix: pass
1194
+ `{ skipNonInteractive: true }` when attaching to the program command.
1195
+
1196
+ ---
1197
+
1198
+ ## Performance & limits
1199
+
1200
+ ### Connection pooling
1201
+
1202
+ The CLI opens one WebSocket per invocation. There's no keepalive across
1203
+ invocations. Each `huly <cmd>` is a fresh process, so model reload happens
1204
+ every time.
1205
+
1206
+ For long-running scripts that make many CLI calls, prefer inlining via
1207
+ the SDK directly:
1208
+
1209
+ ```js
1210
+ import { connect } from '@hcengineering/api-client'
1211
+ const client = await connect(url, { workspace, token })
1212
+ // ... reuse client
1213
+ await client.close()
1214
+ ```
1215
+
1216
+ ### Query limits
1217
+
1218
+ Default `--limit` is unlimited (server caps at chunked response size).
1219
+ For predictable response sizes:
1220
+ ```bash
1221
+ huly issue list --limit 100
1222
+ ```
1223
+
1224
+ ### Timeouts
1225
+
1226
+ | Operation | Timeout |
1227
+ |---|---|
1228
+ | Login | 30s |
1229
+ | Connection | 30s |
1230
+ | findAll | 60s (then chunked) |
1231
+ | Markup fetch (read) | 5s (with fallback) |
1232
+ | Ping/pong | 30s |
1233
+
1234
+ The CLI never silently hangs. If an operation times out, you get an
1235
+ explicit error.
1236
+
1237
+ ### Bulk operations
1238
+
1239
+ For >1000 docs, prefer chunked scripts:
1240
+
1241
+ ```bash
1242
+ huly issue list --limit 1000 --offset 0 # batch 1
1243
+ huly issue list --limit 1000 --offset 1000 # batch 2
1244
+ ```
1245
+
1246
+ The SDK supports `{limit, total: true}` for accurate counts but the
1247
+ CLI's --limit/--offset doesn't expose it.
1248
+
1249
+ ---
1250
+
1251
+ ## Security model
1252
+
1253
+ ### What the CLI does
1254
+
1255
+ - Loads credentials from env or `~/.config/huly/.env` (mode 0600)
1256
+ - Caches JWTs to `~/.config/huly/credentials.json` (mode 0600)
1257
+ - Connects over TLS to the server (no plaintext HTTP)
1258
+ - Never logs tokens (not even at debug level)
1259
+ - Validates server certs (no self-signed bypass)
1260
+
1261
+ ### What the CLI does NOT do
1262
+
1263
+ - Does NOT handle password rotation (CLI just reads `HULY_PASSWORD`)
1264
+ - Does NOT enforce workspace-level RBAC (the server does)
1265
+ - Does NOT store secrets in source control (use `.env` outside git)
1266
+ - Does NOT support OAuth or SSO (password login only)
1267
+ - Does NOT support TOTP / 2FA login (server-side only)
1268
+
1269
+ ### Credential storage recommendations
1270
+
1271
+ For personal use: the defaults (mode 0600) are fine.
1272
+
1273
+ For shared CI runners: use `HULY_TOKEN` with a service-account JWT, never
1274
+ embed passwords. Set short TTLs on the token.
1275
+
1276
+ For production automation: consider a secrets manager (Vault, AWS
1277
+ Secrets Manager, etc.) that injects env vars at runtime.
1278
+
1279
+ ### Threat model
1280
+
1281
+ The CLI assumes:
1282
+ - The server is trusted (run it on your own infrastructure)
1283
+ - The local filesystem is trusted (no other users can read ~/.config/huly/)
1284
+ - The shell environment is trusted (env vars may be logged by parent processes)
1285
+
1286
+ If any of these don't hold, the CLI's threat model is violated.
1287
+
1288
+ ---
1289
+
1290
+ ## Compatibility matrix
1291
+
1292
+ ### Server versions tested
1293
+
1294
+ | Huly version | Status | Notes |
1295
+ |---|---|---|
1296
+ | 0.7.423 (local-fix images) | ✅ Fully tested | All 13 implemented smoke phases pass |
1297
+ | 0.7.422 | ⚠️ Mostly works | MODEL_VERSION mismatch on workspace pod |
1298
+ | 0.7.421 and earlier | ❌ Not tested | API may have changed |
1299
+
1300
+ ### Node versions tested
1301
+
1302
+ | Node | Status |
1303
+ |---|---|
1304
+ | 22.11 | ✅ Recommended |
1305
+ | 24.x | ✅ Works |
1306
+ | 26.x | ❌ Fails rush build check |
1307
+ | 20.x | ❌ Missing crypto features |
1308
+
1309
+ ### OS tested
1310
+
1311
+ - Linux (Ubuntu 22.04, Debian 12) — primary development platform
1312
+ - macOS (14.x Apple Silicon) — secondary; works
1313
+ - Windows (10, 11 with WSL2) — works via WSL
1314
+ - Native Windows — untested; use WSL
1315
+
1316
+ ---
1317
+
1318
+ ## Development
1319
+
1320
+ ### Setup
1321
+
1322
+ ```bash
1323
+ git clone https://github.com/your-org/huly-cli.git
1324
+ cd huly-cli
1325
+ npm install
1326
+ npm run build # compile TS → dist/
1327
+ npm run dev # watch mode (tsc --watch)
1328
+ node dist/index.js # run CLI
1329
+ ```
1330
+
1331
+ ### Run the smoke test
1332
+
1333
+ ```bash
1334
+ # All phases
1335
+ bash scripts/smoke.sh all
1336
+
1337
+ # One phase
1338
+ bash scripts/smoke.sh 6
1339
+
1340
+ # With debug output
1341
+ DEBUG=1 bash scripts/smoke.sh 0
1342
+ ```
1343
+
1344
+ ### Project conventions
1345
+
1346
+ - TypeScript strict mode (no `any` except at API boundaries)
1347
+ - camelCase functions, PascalCase classes, SCREAMING_SNAKE constants
1348
+ - One resource per file in `src/resources/`
1349
+ - New class IDs go in `src/transport/identifiers.ts`
1350
+ - Each new command must have a corresponding smoke test
1351
+ - Help text MUST describe each flag, even if obvious
1352
+ - Errors throw `CliError(ExitCode.X, msg, hint?)` — never raw `Error`
1353
+
1354
+ ### Adding a new command
1355
+
1356
+ 1. Add the resource function in `src/resources/<surface>.ts`
1357
+ 2. Add the class ID to `src/transport/identifiers.ts`
1358
+ 3. Wire the command in `src/cli.ts` (find the relevant `program.command(...)`)
1359
+ 4. Add a smoke test case in `scripts/smoke.sh`
1360
+ 5. Update `README.md` with the new command
1361
+ 6. Run `npm run build && bash scripts/smoke.sh all`
1362
+
1363
+ ### Adding a new resource (e.g. Phase 11's `space`)
1364
+
1365
+ 1. Create `src/resources/space.ts`
1366
+ 2. Add class IDs to `src/transport/identifiers.ts`
1367
+ 3. Wire 10+ subcommands in `src/cli.ts`
1368
+ 4. Add a phase to `scripts/smoke.sh` (increment the phase number)
1369
+ 5. Update `README.md` with the new resource section
1370
+ 6. Run `bash scripts/smoke.sh all`
1371
+
1372
+ ---
1373
+
1374
+ ## Cross-references
1375
+
1376
+ - `docs/HANDOVER.md` — what to read first when resuming work
1377
+ - `docs/learnings.md` — detailed server architecture and gotchas
1378
+ - `docs/issues.md` — historical bug inventory (2026-06-27)
1379
+ - `docs/open-issues.md` — current open issues (excludes verified fixes)
1380
+
1381
+ ---
1382
+
1383
+ ## License
1384
+
1385
+ Eclipse Public License 2.0 (matching the upstream platform).
1386
+
1387
+ ```
1388
+ This program and the accompanying materials are made available under the
1389
+ terms of the Eclipse Public License 2.0 which is available at
1390
+ https://www.eclipse.org/legal/epl-2.0/
1391
+ ```
1392
+ ---
1393
+
1394
+ ## Server architecture (deep dive)
1395
+
1396
+ This section explains how the CLI interacts with the Huly server. Useful
1397
+ for debugging, performance tuning, and writing automation.
1398
+
1399
+ ### Service map
1400
+
1401
+ The selfhost has ~16 services. The CLI talks to four of them:
1402
+
1403
+ | Service | What the CLI does with it |
1404
+ |---|---|
1405
+ | `account` (port 3000) | Login, workspace ops, account token management |
1406
+ | `transactor` (port 3333) | WebSocket RPC: findAll, findOne, createDoc, updateDoc, tx, loadModel |
1407
+ | `collaborator` (port 3078) | Read path only: fetchMarkup, getContent. The CLI's read timeout (5s) covers this. |
1408
+ | `nginx` (port 80, behind caddy on 443) | Reverse proxies the above. TLS terminator is caddy on the host. |
1409
+
1410
+ The CLI never talks to `workspace`, `kvs`, `minio`, `redpanda`, `elastic`,
1411
+ `cockroach`, or `front` directly. Those are server-internal.
1412
+
1413
+ ### Database layout (cockroach)
1414
+
1415
+ CockroachDB holds everything. Two schemas per workspace:
1416
+
1417
+ **`defaultdb` (the account DB)** — global across the cluster:
1418
+ - `global_account.workspace` — uuid, name, dataId (the workspace's DB name)
1419
+ - `global_account.workspace_status` — mode, is_disabled, processing_attempts, version_*
1420
+ - `global_account.workspace_members` — (account_uuid, workspace_uuid, role)
1421
+ - `global_account.account`, `global_account.person`, `global_account.social_id`
1422
+ - `global_account.region`, `global_account.invite`, etc.
1423
+
1424
+ **Per-workspace DB** (named after `workspace.dataId`):
1425
+ - `public.tx` — the transaction log (every CUD as TxCreateDoc/Update/Remove)
1426
+ - `public.tracker` — Project, Issue, Component, Milestone, IssueStatus, etc.
1427
+ - `public.document` — Document, DocumentSnapshot
1428
+ - `public.calendar` — Calendar, Event, Schedule
1429
+ - `public.chunter` — Channel, ChatMessage
1430
+ - `public.time` — ToDo, WorkSlot
1431
+ - `public.card` — Card, CardSpace, MasterTag
1432
+ - `public.contact` — Person
1433
+ - `public.config` — workspace config
1434
+
1435
+ **To inspect a workspace's data directly:**
1436
+ ```bash
1437
+ docker exec -e PGPASSWORD=$CR_USER_PASSWORD huly_v7-cockroach-1 \
1438
+ /cockroach/cockroach sql --insecure -d defaultdb -u selfhost \
1439
+ -e "SELECT * FROM global_account.workspace_members LIMIT 5"
1440
+ ```
1441
+
1442
+ Use cockroach root (cert-based) for full access:
1443
+ ```bash
1444
+ docker exec -u root huly_v7-cockroach-1 /cockroach/cockroach sql \
1445
+ --url 'postgresql://root@127.0.0.1:26257/defaultdb?sslcert=certs/client.root.crt&sslkey=certs/client.root.key&sslmode=verify-full&sslrootcert=certs/ca.crt'
1446
+ ```
1447
+
1448
+ ### The model — class hierarchy and domain model
1449
+
1450
+ The Huly "model" is the sum of all classes registered in the workspace.
1451
+ Classes are organized into **plugins** (tracker, calendar, chunter, ...).
1452
+ Each class has a domain (storage bucket):
1453
+
1454
+ - `tracker` (DOMAIN_TRACKER): Project, Issue, Component, Milestone, IssueStatus, IssueTemplate, TypeIssuePriority, TimeSpendReport, RelatedIssueTarget
1455
+ - `calendar` (DOMAIN_CALENDAR): Calendar, Event, ReccuringEvent, ReccuringInstance, Schedule
1456
+ - `document` (DOMAIN_DOCUMENT): Document, DocumentSnapshot, DocumentEmbedding, Teamspace
1457
+ - `chunter` (DOMAIN_CHUNTER): Channel, ChatMessage, DirectMessage, Message, ThreadMessage
1458
+ - `time` (DOMAIN_TIME): ToDo, WorkSlot
1459
+ - `card` (DOMAIN_CARD): Card, CardSpace, MasterTag
1460
+ - `core` (DOMAIN_MODEL): Type, Status, ArrOf, EmbValue, and all base classes
1461
+ - `contact` (DOMAIN_CONTACT): Person
1462
+
1463
+ The model's `findAll` behavior depends on the class's domain:
1464
+ - DOMAIN_MODEL classes: query the local `ModelDb` (in-memory index)
1465
+ - All other domains: query the server (via WebSocket)
1466
+
1467
+ **The CLI's local model is incomplete** (3-key stub). This means queries
1468
+ against DOMAIN_MODEL classes (TypeIssuePriority, etc.) often return
1469
+ empty even though the data exists on the server. See the `conn.findAll`
1470
+ bypass in `src/resources/issue.ts`.
1471
+
1472
+ ### Workspace lifecycle
1473
+
1474
+ A workspace goes through these states (mode column):
1475
+
1476
+ ```
1477
+ [created] → pending-creation → creating → active
1478
+ [upgraded] → pending-upgrade → upgrading → active
1479
+ [deleted by owner] → pending-deletion → deleting → [gone]
1480
+ [archived] → archiving-pending-backup → archiving-backup → archiving-pending-clean
1481
+ → archiving-clean → archived
1482
+ [migrated] → migration-pending-backup → migration-backup → migration-pending-cleanup → [deleted]
1483
+ [restored] → pending-restore → restoring → active
1484
+ ```
1485
+
1486
+ The workspace pod polls for pending workspaces and processes them.
1487
+ `WS_OPERATION` env var controls which states the pod handles:
1488
+
1489
+ | WS_OPERATION value | Processes |
1490
+ |---|---|
1491
+ | `upgrade` (default) | only `pending-upgrade` |
1492
+ | `all` (after Fix #5) | `pending-creation` + `pending-upgrade` + `pending-deletion` |
1493
+ | `all+backup` | all of `all` + `migration-pending-*` + `archiving-pending-*` + `pending-restore` |
1494
+
1495
+ For self-hosted single-pod deployments, use `WS_OPERATION=all+backup`.
1496
+
1497
+ ### The WebSocket protocol
1498
+
1499
+ The CLI speaks Huly's binary RPC protocol over WebSocket. Key methods:
1500
+
1501
+ | Method | Direction | Purpose |
1502
+ |---|---|---|
1503
+ | `hello` | client → server | First message; identifies client (binary mode, compression) |
1504
+ | `findAll` | client → server | Query; server returns array + total |
1505
+ | `findOne` | client → server | Single-doc query |
1506
+ | `loadModel` | client → server | Initial model load (returns txs since last hash) |
1507
+ | `loadChunk` | client → server | Lazy-load a domain's documents |
1508
+ | `tx` | client → server | Apply a transaction |
1509
+ | `updateFromRemote` | server → client | Push a tx (server-initiated) |
1510
+ | `ping` / `pong` | both | Keepalive |
1511
+
1512
+ Chunks are how the server streams large query results. The default chunk
1513
+ size is whatever fits in a WebSocket frame (~64KB compressed).
1514
+
1515
+ ### Transaction model
1516
+
1517
+ Every write in Huly is a transaction (tx). A tx is one of:
1518
+ - `TxCreateDoc` — new document
1519
+ - `TxUpdateDoc` — update document fields
1520
+ - `TxRemoveDoc` — delete document
1521
+ - `TxMixin` — attach/update a mixin
1522
+ - `TxApplyIf` — atomic tx group (commit-on-condition)
1523
+
1524
+ The CLI generates these via the SDK's `client.createDoc`, `client.updateDoc`,
1525
+ etc. Each tx has:
1526
+ - `_id` — tx UUID (generated client-side)
1527
+ - `_class` — tx type class
1528
+ - `space` — where the tx lives (`core:space:Tx`)
1529
+ - `objectId` — the document being created/updated
1530
+ - `objectClass` — the doc's class
1531
+ - `objectSpace` — the doc's space
1532
+ - `modifiedBy`, `modifiedOn` — actor + timestamp
1533
+ - `attributes` — the create/update payload
1534
+
1535
+ The server applies txs in order, checking model consistency. A tx can be
1536
+ rejected if:
1537
+ - The `objectClass` doesn't exist in the model
1538
+ - A referenced object doesn't exist
1539
+ - The user lacks permission
1540
+ - The doc was deleted concurrently
1541
+
1542
+ Rejected txs surface as PlatformError. The CLI surfaces these as CliError.
1543
+
1544
+ ### Markup and y-docs
1545
+
1546
+ For content-bearing fields (description, body, content), the platform uses
1547
+ a markup reference indirection:
1548
+
1549
+ 1. CLI sends `MarkupContent { content: 'markdown text', kind: 'markdown' }`
1550
+ 2. SDK's `processMarkup` calls `client.uploadMarkup(class, id, attr, text, kind)`
1551
+ 3. Collaborator creates a y-doc with the markdown text
1552
+ 4. The doc's data field stores a `MarkupRef { content: 'blobId' }` instead of the text
1553
+ 5. On read, `client.fetchMarkup(...)` retrieves and renders the y-doc
1554
+
1555
+ **Failure mode on this selfhost:** the collaborator's `createMarkup` RPC
1556
+ hangs (hocuspocus connection timeout). Fix #2 wraps `getContent` in a
1557
+ 3s timeout; the corresponding `createMarkup` fix is **not yet deployed**.
1558
+ The CLI works around by passing raw strings instead of `MarkupContent`.
1559
+
1560
+ This means:
1561
+ - Write path: body stored as plain string (not a ref) ✓
1562
+ - Read path: returns raw string for CLI-created docs ✓
1563
+ - Read path: returns ref string (not rendered text) for web-UI-created docs ⚠
1564
+
1565
+ For this selfhost, only CLI creates docs, so the read path is consistent.
1566
+
1567
+ ### Account-server permission model
1568
+
1569
+ The account server gates every method by token type:
1570
+
1571
+ | Token type | `extra.service` | Granted methods |
1572
+ |---|---|---|
1573
+ | Login token (password / OAuth) | undefined | User-level methods only: login, selectWorkspace, listWorkspaces, findPersonBySocialKey (after Fix #1), getWorkspaceInfo, getSocialIds, etc. |
1574
+ | Service token | `'tool' \| 'workspace' \| 'aibot' \| 'backup' \| 'payment' \| ...` | Service-level methods: getPendingWorkspace, updateWorkspaceInfo, etc. |
1575
+ | Admin token | `admin === 'true'` | All methods |
1576
+
1577
+ The CLI uses login tokens. Service-to-service calls (e.g. the worker
1578
+ calling `getPendingWorkspace`) use service tokens.
1579
+
1580
+ **Common pitfall:** calling a service-only method with a login token
1581
+ returns Forbidden. Always use the right token type.
1582
+
1583
+ ### The model-upgrade queue
1584
+
1585
+ When a workspace's `version_major/minor/patch` is less than the server's
1586
+ current version, the workspace pod applies model-upgrade txs:
1587
+
1588
+ 1. Pod calls `getPendingWorkspace(this.region, this.version, 'upgrade')`
1589
+ 2. Account server returns workspaces where `version_* < current`
1590
+ 3. Pod loads the model-upgrade txs from the platform's source tree
1591
+ 4. Pod applies them in order
1592
+ 5. Pod calls `updateWorkspaceInfo(workspace, 'upgrade-done', version)`
1593
+ 6. Workspace's `version_*` is bumped, status becomes `active`
1594
+
1595
+ The model-upgrade txs are auto-generated from the platform's `@Model(...)`
1596
+ decorators in `~/platform/models/<m>/src/`. Each plugin contributes a
1597
+ batch of class-creation txs.
1598
+
1599
+ **Known issue:** if the model-upgrade tx batch has internal dependencies
1600
+ (e.g. an update tx that references a class created by a later tx), the
1601
+ server applies them in the wrong order and skips update txs whose target
1602
+ class doesn't exist yet. Fix #3 (1-pass retry) helps but doesn't fully
1603
+ resolve the issue.
1604
+
1605
+ **Symptom:** transactor logs show:
1606
+ ```
1607
+ no document found, failed to apply model transaction, skipping _class="core:class:TxUpdateDoc"
1608
+ ```
1609
+
1610
+ This is cosmetic — doesn't affect runtime behavior. The skipped txs are
1611
+ typically for older class versions that no longer matter.
1612
+
1613
+ ### The `dataId` quirk
1614
+
1615
+ When you `createWorkspace`, the server assigns a `dataId` (a cockroach
1616
+ DB name). All subsequent docs for this workspace go into that DB.
1617
+
1618
+ **Bug:** if kafka replays a `workspace-deleted` event for a workspace
1619
+ that was already hard-deleted (e.g. via direct SQL), the worker re-creates
1620
+ the workspace row **without** a `dataId`. Subsequent operations on this
1621
+ workspace fail because there's no DB to write to.
1622
+
1623
+ **Workaround:** if you hard-delete via SQL, also delete the kafka
1624
+ events for that workspace. Or just leave the workspace in
1625
+ `pending-deletion` mode and let the worker process it eventually.
1626
+
1627
+ ### Backup strategy
1628
+
1629
+ Backups are stored in MinIO bucket `huly-backups`. The CLI/server doesn't
1630
+ configure MinIO lifecycle, so backups accumulate forever unless you set
1631
+ up ILM externally:
1632
+
1633
+ ```bash
1634
+ docker exec huly_v7-minio-1 mc alias set local http://localhost:9000 minioadmin minioadmin
1635
+ docker exec huly_v7-minio-1 mc mb --ignore-existing local/huly-backups
1636
+ docker exec huly_v7-minio-1 mc ilm add local/huly-backups --expiry-days 14
1637
+ ```
1638
+
1639
+ This sets 14-day expiry on all backups. Adjust as needed for compliance.
1640
+
1641
+ ### Redpanda SASL bootstrap
1642
+
1643
+ The kafka broker (Redpanda) requires SASL auth. During initial bootstrap,
1644
+ `rpk cluster info -X user=admin -X pass=...` returns `ILLEGAL_SASL_STATE`
1645
+ because SASL isn't ready yet.
1646
+
1647
+ **Fix:** use an unauthenticated metadata probe:
1648
+ ```yaml
1649
+ healthcheck:
1650
+ test: ['CMD-SHELL', 'rpk cluster info --brokers=localhost:9092 || exit 1']
1651
+ interval: 10s
1652
+ timeout: 5s
1653
+ retries: 20
1654
+ start_period: 30s
1655
+ ```
1656
+
1657
+ Then set `depends_on: { redpanda: { condition: service_healthy } }` on
1658
+ every kafka-dependent service.
1659
+
1660
+ ### Workspace version sync
1661
+
1662
+ The transactor and workspace pod must be at the **same MODEL_VERSION**
1663
+ (derived from `~/platform/common/scripts/version.txt`). If they drift,
1664
+ the transactor's `sessionManager` rejects WebSocket connections:
1665
+
1666
+ ```
1667
+ version mismatch: transactor 0.7.422 != workspace 0.7.423
1668
+ ```
1669
+
1670
+ **Fix:** keep `~/platform/common/scripts/version.txt` in sync across
1671
+ builds. After bumping, rebuild all pods that consume the version.
1672
+
1673
+ The CLI reads the version from `bundle.js` (the SDK). The server's
1674
+ `hello` response includes `serverVersion`. The CLI logs `Connected to
1675
+ server: <version>` on connect.
1676
+
1677
+
1678
+ ---
1679
+
1680
+ ## Migration guides
1681
+
1682
+ ### Migrating from `huly-mcp` (the MCP server)
1683
+
1684
+ If you're using the MCP server (`huly-mcp`) and want to switch to `huly-cli`:
1685
+
1686
+ **Same operations, different invocation:**
1687
+ ```bash
1688
+ # MCP: list_issues
1689
+ # CLI:
1690
+ huly issue list --json
1691
+
1692
+ # MCP: create_issue
1693
+ # CLI:
1694
+ huly issue create --project TSK --title "..." --json
1695
+
1696
+ # MCP: get_issue
1697
+ # CLI:
1698
+ huly issue get TSK-1 --json
1699
+ ```
1700
+
1701
+ **Output format:** both produce JSON arrays. The MCP server wraps
1702
+ responses in `{ result: [...] }`; the CLI returns raw `[...]`. Strip the
1703
+ wrapper if you're reusing MCP client code.
1704
+
1705
+ **Auth:** both use the same `account-token` JWT. You can reuse the
1706
+ MCP server's credentials cache by symlinking it:
1707
+ ```bash
1708
+ ln -s ~/.config/huly-mcp/credentials.json ~/.config/huly/credentials.json
1709
+ ```
1710
+
1711
+ **Tool naming:** MCP uses `snake_case` (e.g. `list_issues`); CLI uses
1712
+ `kebab-case` (e.g. `issue list`). The MCP names map to CLI as:
1713
+ - `list_<resources>` → `<resource> list`
1714
+ - `get_<resource>` → `<resource> get`
1715
+ - `create_<resource>` → `<resource> create`
1716
+ - `update_<resource>` → `<resource> update`
1717
+ - `delete_<resource>` → `<resource> delete`
1718
+ - `<verb>_<resource>` (e.g. `add_comment`) → `<resource> <verb>`
1719
+
1720
+ ### Migrating from the web UI
1721
+
1722
+ If you're used to clicking around in the web UI:
1723
+
1724
+ | Web UI action | CLI command |
1725
+ |---|---|
1726
+ | Click project in sidebar | `huly workspace use <name>` then `huly project list` |
1727
+ | Open issue TSK-1 | `huly issue get TSK-1 --markdown` |
1728
+ | Create new issue | `huly issue create --project TSK --title "..."` |
1729
+ | Move issue to "Done" | `huly issue update TSK-1 --status Done` |
1730
+ | Add label "bug" | `huly issue label TSK-1 add bug` |
1731
+ | Comment on issue | `huly comment add --issue TSK-1 --body "..."` |
1732
+ | Send DM | `huly dm send --person alice@... --body "..."` |
1733
+ | Create channel | `huly channel create --name engineering` |
1734
+ | Create calendar event | `huly calendar create --title "Standup" --start ... --end ...` |
1735
+ | Log time | `huly time log --issue TSK-1 --minutes 30` |
1736
+ | Switch workspace | `huly workspace use <name>` |
1737
+
1738
+ ### Migrating from the Huly SDK (TypeScript)
1739
+
1740
+ If you have scripts using the SDK directly:
1741
+
1742
+ ```ts
1743
+ // SDK
1744
+ import { connect } from '@hcengineering/api-client'
1745
+ const client = await connect(url, { workspace, token })
1746
+ const issues = await client.findAll('tracker:class:Issue', { space: project._id })
1747
+ ```
1748
+
1749
+ ```bash
1750
+ # CLI equivalent (in shell)
1751
+ huly --workspace $WORKSPACE issue list --project $PROJECT --json
1752
+ ```
1753
+
1754
+ The CLI wraps the SDK and handles auth, caching, model loading, and
1755
+ error formatting. Prefer the CLI for one-off scripts; prefer the SDK
1756
+ for long-running services.
1757
+
1758
+ ### Migrating from the REST API
1759
+
1760
+ If you're using `curl` against the Huly REST API:
1761
+
1762
+ ```bash
1763
+ # REST (raw)
1764
+ curl -X GET "$HULY_URL/api/v1/version"
1765
+
1766
+ # CLI
1767
+ huly api GET /api/v1/version
1768
+ ```
1769
+
1770
+ The CLI's `api` command passes through to the REST API but handles auth
1771
+ headers automatically. Use it for ad-hoc endpoints the CLI doesn't cover.
1772
+
1773
+ ### Migrating from the GraphQL API
1774
+
1775
+ Huly doesn't ship a GraphQL API. The CLI is the closest equivalent — it
1776
+ wraps the platform's RPCs into REST-like commands. If you need GraphQL,
1777
+ you're out of luck.
1778
+
1779
+ ---
1780
+
1781
+ ## Recipes
1782
+
1783
+ ### Recipe: CI integration
1784
+
1785
+ ```yaml
1786
+ # .github/workflows/huly-sync.yml
1787
+ name: Sync CI status to Huly
1788
+ on: [push]
1789
+ jobs:
1790
+ sync:
1791
+ runs-on: ubuntu-latest
1792
+ steps:
1793
+ - uses: actions/checkout@v4
1794
+ - run: npm install -g huly-cli
1795
+ - name: Sync status to Huly
1796
+ env:
1797
+ HULY_URL: ${{ secrets.HULY_URL }}
1798
+ HULY_TOKEN: ${{ secrets.HULY_TOKEN }}
1799
+ HULY_WORKSPACE: ${{ vars.HULY_WORKSPACE }}
1800
+ run: |
1801
+ COMMIT_MSG=$(git log -1 --pretty=%B)
1802
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
1803
+ huly issue create --project CI --title "$BRANCH: $COMMIT_MSG" \
1804
+ --label auto --label ci --yes
1805
+ ```
1806
+
1807
+ ### Recipe: Daily standup bot
1808
+
1809
+ ```bash
1810
+ #!/bin/bash
1811
+ # standup.sh — runs daily, posts to #standup channel
1812
+ set -e
1813
+
1814
+ # Get yesterday's issues you closed
1815
+ CLOSED=$(huly issue list --json | \
1816
+ jq -r --arg date "$(date -u -d 'yesterday' +%Y-%m-%d)" \
1817
+ '.[] | select(.modifiedOn > ($date | strptime("%Y-%m-%d") | mktime * 1000)) | select(.status == "Done") | "- #\(.identifier) \(.title)"')
1818
+
1819
+ # Post to channel
1820
+ huly channel message create "standup" --body "Yesterday I closed:
1821
+ $CLOSED
1822
+ "
1823
+ ```
1824
+
1825
+ ### Recipe: Bulk-migrate issues
1826
+
1827
+ ```bash
1828
+ #!/bin/bash
1829
+ # migrate-issues.sh — copy issues from one project to another
1830
+ set -e
1831
+
1832
+ SOURCE=$1
1833
+ DEST=$2
1834
+ STATUS="open"
1835
+
1836
+ IDS=$(huly issue list --project "$SOURCE" --status-category "$STATUS" --json | jq -r '.[]._id')
1837
+
1838
+ for id in $IDS; do
1839
+ # Get full issue
1840
+ issue=$(huly issue get "$id" --json)
1841
+ title=$(echo "$issue" | jq -r .title)
1842
+ desc=$(echo "$issue" | jq -r .description)
1843
+
1844
+ # Create in dest
1845
+ huly issue create --project "$DEST" --title "$title" --description "$desc" --yes
1846
+
1847
+ echo "migrated: $id ($title)"
1848
+ done
1849
+ ```
1850
+
1851
+ ### Recipe: Weekly digest email
1852
+
1853
+ ```bash
1854
+ #!/bin/bash
1855
+ # weekly-digest.sh
1856
+ set -e
1857
+
1858
+ WEEK_AGO=$(date -u -d '7 days ago' +%Y-%m-%d)
1859
+
1860
+ # Issues created this week
1861
+ NEW=$(huly issue list --since "$WEEK_AGO" --json | jq -r '.[] | "- #\(.identifier) \(.title) (\(.assignee // "unassigned"))"')
1862
+
1863
+ # Issues closed this week
1864
+ CLOSED=$(huly issue list --status Done --since "$WEEK_AGO" --json | jq -r '.[] | "- #\(.identifier) \(.title)"')
1865
+
1866
+ # Send via your mailer (here we use sendmail)
1867
+ {
1868
+ echo "Subject: Huly Weekly Digest"
1869
+ echo
1870
+ echo "This week:"
1871
+ echo "$NEW"
1872
+ echo
1873
+ echo "Closed:"
1874
+ echo "$CLOSED"
1875
+ } | sendmail -t
1876
+ ```
1877
+
1878
+ ### Recipe: Audit orphan documents
1879
+
1880
+ ```bash
1881
+ # Find documents with no teamspace
1882
+ huly document list --json | jq -r '.[] | select(.space == null) | ._id' | \
1883
+ xargs -I{} echo "orphan doc: {}"
1884
+
1885
+ # Find documents with no author
1886
+ huly document list --json | jq -r '.[] | select(.createdBy == null) | ._id' | \
1887
+ xargs -I{} echo "no-author doc: {}"
1888
+ ```
1889
+
1890
+ ### Recipe: Backup via cron
1891
+
1892
+ ```cron
1893
+ # /etc/cron.d/huly-backup
1894
+ 0 2 * * * huly user get > /dev/null && echo "workspace OK at $(date)" >> /var/log/huly-health.log
1895
+ ```
1896
+
1897
+ Or use the Huly server's own backup mechanism (see "Backup strategy" above).
1898
+
1899
+ ### Recipe: Generate report for management
1900
+
1901
+ ```bash
1902
+ #!/bin/bash
1903
+ # management-report.sh
1904
+ set -e
1905
+
1906
+ cat <<EOF
1907
+ Weekly Status Report — $(date +%Y-%m-%d)
1908
+
1909
+ Open issues: $(huly issue list --status-category Active --json | jq length)
1910
+ Closed this week: $(huly issue list --status Done --since "$(date -u -d '7 days ago' +%Y-%m-%d)" --json | jq length)
1911
+
1912
+ Top contributors:
1913
+ $(huly issue list --since "$(date -u -d '7 days ago' +%Y-%m-%d)" --json | \
1914
+ jq -r '.[].assignee' | sort | uniq -c | sort -rn | head -5)
1915
+
1916
+ ---
1917
+
1918
+ ## Migration guides
1919
+
1920
+ ### Migrating from `huly-mcp` (the MCP server)
1921
+
1922
+ If you're using the MCP server (`huly-mcp`) and want to switch to `huly-cli`:
1923
+
1924
+ **Same operations, different invocation:**
1925
+ ```bash
1926
+ # MCP: list_issues
1927
+ # CLI:
1928
+ huly issue list --json
1929
+
1930
+ # MCP: create_issue
1931
+ # CLI:
1932
+ huly issue create --project TSK --title "..." --json
1933
+
1934
+ # MCP: get_issue
1935
+ # CLI:
1936
+ huly issue get TSK-1 --json
1937
+ ```
1938
+
1939
+ **Output format:** both produce JSON arrays. The MCP server wraps
1940
+ responses in `{ result: [...] }`; the CLI returns raw `[...]`. Strip the
1941
+ wrapper if you're reusing MCP client code.
1942
+
1943
+ **Auth:** both use the same `account-token` JWT. You can reuse the
1944
+ MCP server's credentials cache by symlinking it:
1945
+ ```bash
1946
+ ln -s ~/.config/huly-mcp/credentials.json ~/.config/huly/credentials.json
1947
+ ```
1948
+
1949
+ **Tool naming:** MCP uses `snake_case` (e.g. `list_issues`); CLI uses
1950
+ `kebab-case` (e.g. `issue list`). The MCP names map to CLI as:
1951
+ - `list_<resources>` becomes `<resource> list`
1952
+ - `get_<resource>` becomes `<resource> get`
1953
+ - `create_<resource>` becomes `<resource> create`
1954
+ - `update_<resource>` becomes `<resource> update`
1955
+ - `delete_<resource>` becomes `<resource> delete`
1956
+ - `<verb>_<resource>` (e.g. `add_comment`) becomes `<resource> <verb>`
1957
+
1958
+ ### Migrating from the web UI
1959
+
1960
+ If you're used to clicking around in the web UI:
1961
+
1962
+ | Web UI action | CLI command |
1963
+ |---|---|
1964
+ | Click project in sidebar | `huly workspace use <name>` then `huly project list` |
1965
+ | Open issue TSK-1 | `huly issue get TSK-1 --markdown` |
1966
+ | Create new issue | `huly issue create --project TSK --title "..."` |
1967
+ | Move issue to "Done" | `huly issue update TSK-1 --status Done` |
1968
+ | Add label "bug" | `huly issue label TSK-1 add bug` |
1969
+ | Comment on issue | `huly comment add --issue TSK-1 --body "..."` |
1970
+ | Send DM | `huly dm send --person alice@... --body "..."` |
1971
+ | Create channel | `huly channel create --name engineering` |
1972
+ | Create calendar event | `huly calendar create --title "Standup" --start ... --end ...` |
1973
+ | Log time | `huly time log --issue TSK-1 --minutes 30` |
1974
+ | Switch workspace | `huly workspace use <name>` |
1975
+
1976
+ ### Migrating from the Huly SDK (TypeScript)
1977
+
1978
+ If you have scripts using the SDK directly:
1979
+
1980
+ ```ts
1981
+ // SDK
1982
+ import { connect } from '@hcengineering/api-client'
1983
+ const client = await connect(url, { workspace, token })
1984
+ const issues = await client.findAll('tracker:class:Issue', { space: project._id })
1985
+ ```
1986
+
1987
+ ```bash
1988
+ # CLI equivalent (in shell)
1989
+ huly --workspace $WORKSPACE issue list --project $PROJECT --json
1990
+ ```
1991
+
1992
+ The CLI wraps the SDK and handles auth, caching, model loading, and
1993
+ error formatting. Prefer the CLI for one-off scripts; prefer the SDK
1994
+ for long-running services.
1995
+
1996
+ ### Migrating from the REST API
1997
+
1998
+ If you're using `curl` against the Huly REST API:
1999
+
2000
+ ```bash
2001
+ # REST (raw)
2002
+ curl -X GET "$HULY_URL/api/v1/version"
2003
+
2004
+ # CLI
2005
+ huly api GET /api/v1/version
2006
+ ```
2007
+
2008
+ The CLI's `api` command passes through to the REST API but handles auth
2009
+ headers automatically. Use it for ad-hoc endpoints the CLI doesn't cover.
2010
+
2011
+ ### Migrating from the GraphQL API
2012
+
2013
+ Huly doesn't ship a GraphQL API. The CLI is the closest equivalent — it
2014
+ wraps the platform's RPCs into REST-like commands. If you need GraphQL,
2015
+ you're out of luck.
2016
+
2017
+ ---
2018
+
2019
+ ## Recipes
2020
+
2021
+ ### Recipe: CI integration
2022
+
2023
+ ```yaml
2024
+ # .github/workflows/huly-sync.yml
2025
+ name: Sync CI status to Huly
2026
+ on: [push]
2027
+ jobs:
2028
+ sync:
2029
+ runs-on: ubuntu-latest
2030
+ steps:
2031
+ - uses: actions/checkout@v4
2032
+ - run: npm install -g huly-cli
2033
+ - name: Sync status to Huly
2034
+ env:
2035
+ HULY_URL: ${{ secrets.HULY_URL }}
2036
+ HULY_TOKEN: ${{ secrets.HULY_TOKEN }}
2037
+ HULY_WORKSPACE: ${{ vars.HULY_WORKSPACE }}
2038
+ run: |
2039
+ COMMIT_MSG=$(git log -1 --pretty=%B)
2040
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
2041
+ huly issue create --project CI --title "$BRANCH: $COMMIT_MSG" \
2042
+ --label auto --label ci --yes
2043
+ ```
2044
+
2045
+ ### Recipe: Daily standup bot
2046
+
2047
+ ```bash
2048
+ #!/bin/bash
2049
+ # standup.sh - runs daily, posts to #standup channel
2050
+ set -e
2051
+
2052
+ # Get yesterday's issues you closed
2053
+ CLOSED=$(huly issue list --json | \
2054
+ jq -r --arg date "$(date -u -d 'yesterday' +%Y-%m-%d)" \
2055
+ '.[] | select(.modifiedOn > ($date | strptime("%Y-%m-%d") | mktime * 1000)) | select(.status == "Done") | "- #\(.identifier) \(.title)"')
2056
+
2057
+ # Post to channel
2058
+ huly channel message create "standup" --body "Yesterday I closed:
2059
+ $CLOSED
2060
+ "
2061
+ ```
2062
+
2063
+ ### Recipe: Bulk-migrate issues
2064
+
2065
+ ```bash
2066
+ #!/bin/bash
2067
+ # migrate-issues.sh - copy issues from one project to another
2068
+ set -e
2069
+
2070
+ SOURCE=$1
2071
+ DEST=$2
2072
+ STATUS="open"
2073
+
2074
+ IDS=$(huly issue list --project "$SOURCE" --status-category "$STATUS" --json | jq -r '.[]._id')
2075
+
2076
+ for id in $IDS; do
2077
+ # Get full issue
2078
+ issue=$(huly issue get "$id" --json)
2079
+ title=$(echo "$issue" | jq -r .title)
2080
+ desc=$(echo "$issue" | jq -r .description)
2081
+
2082
+ # Create in dest
2083
+ huly issue create --project "$DEST" --title "$title" --description "$desc" --yes
2084
+
2085
+ echo "migrated: $id ($title)"
2086
+ done
2087
+ ```
2088
+
2089
+ ### Recipe: Weekly digest email
2090
+
2091
+ ```bash
2092
+ #!/bin/bash
2093
+ # weekly-digest.sh
2094
+ set -e
2095
+
2096
+ WEEK_AGO=$(date -u -d '7 days ago' +%Y-%m-%d)
2097
+
2098
+ # Issues created this week
2099
+ NEW=$(huly issue list --since "$WEEK_AGO" --json | \
2100
+ jq -r '.[] | "- #\(.identifier) \(.title) (\(.assignee // "unassigned"))"')
2101
+
2102
+ # Issues closed this week
2103
+ CLOSED=$(huly issue list --status Done --since "$WEEK_AGO" --json | \
2104
+ jq -r '.[] | "- #\(.identifier) \(.title)"')
2105
+
2106
+ # Render the email body
2107
+ {
2108
+ echo "Subject: Huly Weekly Digest"
2109
+ echo ""
2110
+ echo "This week:"
2111
+ echo "$NEW"
2112
+ echo ""
2113
+ echo "Closed:"
2114
+ echo "$CLOSED"
2115
+ }
2116
+ ```
2117
+
2118
+ ### Recipe: Audit orphan documents
2119
+
2120
+ ```bash
2121
+ # Find documents with no teamspace
2122
+ huly document list --json | \
2123
+ jq -r '.[] | select(.space == null) | ._id' | \
2124
+ xargs -I{} echo "orphan doc: {}"
2125
+
2126
+ # Find documents with no author
2127
+ huly document list --json | \
2128
+ jq -r '.[] | select(.createdBy == null) | ._id' | \
2129
+ xargs -I{} echo "no-author doc: {}"
2130
+ ```
2131
+
2132
+ ### Recipe: Backup health check
2133
+
2134
+ ```bash
2135
+ #!/bin/bash
2136
+ # backup-health.sh - verify Huly is reachable and authenticated
2137
+ set -e
2138
+
2139
+ if ! huly user get > /dev/null 2>&1; then
2140
+ echo "ALERT: huly not reachable or auth failed"
2141
+ exit 1
2142
+ fi
2143
+
2144
+ if ! huly workspace list > /dev/null 2>&1; then
2145
+ echo "ALERT: workspace list failed"
2146
+ exit 1
2147
+ fi
2148
+
2149
+ echo "OK at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
2150
+ ```
2151
+
2152
+ ### Recipe: Cross-workspace issue link
2153
+
2154
+ ```bash
2155
+ # Get an issue ID from workspace A and reference it in workspace B's issue
2156
+ WS_A_ISSUE=$(huly --workspace prod issue get TSK-1 --json | jq -r '._id')
2157
+ huly --workspace staging issue create \
2158
+ --project TST \
2159
+ --title "Mirrored from prod: $WS_A_ISSUE" \
2160
+ --description "Track this issue across workspaces"
2161
+ ```
2162
+
2163
+ ### Recipe: Cleanup on workspace delete
2164
+
2165
+ ```bash
2166
+ # Delete all docs in a teamspace before deleting the teamspace
2167
+ TS_REF="document:teamspace:Engineering"
2168
+ huly document list --json | \
2169
+ jq -r --arg ts "$TS_REF" '.[] | select(.space == $ts) | ._id' | \
2170
+ xargs -I{} huly document delete {} --yes
2171
+
2172
+ huly teamspace delete "$TS_REF" --yes
2173
+ ```
2174
+
2175
+ ### Recipe: Self-test (no auth needed)
2176
+
2177
+ ```bash
2178
+ # Verify CLI is installed and version
2179
+ huly --version
2180
+
2181
+ # Verify command list
2182
+ huly --help | head -20
2183
+
2184
+ # Run smoke test (requires auth)
2185
+ bash scripts/smoke.sh 0
2186
+ ```
2187
+
2188
+ ### Recipe: Monitor model-upgrade progress
2189
+
2190
+ ```bash
2191
+ # Tail transactor logs for upgrade completion
2192
+ docker logs -f huly_v7-transactor-1 2>&1 | grep -E "upgrade|Processing upgrade"
2193
+ ```
2194
+
2195
+ ### Recipe: Generate report for management
2196
+
2197
+ ```bash
2198
+ #!/bin/bash
2199
+ # management-report.sh
2200
+ set -e
2201
+
2202
+ cat <<EOF
2203
+ Weekly Status Report - $(date +%Y-%m-%d)
2204
+
2205
+ Open issues: $(huly issue list --status-category Active --json | jq length)
2206
+ Closed this week: $(huly issue list --status Done --since "$(date -u -d '7 days ago' +%Y-%m-%d)" --json | jq length)
2207
+
2208
+ Top contributors:
2209
+ $(huly issue list --since "$(date -u -d '7 days ago' +%Y-%m-%d)" --json | \
2210
+ jq -r '.[].assignee' | sort | uniq -c | sort -rn | head -5)
2211
+ EOF
2212
+ ```
2213
+
2214
+ ### Recipe: Backup script
2215
+
2216
+ ```bash
2217
+ #!/bin/bash
2218
+ # backup-workspace.sh - export all data to JSON
2219
+ set -e
2220
+
2221
+ WORKSPACE="${1:?usage: $0 <workspace>}"
2222
+ OUT="/tmp/huly-backup-$WORKSPACE-$(date +%Y%m%d-%H%M%S).json"
2223
+
2224
+ {
2225
+ echo "{"
2226
+ echo "\"workspace\": $(huly --workspace $WORKSPACE workspace info --json),"
2227
+ echo "\"projects\": $(huly --workspace $WORKSPACE project list --json),"
2228
+ echo "\"issues\": $(huly --workspace $WORKSPACE issue list --limit 10000 --json),"
2229
+ echo "\"channels\": $(huly --workspace $WORKSPACE channel list --json),"
2230
+ echo "\"teamspaces\": $(huly --workspace $WORKSPACE teamspace list --json)"
2231
+ echo "}"
2232
+ } > "$OUT"
2233
+
2234
+ echo "backed up to $OUT ($(du -h $OUT | cut -f1))"
2235
+ ```
2236
+
2237
+ ### Recipe: Diff two workspaces
2238
+
2239
+ ```bash
2240
+ # Compare issues between staging and prod
2241
+ diff <(huly --workspace staging issue list --json | jq -S 'sort_by(._id)') \
2242
+ <(huly --workspace prod issue list --json | jq -S 'sort_by(._id)')
2243
+ ```
2244
+
2245
+ ### Recipe: Interactive REPL
2246
+
2247
+ ```bash
2248
+ # Use rlwrap for a huly REPL
2249
+ rlwrap -a -S 'huly> ' -- node -e "
2250
+ const { connect } = require('@hcengineering/api-client');
2251
+ const client = await connect(process.env.HULY_URL, {
2252
+ workspace: process.env.HULY_WORKSPACE,
2253
+ token: process.env.HULY_TOKEN
2254
+ });
2255
+ const issues = await client.findAll('tracker:class:Issue', {});
2256
+ console.log(issues);
2257
+ "
2258
+ ```
2259
+
2260
+ ### Recipe: Generate CLI reference card
2261
+
2262
+ ```bash
2263
+ # One-page cheat sheet
2264
+ huly --help | head -30 > /tmp/cli-cheatsheet.txt
2265
+ for cmd in workspace user project issue channel dm document calendar time; do
2266
+ echo "=== $cmd ===" >> /tmp/cli-cheatsheet.txt
2267
+ huly $cmd --help | head -20 >> /tmp/cli-cheatsheet.txt
2268
+ done
2269
+ cat /tmp/cli-cheatsheet.txt
2270
+ ```
2271
+
2272
+ ---
2273
+
2274
+ ## Performance tuning
2275
+
2276
+ ### Connection reuse across commands
2277
+
2278
+ The CLI opens a new WebSocket per process. For scripts with many calls:
2279
+
2280
+ ```bash
2281
+ # Slow (N connections)
2282
+ for id in $(seq 1 100); do
2283
+ huly issue get "TSK-$id"
2284
+ done
2285
+
2286
+ # Fast (1 connection, N commands)
2287
+ node -e "
2288
+ const { connect } = require('@hcengineering/api-client');
2289
+ const c = await connect(url, { workspace, token });
2290
+ for (let i = 1; i <= 100; i++) {
2291
+ await c.findOne('tracker:class:Issue', { identifier: 'TSK-' + i });
2292
+ }
2293
+ await c.close();
2294
+ "
2295
+ ```
2296
+
2297
+ ### Pagination for large workspaces
2298
+
2299
+ ```bash
2300
+ # First 1000
2301
+ huly issue list --limit 1000 --json > /tmp/issues-1k.json
2302
+
2303
+ # Next 1000 (offset)
2304
+ huly issue list --limit 1000 --offset 1000 --json > /tmp/issues-2k.json
2305
+ ```
2306
+
2307
+ Combine with `jq` for memory-efficient streaming:
2308
+ ```bash
2309
+ huly issue list --limit 10000 --json | jq -c '.[]' | head -100
2310
+ ```
2311
+
2312
+ ### Parallel execution
2313
+
2314
+ ```bash
2315
+ # Run multiple reads in parallel (CLI doesn't share state, so each is safe)
2316
+ huly issue get TSK-1 &
2317
+ huly issue get TSK-2 &
2318
+ huly issue get TSK-3 &
2319
+ wait
2320
+ ```
2321
+
2322
+ ### Avoid full-model loads
2323
+
2324
+ The CLI loads the full model on every connection. For high-frequency
2325
+ scripts, consider whether you really need model-aware operations:
2326
+
2327
+ - `findAll` always loads the model
2328
+ - `api GET /...` (REST) doesn't
2329
+ - `ws` escape hatch doesn't load the client-side model (only the connection)
2330
+
2331
+ ### Bulk-write batching
2332
+
2333
+ The CLI writes one tx per command. For bulk inserts:
2334
+
2335
+ ```bash
2336
+ # Slow (N round-trips)
2337
+ for title in $(seq 1 100); do
2338
+ huly issue create --project TSK --title "Issue $title" --yes
2339
+ done
2340
+
2341
+ # Fast (1 round-trip, N txs in one TxApplyIf)
2342
+ node -e "
2343
+ const { connect } = require('@hcengineering/api-client');
2344
+ const c = await connect(url, { workspace, token });
2345
+ const ops = c.apply();
2346
+ for (let i = 0; i < 100; i++) {
2347
+ ops.createDoc('tracker:class:Issue', projectSpace, { title: 'Issue ' + i, ... });
2348
+ }
2349
+ await ops.commit();
2350
+ "
2351
+ ```
2352
+
2353
+ ---
2354
+
2355
+ ## CLI reference card
2356
+
2357
+ Quick lookup of all flags and their purposes.
2358
+
2359
+ ### Workspace
2360
+ ```
2361
+ list list accessible workspaces
2362
+ current show current workspace
2363
+ use <name> set active workspace
2364
+ create --name X --yes create workspace
2365
+ delete --yes delete current
2366
+ delete --yes --force delete active workspace
2367
+ info show uuid, region, mode
2368
+ members list workspace members
2369
+ member <uuid> --role MAINTAINER change member role
2370
+ rename <new-name> rename current
2371
+ guests --read-only true|false toggle guest read-only
2372
+ guests --sign-up true|false toggle guest sign-up
2373
+ access-link --role GUEST create invite link
2374
+ regions list available regions
2375
+ ```
2376
+
2377
+ ### Project
2378
+ ```
2379
+ list [--limit N] [--offset N]
2380
+ get <ref> by identifier, name, or _id
2381
+ create --name X --identifier BACKEND [--description] [--private]
2382
+ update <ref> --set key=value update fields (null to clear)
2383
+ delete <ref...> [--yes]
2384
+ statuses --project TSK list issue statuses
2385
+ target-preferences --project TSK list target preferences
2386
+ target-preference upsert ... upsert a target preference
2387
+ ```
2388
+
2389
+ ### Issue
2390
+ ```
2391
+ list [--project TSK] [--status <name>] [--status-category Active]
2392
+ [--assignee <email>] [--label bug] [--parent <ref>|null]
2393
+ [--description-search <q>] [--limit N] [--offset N]
2394
+ get <ref> [--markdown]
2395
+ create --project TSK --title "..." [--description] [--body]
2396
+ [--body-file <path>] [--status <name>] [--priority <p>]
2397
+ [--assignee <email>] [--label bug --label auth] [--due ISO]
2398
+ [--parent <ref>] [--task-type <name>]
2399
+ update <ref> --title "..." update fields
2400
+ delete <ref...> [--yes]
2401
+ preview-delete <ref...> show impact of delete
2402
+ label <ref> add <name> add a label
2403
+ label <ref> remove <name> remove a label
2404
+ relation <ref> add <type> <target> add a relation
2405
+ relation <ref> remove <type> <target> remove
2406
+ relation <ref> list list relations
2407
+ link-document <issueRef> <docRef> link a document
2408
+ unlink-document <issueRef> <docRef> unlink
2409
+ move <ref> --parent <ref|null> set/clear parent
2410
+ related-targets --project TSK list related targets
2411
+ related-target set --project ... create a related target
2412
+ ```
2413
+
2414
+ ### Document
2415
+ ```
2416
+ list
2417
+ create --title "..." [--body <md>] [--body-file <path>]
2418
+ [--teamspace <name>] [--parent <ref>] [--description] [--archived]
2419
+ update <ref> [--title] [--body] [--body-file]
2420
+ [--old-text] [--new-text] [--replace-all]
2421
+ delete <ref...> [--yes]
2422
+ snapshots <ref> list version snapshots
2423
+ snapshot <ref> get a specific snapshot
2424
+ inline-comments <ref> list inline comments
2425
+ ```
2426
+
2427
+ ### Teamspace
2428
+ ```
2429
+ list
2430
+ get <ref>
2431
+ create --name "Engineering" [--description] [--private]
2432
+ delete <ref...> [--yes]
2433
+ ```
2434
+
2435
+ ### Channel
2436
+ ```
2437
+ list [--archived]
2438
+ get <ref>
2439
+ create --name "engineering" [--topic "..."] [--private]
2440
+ update <ref> --topic "..."
2441
+ delete <ref...> [--yes]
2442
+ archive <ref> [--value false]
2443
+ members <ref>
2444
+ join <ref> [--member <email>]
2445
+ leave <ref>
2446
+ add-member <ref> <email...>
2447
+ remove-member <ref> <email...>
2448
+ message list <channelRef>
2449
+ message get <channelRef> <messageRef> [--markdown]
2450
+ message create <channelRef> --body "..."
2451
+ message update <channelRef> <messageRef> --body "..."
2452
+ message delete <channelRef> <messageRef...> [--yes]
2453
+ ```
2454
+
2455
+ ### DM
2456
+ ```
2457
+ list
2458
+ create --person <email>
2459
+ messages <dmRef>
2460
+ send <dmRef> --body "..."
2461
+ send --person <email> --body "..." auto-creates DM
2462
+ ```
2463
+
2464
+ ### Thread
2465
+ ```
2466
+ list <targetRef>
2467
+ add <targetRef> --body "..."
2468
+ update <replyRef> --body "..."
2469
+ delete <replyRef...> [--yes]
2470
+ ```
2471
+
2472
+ ### Card
2473
+ ```
2474
+ list
2475
+ get <ref> [--markdown]
2476
+ create --master-tag <name|id> --title "..." [--body] [--body-file]
2477
+ update <ref> [--title] [--description] [--body] [--body-file]
2478
+ delete <ref...> [--yes]
2479
+ ```
2480
+
2481
+ ### Card-space
2482
+ ```
2483
+ list
2484
+ get <ref>
2485
+ create --name "Engineering" [--description] [--private]
2486
+ delete <ref...> [--yes]
2487
+ ```
2488
+
2489
+ ### Master-tag
2490
+ ```
2491
+ list
2492
+ ```
2493
+
2494
+ ### Action (Planner)
2495
+ ```
2496
+ list [--completed all|open|done] [--priority High] [--owner <email>]
2497
+ get <ref>
2498
+ create --title "..." [--description] [--body] [--body-file]
2499
+ [--due ISO] [--priority High] [--owner <email>]
2500
+ [--attached-to <ref>] [--attached-to-class <classId>]
2501
+ update <ref> [--title] [--description] [--body] [--body-file]
2502
+ complete <ref> sets doneOn=now
2503
+ reopen <ref> clears doneOn
2504
+ schedule <ref> creates WorkSlot
2505
+ unschedule <ref> removes WorkSlots
2506
+ delete <ref...> [--yes]
2507
+ ```
2508
+
2509
+ ### Calendar
2510
+ ```
2511
+ calendars list calendars (not events)
2512
+ create-calendar --name "Work" [--description] [--private] [--access ...]
2513
+ delete-calendar <ref>
2514
+ list list events
2515
+ get <eventRef> [--markdown]
2516
+ create --title "..." [--start ISO] [--end ISO] [--attendee <email>]
2517
+ [--location] [--all-day] [--description] [--body]
2518
+ [--calendar-id <ref>] [--rrule "FREQ=DAILY;COUNT=3"]
2519
+ update <eventRef> [--title] [--start] [--end] [--attendee]
2520
+ delete <eventRef...> [--yes]
2521
+ recurring list recurring event definitions
2522
+ recurring-instances <recRef> list materialized instances
2523
+ ```
2524
+
2525
+ ### Schedule
2526
+ ```
2527
+ list
2528
+ create --owner <userUuid> [--time-zone UTC] [--description]
2529
+ [--duration 30] [--interval 30]
2530
+ update <ref> [...]
2531
+ delete <ref...> [--yes]
2532
+ ```
2533
+
2534
+ ### Time
2535
+ ```
2536
+ log --issue TSK-1 --minutes 30 --description "..."
2537
+ log --issue TSK-1 --hours 2 --description "..."
2538
+ report --from 2026-06-01 --to 2026-06-30 [--user <email>] [--project TSK]
2539
+ delete <entryRef...> [--yes]
2540
+ ```
2541
+
2542
+ ### Component / Milestone / Issue-template
2543
+ ```
2544
+ list --project TSK
2545
+ get <ref>
2546
+ create --project TSK --label "..."
2547
+ update <ref> --label "..."
2548
+ delete <ref...> [--yes]
2549
+ ```
2550
+
2551
+ (Issue-template additionally has `add-child` and `remove-child`.)
2552
+
2553
+ ### Comment
2554
+ ```
2555
+ list --issue TSK-1
2556
+ add --issue TSK-1 --body "..."
2557
+ add --issue TSK-1 --body-file <path>
2558
+ update <commentRef> --body "..."
2559
+ delete <ref...> [--yes]
2560
+ ```
2561
+
2562
+ ### User
2563
+ ```
2564
+ get [--ref <uuid>]
2565
+ update --city "Berlin"
2566
+ find <email> account-level or workspace-local lookup
2567
+ ```
2568
+
2569
+ ### API / WS escape hatches
2570
+ ```
2571
+ api <METHOD> <path> [--body json] [--query k=v] [--header k=v]
2572
+ ws <method> [params-json]
2573
+ ```
2574
+
2575
+ EOF
2576
+ wc -l README.md