avo-cli 4.2.0.beta.3 → 4.2.0.beta.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80f98f637e3c8fdcf98240d286c5fba151033e1333fd4aa49ddd3d0b4d25ddad
4
- data.tar.gz: 6326d242760f69de8421da87b7b87c45ddd78737bb689fc8a0a8002a0a77fc36
3
+ metadata.gz: 66d0e9e6a0168a500f44a92e621a3f49f3a388a8eaf063233d55784b86ebf7a2
4
+ data.tar.gz: f8a276ec085a14b64f9ee1ab48e3ddcf2ef958dfab0f9462fbd71d763daed8b4
5
5
  SHA512:
6
- metadata.gz: 4673f8787bbc91a3fdb3c35e70ef06154d528578896ef93261d9205d18396aac2503a378f226da78e510a15a384a7ab707fdae1280d7209fea359bdeb5a2509f
7
- data.tar.gz: d479b7cc761f23cd443e7aae09209e4856868e3ae1c77214e4c58f38e702fe641c2f1d368775b33a511ae92459b2719a1d0c8790c94fa62ad84e733bc02661e0
6
+ metadata.gz: 2a013c30323a74482001cbc1b013f4b680ed3645e9938cbcf085338816753a5c1c438e5d63f7f04f7d29e8db9c5f3ec8596f3b9fc45fc448d5bacf41e106ee55
7
+ data.tar.gz: 117498f71159878438130a4fd033572d94c08501bfbd29b0c7472907249c33f6044e6ff8f2285930f770532c5022f3d88bb79e771b185bce247b6818a7845ec1
data/README.md CHANGED
@@ -1,263 +1,65 @@
1
1
  # avo-cli
2
2
 
