@sudosapient/cli 0.1.0 → 0.2.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 +43 -2
- package/package.json +2 -2
- package/src/cli.js +158 -15
- package/src/commands.js +1202 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @sudosapient/cli (`sudo-cli`)
|
|
2
2
|
|
|
3
|
-
Sudo Sapient from the terminal:
|
|
4
|
-
for scripts. It talks only to the public API (`/api/v1`) with a personal API key, so it can do exactly
|
|
3
|
+
Sudo Sapient from the terminal: everything you can do in the app, from capturing a task to sharing a
|
|
4
|
+
project, uploading a file or managing people, with `--json` for scripts. It talks only to the public API (`/api/v1`) with a personal API key, so it can do exactly
|
|
5
5
|
what you can do in the app, and its changes show in Activity as "you via CLI".
|
|
6
6
|
|
|
7
7
|
## Install
|
|
@@ -60,10 +60,51 @@ sudo-cli open WEB-212 # in the browser (--print just prints the URL)
|
|
|
60
60
|
sudo-cli tasks --json | jq '.[].key'
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
### Everything else
|
|
64
|
+
|
|
65
|
+
`sudo-cli help <command>` lists a command's subcommands and options (or `sudo-cli <command> --help`).
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
sudo-cli tasks edit WEB-212 --status review --label bug,frontend --repeat weekly --depends WEB-200
|
|
69
|
+
sudo-cli tasks archive WEB-212 · tasks restore · tasks dup · tasks fav · tasks rm · tasks undelete <id>
|
|
70
|
+
sudo-cli subtask add WEB-212 "Write tests" -a grace # ids come from sudo-cli show WEB-212
|
|
71
|
+
sudo-cli subtask done WEB-212 <id> · subtask edit · subtask rm · subtask promote
|
|
72
|
+
sudo-cli comment edit <id> "…" · comment rm <id> · comment undelete <id> · react <id> 👍 · unreact <id> 👍
|
|
73
|
+
sudo-cli projects new "Mobile App" --prefix MOB --private --members grace --perm edit
|
|
74
|
+
sudo-cli projects edit MOB --status risk --due 2026-12-01 --milestone "Beta=2026-11-20"
|
|
75
|
+
sudo-cli projects share MOB grace@example.com --perm comment · projects unshare MOB grace
|
|
76
|
+
sudo-cli projects show · archive · restore · dup · rm · request-access · fav · order WEB MOB
|
|
77
|
+
sudo-cli teams new Platform --members grace,ada · teams edit · teams rm
|
|
78
|
+
sudo-cli people invite cara@example.com --role Guest · people resend · people edit · people role grace Admin · people rm
|
|
79
|
+
sudo-cli files upload ./spec.pdf --task WEB-212 · files download <id> [-o path|-] · files attach <id> WEB-212
|
|
80
|
+
sudo-cli files rename · detach · dup · rm
|
|
81
|
+
sudo-cli events new "Launch" --date 2026-12-01 --time 09:30 -p WEB · events --from today --to +30d
|
|
82
|
+
sudo-cli views new "Urgent" -p WEB --type board --filter priority=urgent,high --filter assignee!=me
|
|
83
|
+
sudo-cli inbox --unread · inbox read <id> · inbox read --all · activity -p WEB · search "login"
|
|
84
|
+
sudo-cli me edit --title Engineer · prefs set theme=dark tz=Europe/Berlin · notify-prefs set email_comment=on
|
|
85
|
+
sudo-cli workspace · favorites
|
|
86
|
+
sudo-cli api GET /tasks -f project=WEB # any endpoint; -f sends strings, -F typed values, --data JSON
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **Admin commands** (`people role`, `people rm`, others' names and titles, `workspace edit`,
|
|
90
|
+
`workspace invite-link`, `bots …`) work when the key's person is an Owner or Admin, as in the app.
|
|
91
|
+
For anyone else they fail with "Only owners and admins can do this".
|
|
92
|
+
- **Deleting** a project, person, bot, team or file asks first; `--yes` (`-y`) skips the question,
|
|
93
|
+
and without a terminal to ask at (a script) `--yes` is required. Tasks and comments don't ask:
|
|
94
|
+
`tasks rm` and `comment rm` print how to undo them, which works for 10 minutes.
|
|
95
|
+
- **Downloads** go to the file's own name in the current directory (reduced to a plain file name), or
|
|
96
|
+
`-o <path>`, or `-o -` for stdout. An existing file is never overwritten without `--force`.
|
|
97
|
+
- Bot keys are made in Settings → API → Bots only; `bots` can list, change, remove and revoke them.
|
|
98
|
+
- `none` clears a field: `--due none`, `--team none`, `--repeat none`, `--label none`.
|
|
99
|
+
|
|
63
100
|
Due dates: `today`, `tomorrow`, a weekday (`friday`, `fri`; today counts), `next week`, `+3d`, `+2w`, or
|
|
64
101
|
`2026-11-20`. Set `SUDO_PROJECT=WEB` to skip `-p`. Exit codes: `0` ok, `1` the API refused (the
|
|
65
102
|
message says why; with `--json` it's `{ error, code }` on stderr), `2` a usage error.
|
|
66
103
|
|
|
104
|
+
Your keys share one rate limit (120 requests a minute by default). When a bulk run reaches it,
|
|
105
|
+
`sudo-cli` waits until the server says it may go on (`rate limited; retrying in 53 s…` on stderr) and
|
|
106
|
+
then carries on, up to 3 times per request.
|
|
107
|
+
|
|
67
108
|
## License
|
|
68
109
|
|
|
69
110
|
UNLICENSED, all rights reserved. The package is public on npm so anyone can install and run it, but
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudosapient/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Sudo Sapient from the terminal: capture, list, assign and complete tasks.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"README.md"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@sudosapient/client": "0.
|
|
22
|
+
"@sudosapient/client": "0.2.0"
|
|
23
23
|
},
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=22"
|
package/src/cli.js
CHANGED
|
@@ -7,11 +7,10 @@ import readline from 'node:readline';
|
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
8
|
import { ApiError, createClient, plainHttpRemote, seg } from '@sudosapient/client';
|
|
9
9
|
import { clearConfig, configPath, loadConfig, saveConfig } from '@sudosapient/client/config';
|
|
10
|
+
import { ALIASES, NOUNS, PRIORITIES, STATUSES, nounHelp, rawApi, reaction } from './commands.js';
|
|
10
11
|
|
|
11
12
|
// Kept equal to package.json by release-please (extra-files in release-please-config.json).
|
|
12
|
-
export const VERSION = '0.
|
|
13
|
-
const STATUSES = ['backlog', 'todo', 'progress', 'review', 'done'];
|
|
14
|
-
const PRIORITIES = ['urgent', 'high', 'medium', 'low', 'none'];
|
|
13
|
+
export const VERSION = '0.2.0'; // x-release-please-version
|
|
15
14
|
|
|
16
15
|
export const HELP = `sudo-cli: Sudo Sapient from the terminal
|
|
17
16
|
|
|
@@ -27,8 +26,19 @@ Usage
|
|
|
27
26
|
sudo-cli comment <KEY> "<text>"
|
|
28
27
|
sudo-cli projects [list]
|
|
29
28
|
sudo-cli open <KEY> opens a task or project in the browser (--print: just the URL)
|
|
29
|
+
sudo-cli search "<text>"
|
|
30
|
+
|
|
31
|
+
More (sudo-cli help <command> lists each one's subcommands)
|
|
32
|
+
tasks edit, rm, undelete, archive, restore, dup, fav, unfav
|
|
33
|
+
subtask add, done, undo, edit, rm, promote
|
|
34
|
+
comment edit, rm, undelete; react / unreact <comment id> <emoji>
|
|
35
|
+
projects show, new, edit, archive, restore, dup, rm, share, unshare, request-access, fav, unfav, order
|
|
36
|
+
teams, people, bots, files, events, views, inbox, activity, workspace, me, prefs, notify-prefs, favorites
|
|
37
|
+
api <METHOD> <path> [--data <json>] [-f key=value]... [-F key=value]... any endpoint, JSON out
|
|
30
38
|
|
|
31
39
|
Shortcuts: new, done, assign, show (= tasks …). Dates: today, tomorrow, friday, +3d, +2w, 2026-11-20.
|
|
40
|
+
Deleting a project, person, bot, team or file asks first (--yes to skip; required without a
|
|
41
|
+
terminal). Deleted tasks and comments can be undeleted for 10 minutes, so they don't ask.
|
|
32
42
|
Options: --json (machine-readable output), --url, --insecure (allow a plain http:// URL to another
|
|
33
43
|
machine; login remembers it). The key comes from login, else SUDO_API_KEY; --key works too, but it
|
|
34
44
|
lands in your shell history and the process list, so prefer the prompt or the variable.
|
|
@@ -50,6 +60,42 @@ const OPTIONS = {
|
|
|
50
60
|
mine: { type: 'boolean' },
|
|
51
61
|
all: { type: 'boolean' },
|
|
52
62
|
print: { type: 'boolean' },
|
|
63
|
+
archived: { type: 'boolean' },
|
|
64
|
+
title: { type: 'string' },
|
|
65
|
+
start: { type: 'string' },
|
|
66
|
+
label: { type: 'string', multiple: true },
|
|
67
|
+
repeat: { type: 'string' },
|
|
68
|
+
estimate: { type: 'string' },
|
|
69
|
+
depends: { type: 'string' },
|
|
70
|
+
order: { type: 'string' },
|
|
71
|
+
yes: { type: 'boolean', short: 'y' },
|
|
72
|
+
force: { type: 'boolean' },
|
|
73
|
+
output: { type: 'string', short: 'o' },
|
|
74
|
+
task: { type: 'string' },
|
|
75
|
+
perm: { type: 'string' },
|
|
76
|
+
name: { type: 'string' },
|
|
77
|
+
lead: { type: 'string' },
|
|
78
|
+
team: { type: 'string' },
|
|
79
|
+
private: { type: 'boolean' },
|
|
80
|
+
public: { type: 'boolean' },
|
|
81
|
+
milestone: { type: 'string', multiple: true },
|
|
82
|
+
members: { type: 'string' },
|
|
83
|
+
projects: { type: 'string' },
|
|
84
|
+
role: { type: 'string' },
|
|
85
|
+
color: { type: 'string' },
|
|
86
|
+
icon: { type: 'string' },
|
|
87
|
+
prefix: { type: 'string' },
|
|
88
|
+
unread: { type: 'boolean' },
|
|
89
|
+
from: { type: 'string' },
|
|
90
|
+
to: { type: 'string' },
|
|
91
|
+
date: { type: 'string' },
|
|
92
|
+
time: { type: 'string' },
|
|
93
|
+
type: { type: 'string' },
|
|
94
|
+
filter: { type: 'string', multiple: true },
|
|
95
|
+
brand: { type: 'string' },
|
|
96
|
+
data: { type: 'string' },
|
|
97
|
+
field: { type: 'string', short: 'F', multiple: true },
|
|
98
|
+
'raw-field': { type: 'string', short: 'f', multiple: true },
|
|
53
99
|
help: { type: 'boolean', short: 'h' },
|
|
54
100
|
version: { type: 'boolean', short: 'v' },
|
|
55
101
|
};
|
|
@@ -114,18 +160,43 @@ export async function run(argv, io = {}) {
|
|
|
114
160
|
err.write(`sudo-cli: ${e.message}\nRun sudo-cli --help for usage.\n`);
|
|
115
161
|
return 2;
|
|
116
162
|
}
|
|
163
|
+
const noun = n => (NOUNS[n] ? n : ALIASES[n]);
|
|
164
|
+
// `sudo-cli help tasks` and `sudo-cli tasks --help` list a command's subcommands.
|
|
165
|
+
if (pos[0] === 'help' || (o.help && noun(pos[0]))) {
|
|
166
|
+
const n = noun(pos[0] === 'help' ? pos[1] : pos[0]);
|
|
167
|
+
print(n ? nounHelp(n) : HELP);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
117
170
|
if (o.version || o.help || !pos.length) {
|
|
118
171
|
print(o.version ? VERSION : HELP);
|
|
119
172
|
return o.version || o.help ? 0 : 2;
|
|
120
173
|
}
|
|
121
174
|
|
|
122
|
-
// `tasks new …` and `new …` are the same command.
|
|
175
|
+
// `tasks new …` and `new …` are the same command. Subcommands with a handler in commands.js
|
|
176
|
+
// (`tasks edit`, `projects share`, `teams`, …) run from its table.
|
|
123
177
|
let [cmd, ...args] = pos;
|
|
124
|
-
|
|
178
|
+
let table = null;
|
|
179
|
+
const verbOf = (n, v) => (NOUNS[n].verbs[v]?.run ? NOUNS[n].verbs[v] : null);
|
|
180
|
+
const n0 = noun(cmd);
|
|
181
|
+
if (n0 === 'tasks') {
|
|
125
182
|
const sub = args[0];
|
|
126
|
-
|
|
183
|
+
if (verbOf('tasks', sub)) table = { noun: 'tasks', verb: sub, args: args.slice(1) };
|
|
184
|
+
else [cmd, ...args] = ['new', 'done', 'assign', 'show', 'list'].includes(sub) ? [sub === 'list' ? 'ls' : sub, ...args.slice(1)] : ['ls', ...args];
|
|
185
|
+
} else if (n0 === 'projects') {
|
|
186
|
+
if (verbOf('projects', args[0])) table = { noun: 'projects', verb: args[0], args: args.slice(1) };
|
|
187
|
+
else [cmd, args] = ['projects', args[0] === 'list' ? args.slice(1) : args];
|
|
188
|
+
} else if (n0 === 'comment') {
|
|
189
|
+
if (verbOf('comment', args[0])) table = { noun: 'comment', verb: args[0], args: args.slice(1) };
|
|
190
|
+
else cmd = 'comment';
|
|
191
|
+
} else if (n0 === 'me' && !args.length) cmd = 'whoami';
|
|
192
|
+
else if (n0) {
|
|
193
|
+
const v = verbOf(n0, args[0]) ? args[0] : !args.length || args[0].startsWith('-') ? NOUNS[n0].default : null;
|
|
194
|
+
if (!v || !verbOf(n0, v)) {
|
|
195
|
+
err.write(`sudo-cli: Unknown command “${[cmd, args[0]].filter(Boolean).join(' ')}”. Run sudo-cli help ${n0}.\n`);
|
|
196
|
+
return 2;
|
|
197
|
+
}
|
|
198
|
+
table = { noun: n0, verb: v, args: v === args[0] ? args.slice(1) : args };
|
|
127
199
|
}
|
|
128
|
-
if (cmd === 'projects' || cmd === 'project') [cmd, args] = ['projects', args[0] === 'list' ? args.slice(1) : args];
|
|
129
200
|
|
|
130
201
|
const cfg = loadConfig(env);
|
|
131
202
|
const url = (o.url || cfg.url || '').replace(/\/+$/, '');
|
|
@@ -138,17 +209,19 @@ export async function run(argv, io = {}) {
|
|
|
138
209
|
if (plainHttpRemote(u) && !ok)
|
|
139
210
|
throw new UsageError(`${u} is plain http, so your API key would cross the network unencrypted. Use https, or add --insecure if you mean it.`);
|
|
140
211
|
};
|
|
212
|
+
// The client waits out rate limits (Retry-After) by itself; say so, or a bulk run looks stuck.
|
|
213
|
+
const rateLimited = ({ delay }) => err.write(`sudo-cli: rate limited; retrying in ${Math.ceil(delay / 1000)} s…\n`);
|
|
141
214
|
const api = () => {
|
|
142
215
|
if (!url || !key) throw new UsageError('Not logged in. Run: sudo-cli login --url https://your-workspace');
|
|
143
216
|
needSecure(url);
|
|
144
|
-
const a = createClient({ url, key, client: 'CLI', fetch: io.fetch });
|
|
217
|
+
const a = createClient({ url, key, client: 'CLI', fetch: io.fetch, sleep: io.sleep, onRetry: rateLimited });
|
|
145
218
|
// JSON output escapes control characters itself; human output gets them stripped.
|
|
146
219
|
if (o.json) return a;
|
|
147
220
|
const wrap =
|
|
148
221
|
f =>
|
|
149
222
|
async (...args) =>
|
|
150
223
|
clean(await f(...args));
|
|
151
|
-
return { ...a, get: wrap(a.get), post: wrap(a.post), patch: wrap(a.patch), all: wrap(a.all) };
|
|
224
|
+
return { ...a, get: wrap(a.get), post: wrap(a.post), patch: wrap(a.patch), put: wrap(a.put), del: wrap(a.del), upload: wrap(a.upload), all: wrap(a.all) };
|
|
152
225
|
};
|
|
153
226
|
const json = v => print(JSON.stringify(v, null, 2));
|
|
154
227
|
const who = async a => new Map((await a.all('/members')).map(m => [m.id, m.name]));
|
|
@@ -160,7 +233,38 @@ export async function run(argv, io = {}) {
|
|
|
160
233
|
return v;
|
|
161
234
|
};
|
|
162
235
|
|
|
236
|
+
/* What commands.js handlers get. */
|
|
237
|
+
const x = {
|
|
238
|
+
o,
|
|
239
|
+
io: { ...io, stdout: out },
|
|
240
|
+
url,
|
|
241
|
+
api,
|
|
242
|
+
print,
|
|
243
|
+
json,
|
|
244
|
+
bold,
|
|
245
|
+
dim,
|
|
246
|
+
pad,
|
|
247
|
+
need,
|
|
248
|
+
names: who,
|
|
249
|
+
usage: msg => {
|
|
250
|
+
throw new UsageError(msg);
|
|
251
|
+
},
|
|
252
|
+
date: v => (/^none$/i.test(String(v)) ? null : parseDue(v)),
|
|
253
|
+
emit: (data, human) => (o.json ? json(data) : human(data)),
|
|
254
|
+
cancelled: () => (err.write('sudo-cli: cancelled.\n'), 1),
|
|
255
|
+
/* A yes at the terminal, or --yes. Without a terminal to ask at, --yes is required. */
|
|
256
|
+
async confirm(question) {
|
|
257
|
+
if (o.yes) return true;
|
|
258
|
+
if (!(io.stdin || process.stdin).isTTY) throw new UsageError(`${question} Pass --yes to go ahead (there's no terminal to ask at).`);
|
|
259
|
+
return /^y(es)?$/i.test((await ask(io, `${question} [y/N] `)).trim());
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
163
263
|
try {
|
|
264
|
+
if (table) {
|
|
265
|
+
x.args = table.args;
|
|
266
|
+
return (await NOUNS[table.noun].verbs[table.verb].run(x)) || 0;
|
|
267
|
+
}
|
|
164
268
|
switch (cmd) {
|
|
165
269
|
case 'login': {
|
|
166
270
|
const u = (o.url || (await ask(io, `Workspace URL${cfg.url ? ` (${cfg.url})` : ''}: `)) || cfg.url || '').trim().replace(/\/+$/, '');
|
|
@@ -169,7 +273,7 @@ export async function run(argv, io = {}) {
|
|
|
169
273
|
needSecure(u, o.insecure);
|
|
170
274
|
const k = (o.key || (await ask(io, 'API key (Settings → API): ', { hidden: true }))).trim();
|
|
171
275
|
need(k, 'Paste an API key from Settings → API.');
|
|
172
|
-
const me = clean(await createClient({ url: u, key: k, client: 'CLI', fetch: io.fetch }).get('/me'));
|
|
276
|
+
const me = clean(await createClient({ url: u, key: k, client: 'CLI', fetch: io.fetch, sleep: io.sleep, onRetry: rateLimited }).get('/me'));
|
|
173
277
|
const file = saveConfig({ url: u, key: k, ...(o.insecure && plainHttpRemote(u) ? { insecure: true } : {}) }, env);
|
|
174
278
|
if (o.json) json({ ok: true, user: me, config: file });
|
|
175
279
|
else print(`Logged in to ${me.workspace || u} as ${me.name} (${me.key.scope} key “${me.key.name}”). Saved to ${file}`);
|
|
@@ -213,7 +317,8 @@ export async function run(argv, io = {}) {
|
|
|
213
317
|
const mine = o.mine || !project;
|
|
214
318
|
const status = o.status || (o.all ? undefined : 'open');
|
|
215
319
|
const n = Math.min(Math.max(Number(o.limit) || 50, 1), 500);
|
|
216
|
-
const
|
|
320
|
+
const query = { project, status, assignee: mine ? 'me' : undefined, ...(o.archived ? { includeArchived: 'true' } : {}) };
|
|
321
|
+
const [tasks, names] = await Promise.all([a.all('/tasks', query, n), who(a)]);
|
|
217
322
|
if (o.json) {
|
|
218
323
|
json(tasks);
|
|
219
324
|
return 0;
|
|
@@ -276,8 +381,17 @@ export async function run(argv, io = {}) {
|
|
|
276
381
|
print(`${bold(t.key)} ${t.title}`);
|
|
277
382
|
print(dim(`${t.status} · ${t.priority} · ${names.get(t.assignee) || 'unassigned'}${t.due ? ` · due ${t.due.slice(0, 10)}` : ''}`));
|
|
278
383
|
if (t.description) print('\n' + t.description);
|
|
279
|
-
for (const s of t.subtasks)
|
|
280
|
-
|
|
384
|
+
for (const s of t.subtasks)
|
|
385
|
+
print(
|
|
386
|
+
` [${s.done ? 'x' : ' '}] ${s.title}${s.assignee ? ` · ${names.get(s.assignee) || '?'}` : ''}${s.due ? ` · ${s.due.slice(0, 10)}` : ''} ${dim(s.id)}`,
|
|
387
|
+
);
|
|
388
|
+
for (const c of comments.data)
|
|
389
|
+
print(
|
|
390
|
+
`\n${bold(names.get(c.author) || '?')} ${dim(new Date(c.createdAt).toLocaleString())} ${dim(c.id)}\n${c.text}${Object.entries(c.reactions || {})
|
|
391
|
+
.map(([e, w]) => ` ${e}${w.length}`)
|
|
392
|
+
.join('')
|
|
393
|
+
.replace(/^ /, '\n')}`,
|
|
394
|
+
);
|
|
281
395
|
print('\n' + dim(t.url));
|
|
282
396
|
return 0;
|
|
283
397
|
}
|
|
@@ -291,13 +405,15 @@ export async function run(argv, io = {}) {
|
|
|
291
405
|
return 0;
|
|
292
406
|
}
|
|
293
407
|
case 'projects': {
|
|
294
|
-
const ps = await api().all('/projects');
|
|
408
|
+
const ps = await api().all('/projects', o.archived ? { includeArchived: 'true' } : {});
|
|
295
409
|
if (o.json) {
|
|
296
410
|
json(ps);
|
|
297
411
|
return 0;
|
|
298
412
|
}
|
|
299
413
|
for (const p of ps)
|
|
300
|
-
print(
|
|
414
|
+
print(
|
|
415
|
+
`${bold(pad(p.key, 8))} ${pad(p.name, 28)} ${dim(`${p.openTasks} open · ${p.doneTasks} done${p.private ? ' · private' : ''}${p.archived ? ' · archived' : ''}`)}`,
|
|
416
|
+
);
|
|
301
417
|
return 0;
|
|
302
418
|
}
|
|
303
419
|
case 'open': {
|
|
@@ -325,6 +441,33 @@ export async function run(argv, io = {}) {
|
|
|
325
441
|
.unref();
|
|
326
442
|
return 0;
|
|
327
443
|
}
|
|
444
|
+
case 'react':
|
|
445
|
+
case 'unreact': {
|
|
446
|
+
const [id, e] = args;
|
|
447
|
+
need(id && e, `sudo-cli ${cmd} <comment id> 👍 (comment ids are in sudo-cli show <KEY>)`);
|
|
448
|
+
const path = `/comments/${seg(id)}/reactions/${seg(reaction(x, e))}`;
|
|
449
|
+
const c = cmd === 'react' ? await api().put(path) : await api().del(path);
|
|
450
|
+
x.emit(c, () => print(`${cmd === 'react' ? 'Reacted' : 'Took back'} ${reaction(x, e)}`));
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
case 'search': {
|
|
454
|
+
const q = need(args.join(' ').trim(), 'What to search for? sudo-cli search "login bug"');
|
|
455
|
+
const a = api();
|
|
456
|
+
const [r, names] = await Promise.all([a.get('/search', { q }), who(a)]);
|
|
457
|
+
if (o.json) {
|
|
458
|
+
json(r);
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
for (const t of r.tasks) print(row(t, names));
|
|
462
|
+
for (const p of r.projects) print(`${bold(pad(p.key, 9))} ${dim('project')} ${p.name}`);
|
|
463
|
+
for (const m of r.members) print(`${bold(pad(m.name, 22))} ${dim(m.email || m.kind)}`);
|
|
464
|
+
if (!r.tasks.length && !r.projects.length && !r.members.length) print(dim('No matches.'));
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
case 'api':
|
|
468
|
+
x.args = args;
|
|
469
|
+
await rawApi(x);
|
|
470
|
+
return 0;
|
|
328
471
|
default:
|
|
329
472
|
throw new UsageError(`Unknown command “${cmd}”. Run sudo-cli --help.`);
|
|
330
473
|
}
|