@iamcoder18/huly-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2576 -0
- package/bin/huly +9 -0
- package/dist/auth/cache.js +129 -0
- package/dist/auth/cache.js.map +1 -0
- package/dist/auth/client.js +192 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/env.js +101 -0
- package/dist/auth/env.js.map +1 -0
- package/dist/auth/prompts.js +68 -0
- package/dist/auth/prompts.js.map +1 -0
- package/dist/cli.js +1959 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/dry-run.js +39 -0
- package/dist/commands/dry-run.js.map +1 -0
- package/dist/commands/login.js +92 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/whoami.js +64 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/output/errors.js +99 -0
- package/dist/output/errors.js.map +1 -0
- package/dist/output/format.js +607 -0
- package/dist/output/format.js.map +1 -0
- package/dist/output/progress.js +30 -0
- package/dist/output/progress.js.map +1 -0
- package/dist/raw/api.js +67 -0
- package/dist/raw/api.js.map +1 -0
- package/dist/raw/ws.js +157 -0
- package/dist/raw/ws.js.map +1 -0
- package/dist/resources/_helpers.js +258 -0
- package/dist/resources/_helpers.js.map +1 -0
- package/dist/resources/_project-resolve.js +24 -0
- package/dist/resources/_project-resolve.js.map +1 -0
- package/dist/resources/calendar.js +659 -0
- package/dist/resources/calendar.js.map +1 -0
- package/dist/resources/card.js +358 -0
- package/dist/resources/card.js.map +1 -0
- package/dist/resources/channel.js +709 -0
- package/dist/resources/channel.js.map +1 -0
- package/dist/resources/comment.js +142 -0
- package/dist/resources/comment.js.map +1 -0
- package/dist/resources/component.js +154 -0
- package/dist/resources/component.js.map +1 -0
- package/dist/resources/document.js +584 -0
- package/dist/resources/document.js.map +1 -0
- package/dist/resources/issue-template.js +228 -0
- package/dist/resources/issue-template.js.map +1 -0
- package/dist/resources/issue.js +909 -0
- package/dist/resources/issue.js.map +1 -0
- package/dist/resources/milestone.js +177 -0
- package/dist/resources/milestone.js.map +1 -0
- package/dist/resources/misc.js +2 -0
- package/dist/resources/misc.js.map +1 -0
- package/dist/resources/project.js +341 -0
- package/dist/resources/project.js.map +1 -0
- package/dist/resources/project.parse.js +25 -0
- package/dist/resources/project.parse.js.map +1 -0
- package/dist/resources/time.js +148 -0
- package/dist/resources/time.js.map +1 -0
- package/dist/resources/todo.js +463 -0
- package/dist/resources/todo.js.map +1 -0
- package/dist/resources/user.js +131 -0
- package/dist/resources/user.js.map +1 -0
- package/dist/resources/workspace.js +252 -0
- package/dist/resources/workspace.js.map +1 -0
- package/dist/transport/identifiers.js +67 -0
- package/dist/transport/identifiers.js.map +1 -0
- package/dist/transport/ref-resolver.js +108 -0
- package/dist/transport/ref-resolver.js.map +1 -0
- package/dist/transport/sdk.js +69 -0
- package/dist/transport/sdk.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
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
|