3
3
  Command line client for the [Avo REST API](https://docs.avohq.io/4.0/rest-api.html).
4
- It talks to any Rails app running `avo-api`; nothing is installed in the app.
4
+ It talks to any Rails app running `avo-api` over HTTP; nothing is installed in
5
+ the app. A table per command for a person, the raw JSON body under
6
+ `--format json` for a script or an agent.
5
7
 
6
- Status: every command works against any app running `avo-api`, and the docs
7
- site describes them. It ships as a Ruby gem that runs the built client on the
8
- machine's Node ([docs/install.md](docs/install.md)); the gem builds and installs
9
- locally and has not been released yet. What is left is in
10
- [docs/plans/v1-todo.md](docs/plans/v1-todo.md).
8
+ The full guide is at [docs.avohq.io/4.0/cli](https://docs.avohq.io/4.0/cli.html).
11
9
 
12
10
  ## Requirements
13
11
 
14
- - Running the CLI: Node 18.12.1 or newer. 18.0 fails at startup on a missing
15
- `node:fs/promises` export. The installed gem also needs Ruby 3.0 or newer,
16
- for the `avo` executable that finds Node and hands the line over.
17
- - Developing it: Node 22.13 or newer (`.node-version` pins it). The lint toolchain
18
- (`@typescript-eslint` → `eslint-visitor-keys@5`) refuses anything older.
19
- - yarn
12
+ - Ruby 3.0 or newer, for the `avo` executable.
13
+ - Node 18.12.1 or newer on your `PATH`, or named by `AVO_CLI_NODE`. Node is
14
+ not bundled.
15
+ - An app running `avo-api`, reachable over HTTP, and an
16
+ [API token](https://docs.avohq.io/4.0/rest-api.html#create-a-token) created
17
+ in that app.
20
18
 
21
- ## Install (development)
19
+ ## Install
22
20
 
23
21
  ```sh
24
- cd gems/avo-cli
25
- yarn install
26
- ./bin/dev.js --version
22
+ gem install avo-cli
23
+ avo --version
27
24
  ```
28
25
 
29
- `./bin/dev.js` runs the TypeScript sources directly through tsx.
30
- `./bin/run.js` runs the compiled `dist/` after `yarn build`. The installed gem's
31
- `avo` runs that same `run.js`, from the `payload/` the gem carries. To build the
32
- gem and install it locally, see [docs/distribution.md](docs/distribution.md) §6.
33
-
34
- ## Configuration
26
+ ## Connect
35
27
 
36
28
  Log in once and every later command knows the app:
37
29
 
38
30
  ```sh
39
- avo login # asks for the host, the token and the API version (Enter for v1), checks them, saves them
40
- avo schema # works in any terminal from here on
31
+ avo login # asks for the API base URL, the token and the API version, checks them, saves them
32
+ avo schema # what the token may reach
41
33
  avo logout # deletes what was saved
42
34
  ```
43
35
 
44
- `avo login` saves to `~/.config/avo/config.json` (`$XDG_CONFIG_HOME/avo` when
45
- set, `%LOCALAPPDATA%\avo` on Windows), readable by your user only. The token is
46
- stored as plain text, like `~/.bundle/config`, so use a token you can revoke.
47
-
48
- The host is the API base URL: the address `avo-api` answers on, mount path
49
- included, exactly what you would paste into Postman before `/resources/...`.
50
- An app that mounts it the usual way is `http://localhost:3000/api`; one that
51
- mounts it elsewhere, say `mount_avo_api at: "avo"`, is
52
- `http://localhost:3000/avo`. The CLI appends only `/resources/<version>/<path>`
53
- and guesses no mount, so the address that works in a browser works here.
54
-
55
- For CI and scripts, skip `login` and set the variables. A flag wins over its
56
- variable, and either wins over what was saved. The saved host and token are
57
- used only together: name a host or a token yourself and the saved pair is left
58
- out, so a saved token never reaches another host. The saved API version goes
59
- with them: `avo login --api-version v2` once, and later commands use `v2`.
36
+ The API base URL is the address `avo-api` answers on, mount path included,
37
+ e.g. `http://localhost:3000/api`. The token is saved as plain text in
38
+ `~/.config/avo/config.json`, so use one you can revoke.
60
39
 
61
- | Variable | Flag | Default | Purpose |
62
- | ----------------- | --------------- | ------- | ------------------------------------------------------------------- |
63
- | `AVO_API_HOST` | `--host` | none | API base URL, mount path included, e.g. `http://localhost:3000/api` |
64
- | `AVO_API_TOKEN` | `--token` | none | API token secret, sent as `Authorization: Bearer` |
65
- | `AVO_API_VERSION` | `--api-version` | `v1` | API version segment of the URL, saved by `login` |
40
+ For CI and scripts, skip `login` and set `AVO_API_HOST`, `AVO_API_TOKEN` and
41
+ `AVO_API_VERSION`, or pass `--host`, `--token` and `--api-version`. See
42
+ [Connect without logging in](https://docs.avohq.io/4.0/cli.html#connect-without-logging-in).
66
43
 
67
- ## Commands
68
-
69
- The gem is not released yet, so there is no `avo` on anyone's PATH: every
70
- example here runs the source through `./bin/dev.js`. The generated reference
71
- further down prints `avo`, the released name. These examples are swept to it
72
- when the gem ships.
73
-
74
- ```sh
75
- ./bin/dev.js schema # what the token may reach, as a table
76
- ./bin/dev.js schema --format json # the _schema body, byte for byte
77
- ./bin/dev.js schema users # the fields a create sends, with required
78
- ./bin/dev.js schema users --view show # the fields a record reads back
79
- ./bin/dev.js list users # first page, one row per record
80
- ./bin/dev.js list users --page 2 --per-page 10 # paging
81
- ./bin/dev.js list users --sort name --dir desc # sorting, server side
82
- ./bin/dev.js list users --fields id,name,email # pick the columns
83
- ./bin/dev.js get users 5 # one record, field by field
84
- ./bin/dev.js get users 5 --format json # the body, byte for byte
85
- ./bin/dev.js get users 5 --verbose # request and response lines on stderr
86
- ./bin/dev.js create users --data '{"name": "Ada", "email": "ada@example.com"}'
87
- ./bin/dev.js update users 5 --data '{"name": "Ada King", "bio": null}' # null clears
88
- ./bin/dev.js create users --data @user.json # the object from a file
89
- ./bin/dev.js update users 5 --data - < user.json # the object from stdin
90
- ./bin/dev.js delete users 5 # no confirmation, see below
91
- ./bin/dev.js login # save the host, token and API version
92
- ./bin/dev.js logout # delete what login saved
93
- ```
94
-
95
- ### Grammar
44
+ ## Usage
96
45
 
97
46
  Every line has the same shape:
98
47
 
99
48
  ```
100
- ./bin/dev.js <verb> <resource> [id] [flags]
101
- ```
102
-
103
- There are six verbs: `schema`, `list`, `get`, `create`, `update` and `delete`.
104
- The resource is always an argument, never a command, because route keys come
105
- from the app being talked to and only after an HTTP call. `login` and `logout`
106
- are the two commands outside the grammar: they take no resource
107
- ([Configuration](#configuration)).
108
-
109
- `./bin/dev.js --help` lists the six verbs, with `login`, `logout` and `help`
110
- beside them, and `./bin/dev.js get --help` shows one. A line that names no
111
- command prints the usage above, with a did-you-mean when the typo is close to
112
- something real.
113
-
114
- Each verb has a one-letter shortcut — `s l g c u d`, in the order listed above
115
- — in the same slot, so `./bin/dev.js l users` is `list users`. They are hidden
116
- aliases: help, the reference below and the error messages all keep to the full
117
- spelling, and nothing else about the grammar bends. `./bin/dev.js users l` is
118
- refused like any other line that puts the resource first.
119
-
120
- ### Output
121
-
122
- A table is plain text, two spaces between columns. `--fields` picks the
123
- columns and lays a table out in the flag's order; a single `get` reads down
124
- the page in the record's own order. A cell is one line: `-` for null and
125
- empty, a list joined with `, `, an object — an association included — as its
126
- JSON, and anything past 40 characters cut with `…`.
127
-
128
- `--format json` prints the body exactly as the server sent it and ignores
129
- `--fields`; `table` is the default. `--verbose` logs each request and
130
- response line to stderr, so `--format json --verbose | jq` still works.
131
-
132
- ### Writing fields
133
-
134
- Fields are one JSON object, given through `--data` (`-d`): the JSON itself,
135
- `@path` to read it from a file, or `-` to read stdin. Every key is a field as
136
- `schema <resource>` lists them. JSON carries its own types, so nothing is
137
- converted — a value is sent as written. Before sending, the CLI reads the
138
- resource's form view: for the key the body nests under, for the `file` refusal
139
- below, and for the two values the wire wants in another form. What each field
140
- takes:
141
-
142
- | The view says | You write | Sent |
143
- | ------------------------------------------------- | ----------------------------------------------- | --------------------------------------- |
144
- | `shape: array` | `"roles": ["editor", "reviewer"]` | as written; `null` sends `[]` |
145
- | `shape: hash`, `keys: [latitude, longitude]` | `"coordinates": {"latitude": 44.4, …}` | as written; `null` sends every key null |
146
- | polymorphic `belongs_to`, two scalars | `"reactable_type": "Post", "reactable_id": 7` | as written |
147
- | `type: key_value` or `type: code` | `"settings": {"theme": "dark"}` | the object as a JSON string |
148
- | `type: tags` | `"skills": "ruby,rails"` | the string; the field splits it |
149
- | anything else | `"active": false`, `"birth_date": "1985-03-04"` | as written; the app casts |
150
-
151
- Refused with exit `2`, before any request: `--data` that is not valid JSON or
152
- not one object, and an `@path` that cannot be read. Refused once the view is
153
- read, before the write: a `file` or `files` field, which needs a multipart
154
- upload the CLI does not send. Nothing else is checked against the view: the API
155
- validates a write, so an empty object, a field the view does not list or a
156
- value outside a field's `options` goes out as written and the server answers.
157
- An empty object is a `400`. A value outside a field's `options` is written as
158
- given, or a `500` where a Rails enum raises on it, and a misspelled field name
159
- or a value of the wrong shape is mostly dropped by the app's strong params
160
- without a word — the write exits `0` with that field unchanged — until
161
- [AVO-1804](https://linear.app/avo-hq/issue/AVO-1804) makes the API answer those
162
- too. Read the record back when it matters.
163
- [docs/field-types.md](docs/field-types.md) is the full table, type by type.
164
-
165
- Quote the object in single quotes: the shell reads the line first, and an
166
- unquoted `{"a":1,"b":2}` is a brace expansion that silently splits in two.
167
- When the quoting gets in the way — a value holding a quote, or an object too
168
- long for one line — write the JSON to a file and pass `--data @user.json`, or
169
- pipe it in with `--data -`.
170
-
171
- A `code` field takes its JSON as a string. Whether the field parses that string
172
- on the way in is a write transform the view does not publish — it describes the
173
- value a write sends, not what the field does with it afterwards — so over a
174
- JSON column the string lands double-encoded unless the resource declares
175
- `pretty_generated: true`. Setting such a field replaces the whole column;
176
- nothing is merged.
177
-
178
- `schema users` lists the fields a create may send, each with every key the
179
- view publishes for it: its `type`, whether it is `required`, its `shape`, and
180
- `options` or `keys` where it has them. `--view update` does the same for an
181
- update, and `--view index` or `--view show` list what a record comes back
182
- with, as `id` and `type`. The views are named by the API action they describe,
183
- the same words `schema` shows under `entitlements`. The answer is always for
184
- one view, never a merge of several, and each view needs the entitlement of the
185
- request it describes: a read-only token gets `index` and `show`, and on
186
- `create` and `update` a `403` the CLI relays like any other. A view the API
187
- does not serve is refused by the API, which names the ones it has. A
188
- `belongs_to` is listed by the key the body sets it through, `team_id`.
189
-
190
- `delete` has no body, so it reads no view and sends straight away. The server
191
- answers every request, and a refusal comes back as a `403` whose `reason` says
192
- which side to widen, `token_entitlement` or `policy`. The CLI names the request
193
- it made and relays the reason as it came:
194
-
195
- ```
196
- › Error: Forbidden: DELETE /api/resources/v1/users/5 was refused (reason: token_entitlement).
197
- ```
198
-
199
- `delete` asks for no confirmation: an agent or a CI job has no terminal to
200
- answer a prompt on, and the line already names the record. A confirmation
201
- step is parked for after v1.
202
-
203
- ### Errors
204
-
205
- Every error is one sentence naming what to change, on stderr. A `404` is the
206
- one message whatever the cause — a mistyped resource, a missing id, an app
207
- without `avo-api` mounted or licensed — because the API does not yet say which,
208
- and the CLI does not guess. `schema` lists the names this token may reach:
209
-
210
- ```
211
- › Error: Not found: http://localhost:3030/api/resources/v1/user.
212
- › Check the resource name and the API version, and that the app has avo-api mounted and licensed.
213
- ```
214
-
215
- A line that names no command is answered with the one grammar there is, and a
216
- correction above it when the typo is close to something real:
217
-
218
- ```
219
- › Error: command lst users not found
220
- › Did you mean: avo list?
221
- › Usage: avo <verb> <resource> [id] [flags]
222
- ```
223
-
224
- A `422` lists what the app refused, one line per field:
225
-
226
- ```
227
- › Error: Failed to create User
228
- › name: can't be blank
229
- › email: has already been taken
49
+ avo <verb> <resource> [id] [flags]
230
50
  ```
231
51
 
232
- A wrong secret, an expired token and a revoked one all read the same
233
- `Unauthorized`: the server does not say which, since saying would confirm a
234
- guessed secret.
235
-
236
- A redirect is reported and never followed, since the token would not survive
237
- it. It is usually `http://` on an app that forces `https://`:
238
-
239
- ```
240
- › Error: http://app.example.com redirected to https://app.example.com/api/resources/v1/_schema. Use the address it points to as the host.
52
+ ```sh
53
+ avo schema users # the fields a create sends
54
+ avo list users --sort name --dir desc # a page of records
55
+ avo get users 5 --format json # one record, as the server sent it
56
+ avo create users --data '{"name": "Ada", "email": "ada@example.com"}'
57
+ avo update users 5 --data '{"name": "Ada King"}'
58
+ avo delete users 5 # no confirmation
241
59
  ```
242
60
 
243
- ### Exit codes
244
-
245
- | Code | Meaning |
246
- | ----- | -------------------------------------------------------------------------------------------- |
247
- | `0` | Success |
248
- | `1` | The request failed: refused (400, 401, 403, 404, 422, 5xx), redirected, unreachable, timeout |
249
- | `2` | Usage: the line or the setup was wrong, and nothing was written |
250
- | `130` | Interrupted: Ctrl-C at an `avo login` prompt, with nothing saved |
251
-
252
- Exit `1` also covers an answer that is not avo-api's: a `200` without the
253
- `records`, `record` or schema the endpoint promises. Exit `2` is an unknown
254
- command, a bad flag, `--view` with no resource, an empty id, a missing host or
255
- token, a host without `http://` or `https://` or with a username and password
256
- in it, a token holding a character a header cannot carry, `--data` that is not
257
- one JSON object or names a file that cannot be read, and a `file` field. It is
258
- also a saved login that cannot be read, written or parsed, `avo login` with a
259
- value left to ask for and no terminal to ask on, and the installed gem finding
260
- no Node, or one older than 18.12.1.
61
+ Field formats, errors and exit codes are in the
62
+ [docs](https://docs.avohq.io/4.0/cli.html#send-the-right-value-for-a-field).
261
63
 
262
64
  <!-- commands -->
263
65
  * [`avo create RESOURCE`](#avo-create-resource)
@@ -478,14 +280,15 @@ EXAMPLES
478
280
 
479
281
  ## `avo logout`
480
282
 
481
- Delete the saved host and token. AVO_API_HOST and AVO_API_TOKEN set in the environment are not affected
283
+ Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep working
482
284
 
483
285
  ```
484
286
  USAGE
485
287
  $ avo logout
486
288
 
487
289
  DESCRIPTION
488
- Delete the saved host and token. AVO_API_HOST and AVO_API_TOKEN set in the environment are not affected
290
+ Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep
291
+ working
489
292
 
490
293
  EXAMPLES
491
294
  $ avo logout
@@ -504,8 +307,8 @@ ARGUMENTS
504
307
  [RESOURCE] A resource by its route key; with none, the resources the API serves are listed
505
308
 
506
309
  FLAGS
507
- --view=<value> Which view's fields: create/update list what a write may send, with required; index/show what a record
508
- reads back (default: create)
310
+ --view=<value> Which view's fields: create/update list what a write may send, with their field_options; index/show
311
+ what a record reads back (default: create)
509
312
 
510
313
  CONNECTION FLAGS
511
314
  --api-version=<value> [env: AVO_API_VERSION] API version segment of the URL; defaults to the one `avo login` saved,
@@ -578,35 +381,3 @@ EXAMPLES
578
381
  $ avo update users 5 --data '{"name": "Ada"}' --format json
579
382
  ```
580
383
  <!-- commandsstop -->
581
-
582
- ## Development
583
-
584
- ```sh
585
- cd gems/avo-cli
586
- node --version # 22.13 or newer, per `.node-version`
587
-
588
- yarn build # compile src/ to dist/
589
- yarn test # mocha, then typecheck and eslint (posttest)
590
- yarn typecheck # tsc over src/ and test/, writing nothing
591
- yarn lint
592
- ```
593
-
594
- Narrower runs, when the whole suite is more than you need:
595
-
596
- ```sh
597
- yarn mocha test/commands/list.test.ts # one file
598
- yarn mocha "test/**/*.test.ts" --grep "404" # every test whose name matches
599
- ```
600
-
601
- Tests never reach the network: HTTP is stubbed with undici's `MockAgent`,
602
- which Node's built-in `fetch` honours (see `test/http.test.ts`).
603
-
604
- The command reference above is generated by `oclif readme` and regenerated on
605
- `yarn version` and on pack. After changing a command's description, flags or
606
- examples, run `yarn build && yarn oclif readme --no-source-links` and commit
607
- the result. The flag matters: without it the reference links into the private
608
- workspace repository, which `test/readme.test.ts` refuses.
609
-
610
- To run the CLI against a real app, command by command, with the expected
611
- output and every way a line can fail, follow
612
- [docs/manual-testing.md](docs/manual-testing.md).
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Avo
4
4
  module Cli
5
- VERSION = "4.2.0.beta.3"
5
+ VERSION = "4.2.0.beta.4"
6
6
  end
7
7
  end
data/payload/README.md CHANGED
@@ -1,263 +1,65 @@
1
1
  # avo-cli
2
2
 
3
3
  Command line client for the [Avo REST API](https://docs.avohq.io/4.0/rest-api.html).
4
- It talks to any Rails app running `avo-api`; nothing is installed in the app.
4
+ It talks to any Rails app running `avo-api` over HTTP; nothing is installed in
5
+ the app. A table per command for a person, the raw JSON body under
6
+ `--format json` for a script or an agent.
5
7
 
6
- Status: every command works against any app running `avo-api`, and the docs
7
- site describes them. It ships as a Ruby gem that runs the built client on the
8
- machine's Node ([docs/install.md](docs/install.md)); the gem builds and installs
9
- locally and has not been released yet. What is left is in
10
- [docs/plans/v1-todo.md](docs/plans/v1-todo.md).
8
+ The full guide is at [docs.avohq.io/4.0/cli](https://docs.avohq.io/4.0/cli.html).
11
9
 
12
10
  ## Requirements
13
11
 
14
- - Running the CLI: Node 18.12.1 or newer. 18.0 fails at startup on a missing
15
- `node:fs/promises` export. The installed gem also needs Ruby 3.0 or newer,
16
- for the `avo` executable that finds Node and hands the line over.
17
- - Developing it: Node 22.13 or newer (`.node-version` pins it). The lint toolchain
18
- (`@typescript-eslint` → `eslint-visitor-keys@5`) refuses anything older.
19
- - yarn
12
+ - Ruby 3.0 or newer, for the `avo` executable.
13
+ - Node 18.12.1 or newer on your `PATH`, or named by `AVO_CLI_NODE`. Node is
14
+ not bundled.
15
+ - An app running `avo-api`, reachable over HTTP, and an
16
+ [API token](https://docs.avohq.io/4.0/rest-api.html#create-a-token) created
17
+ in that app.
20
18
 
21
- ## Install (development)
19
+ ## Install
22
20
 
23
21
  ```sh
24
- cd gems/avo-cli
25
- yarn install
26
- ./bin/dev.js --version
22
+ gem install avo-cli
23
+ avo --version
27
24
  ```
28
25
 
29
- `./bin/dev.js` runs the TypeScript sources directly through tsx.
30
- `./bin/run.js` runs the compiled `dist/` after `yarn build`. The installed gem's
31
- `avo` runs that same `run.js`, from the `payload/` the gem carries. To build the
32
- gem and install it locally, see [docs/distribution.md](docs/distribution.md) §6.
33
-
34
- ## Configuration
26
+ ## Connect
35
27
 
36
28
  Log in once and every later command knows the app:
37
29
 
38
30
  ```sh
39
- avo login # asks for the host, the token and the API version (Enter for v1), checks them, saves them
40
- avo schema # works in any terminal from here on
31
+ avo login # asks for the API base URL, the token and the API version, checks them, saves them
32
+ avo schema # what the token may reach
41
33
  avo logout # deletes what was saved
42
34
  ```
43
35
 
44
- `avo login` saves to `~/.config/avo/config.json` (`$XDG_CONFIG_HOME/avo` when
45
- set, `%LOCALAPPDATA%\avo` on Windows), readable by your user only. The token is
46
- stored as plain text, like `~/.bundle/config`, so use a token you can revoke.
47
-
48
- The host is the API base URL: the address `avo-api` answers on, mount path
49
- included, exactly what you would paste into Postman before `/resources/...`.
50
- An app that mounts it the usual way is `http://localhost:3000/api`; one that
51
- mounts it elsewhere, say `mount_avo_api at: "avo"`, is
52
- `http://localhost:3000/avo`. The CLI appends only `/resources/<version>/<path>`
53
- and guesses no mount, so the address that works in a browser works here.
54
-
55
- For CI and scripts, skip `login` and set the variables. A flag wins over its
56
- variable, and either wins over what was saved. The saved host and token are
57
- used only together: name a host or a token yourself and the saved pair is left
58
- out, so a saved token never reaches another host. The saved API version goes
59
- with them: `avo login --api-version v2` once, and later commands use `v2`.
36
+ The API base URL is the address `avo-api` answers on, mount path included,
37
+ e.g. `http://localhost:3000/api`. The token is saved as plain text in
38
+ `~/.config/avo/config.json`, so use one you can revoke.
60
39
 
61
- | Variable | Flag | Default | Purpose |
62
- | ----------------- | --------------- | ------- | ------------------------------------------------------------------- |
63
- | `AVO_API_HOST` | `--host` | none | API base URL, mount path included, e.g. `http://localhost:3000/api` |
64
- | `AVO_API_TOKEN` | `--token` | none | API token secret, sent as `Authorization: Bearer` |
65
- | `AVO_API_VERSION` | `--api-version` | `v1` | API version segment of the URL, saved by `login` |
40
+ For CI and scripts, skip `login` and set `AVO_API_HOST`, `AVO_API_TOKEN` and
41
+ `AVO_API_VERSION`, or pass `--host`, `--token` and `--api-version`. See
42
+ [Connect without logging in](https://docs.avohq.io/4.0/cli.html#connect-without-logging-in).
66
43
 
67
- ## Commands
68
-
69
- The gem is not released yet, so there is no `avo` on anyone's PATH: every
70
- example here runs the source through `./bin/dev.js`. The generated reference
71
- further down prints `avo`, the released name. These examples are swept to it
72
- when the gem ships.
73
-
74
- ```sh
75
- ./bin/dev.js schema # what the token may reach, as a table
76
- ./bin/dev.js schema --format json # the _schema body, byte for byte
77
- ./bin/dev.js schema users # the fields a create sends, with required
78
- ./bin/dev.js schema users --view show # the fields a record reads back
79
- ./bin/dev.js list users # first page, one row per record
80
- ./bin/dev.js list users --page 2 --per-page 10 # paging
81
- ./bin/dev.js list users --sort name --dir desc # sorting, server side
82
- ./bin/dev.js list users --fields id,name,email # pick the columns
83
- ./bin/dev.js get users 5 # one record, field by field
84
- ./bin/dev.js get users 5 --format json # the body, byte for byte
85
- ./bin/dev.js get users 5 --verbose # request and response lines on stderr
86
- ./bin/dev.js create users --data '{"name": "Ada", "email": "ada@example.com"}'
87
- ./bin/dev.js update users 5 --data '{"name": "Ada King", "bio": null}' # null clears
88
- ./bin/dev.js create users --data @user.json # the object from a file
89
- ./bin/dev.js update users 5 --data - < user.json # the object from stdin
90
- ./bin/dev.js delete users 5 # no confirmation, see below
91
- ./bin/dev.js login # save the host, token and API version
92
- ./bin/dev.js logout # delete what login saved
93
- ```
94
-
95
- ### Grammar
44
+ ## Usage
96
45
 
97
46
  Every line has the same shape:
98
47
 
99
48
  ```
100
- ./bin/dev.js <verb> <resource> [id] [flags]
101
- ```
102
-
103
- There are six verbs: `schema`, `list`, `get`, `create`, `update` and `delete`.
104
- The resource is always an argument, never a command, because route keys come
105
- from the app being talked to and only after an HTTP call. `login` and `logout`
106
- are the two commands outside the grammar: they take no resource
107
- ([Configuration](#configuration)).
108
-
109
- `./bin/dev.js --help` lists the six verbs, with `login`, `logout` and `help`
110
- beside them, and `./bin/dev.js get --help` shows one. A line that names no
111
- command prints the usage above, with a did-you-mean when the typo is close to
112
- something real.
113
-
114
- Each verb has a one-letter shortcut — `s l g c u d`, in the order listed above
115
- — in the same slot, so `./bin/dev.js l users` is `list users`. They are hidden
116
- aliases: help, the reference below and the error messages all keep to the full
117
- spelling, and nothing else about the grammar bends. `./bin/dev.js users l` is
118
- refused like any other line that puts the resource first.
119
-
120
- ### Output
121
-
122
- A table is plain text, two spaces between columns. `--fields` picks the
123
- columns and lays a table out in the flag's order; a single `get` reads down
124
- the page in the record's own order. A cell is one line: `-` for null and
125
- empty, a list joined with `, `, an object — an association included — as its
126
- JSON, and anything past 40 characters cut with `…`.
127
-
128
- `--format json` prints the body exactly as the server sent it and ignores
129
- `--fields`; `table` is the default. `--verbose` logs each request and
130
- response line to stderr, so `--format json --verbose | jq` still works.
131
-
132
- ### Writing fields
133
-
134
- Fields are one JSON object, given through `--data` (`-d`): the JSON itself,
135
- `@path` to read it from a file, or `-` to read stdin. Every key is a field as
136
- `schema <resource>` lists them. JSON carries its own types, so nothing is
137
- converted — a value is sent as written. Before sending, the CLI reads the
138
- resource's form view: for the key the body nests under, for the `file` refusal
139
- below, and for the two values the wire wants in another form. What each field
140
- takes:
141
-
142
- | The view says | You write | Sent |
143
- | ------------------------------------------------- | ----------------------------------------------- | --------------------------------------- |
144
- | `shape: array` | `"roles": ["editor", "reviewer"]` | as written; `null` sends `[]` |
145
- | `shape: hash`, `keys: [latitude, longitude]` | `"coordinates": {"latitude": 44.4, …}` | as written; `null` sends every key null |
146
- | polymorphic `belongs_to`, two scalars | `"reactable_type": "Post", "reactable_id": 7` | as written |
147
- | `type: key_value` or `type: code` | `"settings": {"theme": "dark"}` | the object as a JSON string |
148
- | `type: tags` | `"skills": "ruby,rails"` | the string; the field splits it |
149
- | anything else | `"active": false`, `"birth_date": "1985-03-04"` | as written; the app casts |
150
-
151
- Refused with exit `2`, before any request: `--data` that is not valid JSON or
152
- not one object, and an `@path` that cannot be read. Refused once the view is
153
- read, before the write: a `file` or `files` field, which needs a multipart
154
- upload the CLI does not send. Nothing else is checked against the view: the API
155
- validates a write, so an empty object, a field the view does not list or a
156
- value outside a field's `options` goes out as written and the server answers.
157
- An empty object is a `400`. A value outside a field's `options` is written as
158
- given, or a `500` where a Rails enum raises on it, and a misspelled field name
159
- or a value of the wrong shape is mostly dropped by the app's strong params
160
- without a word — the write exits `0` with that field unchanged — until
161
- [AVO-1804](https://linear.app/avo-hq/issue/AVO-1804) makes the API answer those
162
- too. Read the record back when it matters.
163
- [docs/field-types.md](docs/field-types.md) is the full table, type by type.
164
-
165
- Quote the object in single quotes: the shell reads the line first, and an
166
- unquoted `{"a":1,"b":2}` is a brace expansion that silently splits in two.
167
- When the quoting gets in the way — a value holding a quote, or an object too
168
- long for one line — write the JSON to a file and pass `--data @user.json`, or
169
- pipe it in with `--data -`.
170
-
171
- A `code` field takes its JSON as a string. Whether the field parses that string
172
- on the way in is a write transform the view does not publish — it describes the
173
- value a write sends, not what the field does with it afterwards — so over a
174
- JSON column the string lands double-encoded unless the resource declares
175
- `pretty_generated: true`. Setting such a field replaces the whole column;
176
- nothing is merged.
177
-
178
- `schema users` lists the fields a create may send, each with every key the
179
- view publishes for it: its `type`, whether it is `required`, its `shape`, and
180
- `options` or `keys` where it has them. `--view update` does the same for an
181
- update, and `--view index` or `--view show` list what a record comes back
182
- with, as `id` and `type`. The views are named by the API action they describe,
183
- the same words `schema` shows under `entitlements`. The answer is always for
184
- one view, never a merge of several, and each view needs the entitlement of the
185
- request it describes: a read-only token gets `index` and `show`, and on
186
- `create` and `update` a `403` the CLI relays like any other. A view the API
187
- does not serve is refused by the API, which names the ones it has. A
188
- `belongs_to` is listed by the key the body sets it through, `team_id`.
189
-
190
- `delete` has no body, so it reads no view and sends straight away. The server
191
- answers every request, and a refusal comes back as a `403` whose `reason` says
192
- which side to widen, `token_entitlement` or `policy`. The CLI names the request
193
- it made and relays the reason as it came:
194
-
195
- ```
196
- › Error: Forbidden: DELETE /api/resources/v1/users/5 was refused (reason: token_entitlement).
197
- ```
198
-
199
- `delete` asks for no confirmation: an agent or a CI job has no terminal to
200
- answer a prompt on, and the line already names the record. A confirmation
201
- step is parked for after v1.
202
-
203
- ### Errors
204
-
205
- Every error is one sentence naming what to change, on stderr. A `404` is the
206
- one message whatever the cause — a mistyped resource, a missing id, an app
207
- without `avo-api` mounted or licensed — because the API does not yet say which,
208
- and the CLI does not guess. `schema` lists the names this token may reach:
209
-
210
- ```
211
- › Error: Not found: http://localhost:3030/api/resources/v1/user.
212
- › Check the resource name and the API version, and that the app has avo-api mounted and licensed.
213
- ```
214
-
215
- A line that names no command is answered with the one grammar there is, and a
216
- correction above it when the typo is close to something real:
217
-
218
- ```
219
- › Error: command lst users not found
220
- › Did you mean: avo list?
221
- › Usage: avo <verb> <resource> [id] [flags]
222
- ```
223
-
224
- A `422` lists what the app refused, one line per field:
225
-
226
- ```
227
- › Error: Failed to create User
228
- › name: can't be blank
229
- › email: has already been taken
49
+ avo <verb> <resource> [id] [flags]
230
50
  ```
231
51
 
232
- A wrong secret, an expired token and a revoked one all read the same
233
- `Unauthorized`: the server does not say which, since saying would confirm a
234
- guessed secret.
235
-
236
- A redirect is reported and never followed, since the token would not survive
237
- it. It is usually `http://` on an app that forces `https://`:
238
-
239
- ```
240
- › Error: http://app.example.com redirected to https://app.example.com/api/resources/v1/_schema. Use the address it points to as the host.
52
+ ```sh
53
+ avo schema users # the fields a create sends
54
+ avo list users --sort name --dir desc # a page of records
55
+ avo get users 5 --format json # one record, as the server sent it
56
+ avo create users --data '{"name": "Ada", "email": "ada@example.com"}'
57
+ avo update users 5 --data '{"name": "Ada King"}'
58
+ avo delete users 5 # no confirmation
241
59
  ```
242
60
 
243
- ### Exit codes
244
-
245
- | Code | Meaning |
246
- | ----- | -------------------------------------------------------------------------------------------- |
247
- | `0` | Success |
248
- | `1` | The request failed: refused (400, 401, 403, 404, 422, 5xx), redirected, unreachable, timeout |
249
- | `2` | Usage: the line or the setup was wrong, and nothing was written |
250
- | `130` | Interrupted: Ctrl-C at an `avo login` prompt, with nothing saved |
251
-
252
- Exit `1` also covers an answer that is not avo-api's: a `200` without the
253
- `records`, `record` or schema the endpoint promises. Exit `2` is an unknown
254
- command, a bad flag, `--view` with no resource, an empty id, a missing host or
255
- token, a host without `http://` or `https://` or with a username and password
256
- in it, a token holding a character a header cannot carry, `--data` that is not
257
- one JSON object or names a file that cannot be read, and a `file` field. It is
258
- also a saved login that cannot be read, written or parsed, `avo login` with a
259
- value left to ask for and no terminal to ask on, and the installed gem finding
260
- no Node, or one older than 18.12.1.
61
+ Field formats, errors and exit codes are in the
62
+ [docs](https://docs.avohq.io/4.0/cli.html#send-the-right-value-for-a-field).
261
63
 
262
64
  <!-- commands -->
263
65
  * [`avo create RESOURCE`](#avo-create-resource)
@@ -478,14 +280,15 @@ EXAMPLES
478
280
 
479
281
  ## `avo logout`
480
282
 
481
- Delete the saved host and token. AVO_API_HOST and AVO_API_TOKEN set in the environment are not affected
283
+ Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep working
482
284
 
483
285
  ```
484
286
  USAGE
485
287
  $ avo logout
486
288
 
487
289
  DESCRIPTION
488
- Delete the saved host and token. AVO_API_HOST and AVO_API_TOKEN set in the environment are not affected
290
+ Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep
291
+ working
489
292
 
490
293
  EXAMPLES
491
294
  $ avo logout
@@ -504,8 +307,8 @@ ARGUMENTS
504
307
  [RESOURCE] A resource by its route key; with none, the resources the API serves are listed
505
308
 
506
309
  FLAGS
507
- --view=<value> Which view's fields: create/update list what a write may send, with required; index/show what a record
508
- reads back (default: create)
310
+ --view=<value> Which view's fields: create/update list what a write may send, with their field_options; index/show
311
+ what a record reads back (default: create)
509
312
 
510
313
  CONNECTION FLAGS
511
314
  --api-version=<value> [env: AVO_API_VERSION] API version segment of the URL; defaults to the one `avo login` saved,
@@ -578,35 +381,3 @@ EXAMPLES
578
381
  $ avo update users 5 --data '{"name": "Ada"}' --format json
579
382
  ```
580
383
  <!-- commandsstop -->
581
-
582
- ## Development
583
-
584
- ```sh
585
- cd gems/avo-cli
586
- node --version # 22.13 or newer, per `.node-version`
587
-
588
- yarn build # compile src/ to dist/
589
- yarn test # mocha, then typecheck and eslint (posttest)
590
- yarn typecheck # tsc over src/ and test/, writing nothing
591
- yarn lint
592
- ```
593
-
594
- Narrower runs, when the whole suite is more than you need:
595
-
596
- ```sh
597
- yarn mocha test/commands/list.test.ts # one file
598
- yarn mocha "test/**/*.test.ts" --grep "404" # every test whose name matches
599
- ```
600
-
601
- Tests never reach the network: HTTP is stubbed with undici's `MockAgent`,
602
- which Node's built-in `fetch` honours (see `test/http.test.ts`).
603
-
604
- The command reference above is generated by `oclif readme` and regenerated on
605
- `yarn version` and on pack. After changing a command's description, flags or
606
- examples, run `yarn build && yarn oclif readme --no-source-links` and commit
607
- the result. The flag matters: without it the reference links into the private
608
- workspace repository, which `test/readme.test.ts` refuses.
609
-
610
- To run the CLI against a real app, command by command, with the expected
611
- output and every way a line can fail, follow
612
- [docs/manual-testing.md](docs/manual-testing.md).
@@ -33,7 +33,7 @@ export default class List extends RecordCommand {
33
33
  if (this.printedJson(flags, response))
34
34
  return;
35
35
  const { pagination, records } = parseRecords(response);
36
- this.log(records.length === 0 ? `No ${args.resource} records.` : renderTable(records, flags.fields));
36
+ this.log(records.length === 0 ? `No ${args.resource} records.` : renderTable(records, { columns: flags.fields }));
37
37
  if (typeof pagination?.page === 'number') {
38
38
  this.log(`Page ${pagination.page} of ${pagination.total_pages} (${pagination.total_count} records)`);
39
39
  }
@@ -2,7 +2,7 @@ import { Command } from '@oclif/core';
2
2
  import { fileIn, remove } from '../store.js';
3
3
  // Not a `BaseCommand`: it makes no request, so the connection flags would be on `--help` doing nothing.
4
4
  export default class Logout extends Command {
5
- static description = 'Delete the saved host and token. AVO_API_HOST and AVO_API_TOKEN set in the environment are not affected';
5
+ static description = 'Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep working';
6
6
  static examples = ['<%= config.bin %> logout'];
7
7
  async run() {
8
8
  // Nothing to read from it: parsing is what refuses a flag this command does not have, instead of ignoring it.
@@ -22,7 +22,7 @@ export default class Schema extends BaseCommand {
22
22
  static flags = {
23
23
  // No `options`: the view names are the API's to know, not this flag's.
24
24
  view: Flags.string({
25
- description: `Which view's fields: create/update list what a write may send, with required; index/show what a record reads back (default: ${DEFAULT_VIEW})`,
25
+ description: `Which view's fields: create/update list what a write may send, with their field_options; index/show what a record reads back (default: ${DEFAULT_VIEW})`,
26
26
  }),
27
27
  };
28
28
  static hiddenAliases = ['s'];
@@ -43,7 +43,13 @@ export default class Schema extends BaseCommand {
43
43
  if (this.printedJson(flags, response))
44
44
  return;
45
45
  const { fields } = schema;
46
- this.log(fields.length === 0 ? `No fields on the ${asked} view of ${resource}.` : renderTable(fields));
46
+ // Headed `field id`, `field type`, `field options`, as a person reads them; `--format json` keeps the keys.
47
+ // Never cut: the CLI checks nothing against the view (decisions.md), so a truncated cell is the only place a
48
+ // reader could have seen the `options` before sending.
49
+ const rows = fields.map((field) => Object.fromEntries(Object.entries(field).map(([key, value]) => [key.replaceAll('_', ' '), value])));
50
+ this.log(fields.length === 0
51
+ ? `No fields on the ${asked} view of ${resource}.`
52
+ : renderTable(rows, { maxCellWidth: Infinity }));
47
53
  }
48
54
  async listResources(client, flags) {
49
55
  const { response, schema } = await loadSchema(client);
@@ -53,6 +59,6 @@ export default class Schema extends BaseCommand {
53
59
  this.log(`No resources: nothing API ${schema.api_version} serves is within this token's entitlements.`);
54
60
  return;
55
61
  }
56
- this.log(renderTable(schema.resources, ['route_key', 'name', 'entitlements']));
62
+ this.log(renderTable(schema.resources, { columns: ['route_key', 'name', 'entitlements'] }));
57
63
  }
58
64
  }
@@ -1,9 +1,10 @@
1
1
  import { CliError } from './errors.js';
2
2
  // A value is sent as given and the server validates it: a second copy of the API's rules here would refuse a write a
3
- // newer server accepts. The schema is read only for a file, and for a value whose wire form differs from its JSON.
3
+ // newer server accepts. The view's `field_type` is read only for a file, and for the two types that take their JSON
4
+ // as a string.
4
5
  export function buildPayload(data, fields) {
5
6
  const payload = {};
6
- const byId = new Map(fields.map((field) => [field.id, field]));
7
+ const byId = new Map(fields.map((field) => [field.field_id, field]));
7
8
  for (const [id, value] of Object.entries(data)) {
8
9
  payload[id] = onTheWire(byId.get(id), value);
9
10
  }
@@ -12,24 +13,17 @@ export function buildPayload(data, fields) {
12
13
  // `key_value` and `code` take their JSON as a string and parse it themselves; sent as an object, a `key_value` column
13
14
  // is wiped.
14
15
  const JSON_AS_STRING = new Set(['code', 'key_value']);
15
- // null clears any field, but strong params silently drop a bare null in place of a list or a hash, so there it becomes
16
- // what does clear: the empty list, or every key null (both held by avo-api's `field_types_update_test.rb`).
16
+ // null is sent as given: the API reads it as "clear" on every field, a list and a two-column location included
17
+ // (held by avo-api's `field_types_update_test.rb`).
17
18
  function onTheWire(field, value) {
18
19
  if (!field)
19
20
  return value;
20
- const { id, keys = [], shape, type } = field;
21
- // Whatever its shape, a file is bytes the JSON body cannot carry.
21
+ const { field_id: id, field_type: type } = field;
22
+ // A file is bytes the JSON body cannot carry.
22
23
  if (type === 'file' || type === 'files') {
23
24
  throw CliError.usage(`${id} is a ${type} field, and a file needs a multipart upload the CLI does not send.`);
24
25
  }
25
- if (value === null) {
26
- if (shape === 'array')
27
- return [];
28
- if (shape === 'hash')
29
- return Object.fromEntries(keys.map((key) => [key, null]));
30
- return value;
31
- }
32
- if (typeof value === 'object' && JSON_AS_STRING.has(type))
26
+ if (value !== null && typeof value === 'object' && JSON_AS_STRING.has(type))
33
27
  return JSON.stringify(value);
34
28
  return value;
35
29
  }
@@ -1,8 +1,10 @@
1
- // Wide enough for an email or a title, narrow enough that five columns still fit a terminal.
1
+ // Wide enough for an email or a title, narrow enough that five columns still fit a terminal. A caller whose cells
2
+ // are the answer (`schema`, where a cut `field_options` loses the `options`) passes its own width.
2
3
  const MAX_CELL_WIDTH = 40;
3
4
  // No box drawing and no colour: it has to read the same in a terminal, in a CI log, and to an agent.
4
- export function renderTable(rows, columns = collectColumns(rows)) {
5
- const cells = rows.map((row) => columns.map((column) => truncate(formatCell(row[column]))));
5
+ export function renderTable(rows, options = {}) {
6
+ const { columns = collectColumns(rows), maxCellWidth = MAX_CELL_WIDTH } = options;
7
+ const cells = rows.map((row) => columns.map((column) => truncate(formatCell(row[column]), maxCellWidth)));
6
8
  // Widened a cell at a time: `Math.max(...column)` can exceed the argument limit on a long page.
7
9
  const widths = columns.map((column) => column.length);
8
10
  for (const row of cells) {
@@ -11,7 +13,10 @@ export function renderTable(rows, columns = collectColumns(rows)) {
11
13
  widths[index] = cell.length;
12
14
  }
13
15
  }
14
- const line = (values) => values.map((value, index) => value.padEnd(widths[index])).join(' ').trimEnd();
16
+ const line = (values) => values
17
+ .map((value, index) => value.padEnd(widths[index]))
18
+ .join(' ')
19
+ .trimEnd();
15
20
  return [line(columns), ...cells.map((cell) => line(cell))].join('\n');
16
21
  }
17
22
  // Nothing becomes `-`, so an empty cell is visibly empty. An object becomes its JSON: the wire carries no type, so an
@@ -35,6 +40,6 @@ function collectColumns(rows) {
35
40
  }
36
41
  return [...columns];
37
42
  }
38
- function truncate(text) {
39
- return text.length > MAX_CELL_WIDTH ? `${text.slice(0, MAX_CELL_WIDTH - 1)}…` : text;
43
+ function truncate(text, maxCellWidth) {
44
+ return text.length > maxCellWidth ? `${text.slice(0, maxCellWidth - 1)}…` : text;
40
45
  }
@@ -551,7 +551,7 @@
551
551
  "logout": {
552
552
  "aliases": [],
553
553
  "args": {},
554
- "description": "Delete the saved host and token. AVO_API_HOST and AVO_API_TOKEN set in the environment are not affected",
554
+ "description": "Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep working",
555
555
  "examples": [
556
556
  "<%= config.bin %> logout"
557
557
  ],
@@ -636,7 +636,7 @@
636
636
  "type": "option"
637
637
  },
638
638
  "view": {
639
- "description": "Which view's fields: create/update list what a write may send, with required; index/show what a record reads back (default: create)",
639
+ "description": "Which view's fields: create/update list what a write may send, with their field_options; index/show what a record reads back (default: create)",
640
640
  "name": "view",
641
641
  "hasDynamicHelp": false,
642
642
  "multiple": false,
@@ -789,5 +789,5 @@
789
789
  ]
790
790
  }
791
791
  },
792
- "version": "4.2.0.beta.3"
792
+ "version": "4.2.0.beta.4"
793
793
  }
data/payload/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "avo-cli",
3
3
  "description": "Command line client for the Avo REST API",
4
- "version": "4.2.0.beta.3",
4
+ "version": "4.2.0.beta.4",
5
5
  "author": "Avo",
6
6
  "bin": {
7
7
  "avo": "./bin/run.js"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avo-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0.beta.3
4
+ version: 4.2.0.beta.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Avo