datagrok-tools 6.1.9 → 6.1.11
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/CHANGELOG.md +15 -0
- package/CLAUDE.md +68 -0
- package/GROK_S.md +361 -0
- package/bin/__tests__/build.test.js +116 -0
- package/bin/__tests__/build.test.ts +101 -0
- package/bin/__tests__/node-dapi.connections.test.js +120 -0
- package/bin/__tests__/node-dapi.connections.test.ts +84 -0
- package/bin/__tests__/node-dapi.groups.test.js +467 -0
- package/bin/__tests__/node-dapi.groups.test.ts +298 -0
- package/bin/__tests__/node-dapi.integration.test.js +406 -0
- package/bin/__tests__/node-dapi.integration.test.ts +447 -0
- package/bin/__tests__/node-dapi.shares.test.js +107 -0
- package/bin/__tests__/node-dapi.shares.test.ts +70 -0
- package/bin/__tests__/node-dapi.users.test.js +86 -0
- package/bin/__tests__/node-dapi.users.test.ts +58 -0
- package/bin/__tests__/server-output.test.js +171 -0
- package/bin/__tests__/server-output.test.ts +133 -0
- package/bin/__tests__/server.test.js +277 -0
- package/bin/__tests__/server.test.ts +197 -0
- package/bin/commands/api.js +13 -3
- package/bin/commands/build.js +1 -1
- package/bin/commands/create.js +8 -5
- package/bin/commands/help.js +80 -4
- package/bin/commands/report.js +231 -36
- package/bin/commands/server.js +670 -0
- package/bin/grok.js +3 -1
- package/bin/utils/node-dapi.js +582 -0
- package/bin/utils/server-client.js +15 -0
- package/bin/utils/server-output.js +127 -0
- package/bin/utils/utils.js +35 -5
- package/package-template/package.json +1 -1
- package/package.json +10 -3
- package/vitest.config.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## 6.1.11 (2026-04-27)
|
|
4
|
+
|
|
5
|
+
* `grok report read <path | instance number>` — normalize a report zip/json into one JSON object on stdout (envelope unwrap, `_meta.json` merge, optional `--extract-screenshot` / `--extract-d42` / `--extract-actions`)
|
|
6
|
+
* `grok report fetch` — single round-trip via the new `/reports/by-number/<number>/zip` endpoint, with fallback to the legacy search-then-download path on 404
|
|
7
|
+
|
|
8
|
+
## 6.1.10 (2026-04-17)
|
|
9
|
+
|
|
10
|
+
* `grok s connections save` — create/update a connection from a JSON file (`--save-credentials` optional)
|
|
11
|
+
* `grok s connections test` — test connectivity by JSON body or by existing id/name
|
|
12
|
+
* `grok s users save` — create/update a user from a JSON file
|
|
13
|
+
* `grok s groups save` — create/update a group from a JSON file (`--save-relations` optional)
|
|
14
|
+
* `grok s shares add` — share an entity with one or more groups (`--access View|Edit`)
|
|
15
|
+
* `grok s shares list` — list who an entity is shared with
|
|
16
|
+
* Tools: Normalize package name to lowercase in grok create, preserve original as friendlyName
|
|
17
|
+
|
|
3
18
|
## 5.1.3 (2026-02-03)
|
|
4
19
|
|
|
5
20
|
* GROK-19407:
|
package/CLAUDE.md
CHANGED
|
@@ -28,6 +28,7 @@ The CLI uses a modular command pattern. Each command is a separate module that:
|
|
|
28
28
|
- `test-all.ts` - Run tests across multiple packages
|
|
29
29
|
- `stress-tests.ts` - Run stress tests (must be run from ApiTests package)
|
|
30
30
|
- `api.ts` - Auto-generate TypeScript wrappers for scripts/queries
|
|
31
|
+
- `server.ts` - Manage and inspect a running Datagrok server (`grok server` / `grok s`)
|
|
31
32
|
- `link.ts` - Link libraries for plugin development
|
|
32
33
|
- `claude.ts` - Launch a Dockerized dev environment with Datagrok + Claude Code
|
|
33
34
|
- `migrate.ts` - Update legacy packages
|
|
@@ -220,6 +221,73 @@ Based on `node:22-bookworm-slim`. Pre-installed:
|
|
|
220
221
|
|
|
221
222
|
See `.devcontainer/PACKAGES_DEV.md` for detailed usage docs, architecture diagram, MCP plugin setup (Jira/GitHub), and troubleshooting.
|
|
222
223
|
|
|
224
|
+
### `grok server` / `grok s` Command
|
|
225
|
+
|
|
226
|
+
Use `grok s` to inspect and debug a running Datagrok server from the CLI — list entities,
|
|
227
|
+
call functions, browse files, or hit any API endpoint. Reads config from `~/.grok/config.yaml`
|
|
228
|
+
(same as `grok publish`). Use `--host <alias|url>` to target a specific server.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Create / update entities (from JSON file — supports both create and update)
|
|
232
|
+
grok s users save --json user.json
|
|
233
|
+
grok s groups save --json group.json --save-relations
|
|
234
|
+
|
|
235
|
+
# Share entities
|
|
236
|
+
grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
|
|
237
|
+
grok s shares list <entity-uuid>
|
|
238
|
+
|
|
239
|
+
# List / inspect entities
|
|
240
|
+
grok s users list
|
|
241
|
+
grok s packages list --filter "MyPlugin" # check if a plugin is published
|
|
242
|
+
grok s connections list --output json
|
|
243
|
+
grok s functions list --filter "Chem" # find registered functions
|
|
244
|
+
grok s connections get <id>
|
|
245
|
+
grok s connections delete <id>
|
|
246
|
+
grok s connections save --json conn.json --save-credentials # create or update
|
|
247
|
+
grok s connections test "JohnDoe:MyConnection" # test by id or name
|
|
248
|
+
grok s connections test --json conn.json # test a connection defined in JSON
|
|
249
|
+
|
|
250
|
+
# Call a server function
|
|
251
|
+
grok s functions run 'Chem:smilesToMw("ccc")'
|
|
252
|
+
grok s functions run 'Pkg:fn({a:5,b:22})'
|
|
253
|
+
|
|
254
|
+
# Browse file storage
|
|
255
|
+
grok s files list "System:AppData" -r # list files recursively
|
|
256
|
+
grok s files list "System:AppData/MyPlugin"
|
|
257
|
+
|
|
258
|
+
# Manage group membership
|
|
259
|
+
grok s groups add-members Admins alice bob # add two users (non-admin)
|
|
260
|
+
grok s groups add-members Admins alice --admin # add as admin (flips if already member)
|
|
261
|
+
grok s groups add-members Admins alice --user # force personal-group lookup
|
|
262
|
+
grok s groups remove-members Admins alice bob # remove members
|
|
263
|
+
grok s groups list-members Admins # all members
|
|
264
|
+
grok s groups list-members Admins --admin # admin members only
|
|
265
|
+
grok s groups list-members Admins --no-admin # non-admin members only
|
|
266
|
+
grok s groups list-memberships alice # groups alice belongs to
|
|
267
|
+
|
|
268
|
+
# Hit any API endpoint directly
|
|
269
|
+
grok s raw GET /api/users/current
|
|
270
|
+
grok s raw GET /api/packages/dev/MyPlugin
|
|
271
|
+
|
|
272
|
+
# Describe entity JSON schema
|
|
273
|
+
grok s describe connections
|
|
274
|
+
|
|
275
|
+
# Target a specific server
|
|
276
|
+
grok s users list --host dev
|
|
277
|
+
grok s users list --host "https://my.datagrok.ai/api"
|
|
278
|
+
|
|
279
|
+
# Output formats: table (default), json, csv, quiet (IDs only)
|
|
280
|
+
grok s packages list --output json
|
|
281
|
+
grok s users list --output quiet | xargs ... # pipe IDs
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Windows Git Bash:** prefix raw paths with `MSYS_NO_PATHCONV=1` to prevent POSIX→Windows
|
|
285
|
+
path conversion: `MSYS_NO_PATHCONV=1 grok s raw GET /api/users/current`
|
|
286
|
+
|
|
287
|
+
**Implementation:** `bin/commands/server.ts`, `bin/utils/node-dapi.ts` (Node.js REST client),
|
|
288
|
+
`bin/utils/server-output.ts` (formatters). The Node.js dapi bypasses the Dart interop layer
|
|
289
|
+
and calls `/public/v1/` endpoints directly — same approach as the Python CLI.
|
|
290
|
+
|
|
223
291
|
## Key Patterns and Conventions
|
|
224
292
|
|
|
225
293
|
### Naming Conventions
|
package/GROK_S.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Managing a Datagrok Server with `grok s`
|
|
2
|
+
|
|
3
|
+
The `grok server` command (alias `grok s`) talks to a running Datagrok instance over
|
|
4
|
+
its public REST API. It is the right tool for **any server-management task** that
|
|
5
|
+
does not require rendering the UI: creating users and groups, sharing entities,
|
|
6
|
+
running functions, browsing files, hitting raw API endpoints, or scripting bulk
|
|
7
|
+
imports.
|
|
8
|
+
|
|
9
|
+
Use this instead of UI automation (Playwright, Selenium) whenever the goal is data
|
|
10
|
+
management — it is faster, idempotent, scriptable, and does not depend on the
|
|
11
|
+
browser or a logged-in session.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
| Task | Command |
|
|
16
|
+
|-------------------------------------------------------------|--------------------------------------------------------|
|
|
17
|
+
| Create / update a user | `grok s users save --json user.json` |
|
|
18
|
+
| Block / unblock a user | `grok s users block <login>` / `users unblock <login>` |
|
|
19
|
+
| Create / update a group | `grok s groups save --json group.json` |
|
|
20
|
+
| Add or remove users in a group | `grok s groups add-members <group> <user>...` |
|
|
21
|
+
| List members of a group | `grok s groups list-members <group>` |
|
|
22
|
+
| List the groups a user belongs to | `grok s groups list-memberships <user>` |
|
|
23
|
+
| Share a connection / query / script / project with a group | `grok s shares add <entity> <group> [--access View\|Edit]` |
|
|
24
|
+
| See who an entity is shared with | `grok s shares list <entity-id>` |
|
|
25
|
+
| Create / update a connection | `grok s connections save --json conn.json` |
|
|
26
|
+
| Test a connection | `grok s connections test <id-or-name>` |
|
|
27
|
+
| Run a registered function | `grok s functions run 'Pkg:fn(arg1,arg2)'` |
|
|
28
|
+
| List functions with rich filters | `grok s functions list --type script --language python --package Chem` |
|
|
29
|
+
| List / browse files in a file share | `grok s files list "System:AppData" -r` |
|
|
30
|
+
| Upload / download a table (CSV) | `grok s tables upload <name> file.csv` / `tables download <name> -O out.csv` |
|
|
31
|
+
| Check whether a package is deployed | `grok s packages list --filter "MyPlugin"` |
|
|
32
|
+
| Hit any undocumented endpoint | `grok s raw GET /api/users/current` |
|
|
33
|
+
| Check server + per-module health | `grok s healthcheck [--module <name>]` |
|
|
34
|
+
| Bulk operations in one round-trip | `grok s batch <entity> <verb> --json items.json` |
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Servers and credentials live in `~/.grok/config.yaml`:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
default: local
|
|
42
|
+
servers:
|
|
43
|
+
local:
|
|
44
|
+
url: http://localhost:8888/api
|
|
45
|
+
key: admin
|
|
46
|
+
dev:
|
|
47
|
+
url: https://dev.datagrok.ai/api
|
|
48
|
+
key: <developer-key>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `grok config --server --alias <name> --server <url> --key <key>` writes a new entry.
|
|
52
|
+
- Add `--default` to make it the active server.
|
|
53
|
+
- Every `grok s ...` command accepts `--host <alias-or-url>` to override the default.
|
|
54
|
+
|
|
55
|
+
## Entity operations
|
|
56
|
+
|
|
57
|
+
### List / get / delete
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
grok s users list # default: table, 50 rows
|
|
61
|
+
grok s users list --filter "login = 'admin'" # smart filter
|
|
62
|
+
grok s connections list --output json
|
|
63
|
+
grok s packages list --filter "name:MyPlugin" # table shows friendlyName
|
|
64
|
+
grok s groups get <id-or-name>
|
|
65
|
+
grok s connections delete <id>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Options that work on every entity:
|
|
69
|
+
|
|
70
|
+
| Flag | Meaning |
|
|
71
|
+
|-----------------------|---------------------------------------------------|
|
|
72
|
+
| `--output table\|json\|csv\|quiet` | Output format; `quiet` prints ids only |
|
|
73
|
+
| `--filter "<expr>"` | Server-side smart filter |
|
|
74
|
+
| `--limit <n>` | Page size (default 50) |
|
|
75
|
+
| `--offset <n>` | Skip first `n` rows |
|
|
76
|
+
| `--host <alias\|url>` | Target a specific server from your config |
|
|
77
|
+
|
|
78
|
+
### Save from JSON
|
|
79
|
+
|
|
80
|
+
`grok s users save` and `grok s groups save` accept the same JSON shape that the
|
|
81
|
+
REST API returns on `get`. The simplest valid bodies:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
// user.json
|
|
85
|
+
{ "#type": "User", "login": "alice.mendel", "firstName": "Alice", "lastName": "Mendel", "status": "active" }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
// group.json
|
|
90
|
+
{ "#type": "UserGroup", "name": "Chemists", "friendlyName": "Chemists" }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
grok s users save --json user.json
|
|
95
|
+
grok s groups save --json group.json --save-relations
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Include an `id` to update an existing entity; omit it to create. To introspect the
|
|
99
|
+
shape of an existing entity, use `grok s <entity> get <id-or-name> --output json`.
|
|
100
|
+
|
|
101
|
+
### Connections
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
grok s connections save --json conn.json --save-credentials
|
|
105
|
+
grok s connections test "MyUser:MyConnection" # by "author:name"
|
|
106
|
+
grok s connections test --json conn.json # test before saving
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
See `public/packages/Chembl/connections/` for connection JSON examples.
|
|
110
|
+
|
|
111
|
+
## Group membership
|
|
112
|
+
|
|
113
|
+
`grok s groups add-members` is idempotent — it resolves each member (by login, name,
|
|
114
|
+
or UUID), compares against the group's current children, and only writes when
|
|
115
|
+
something changes. Results are returned per member with statuses `added`, `updated`,
|
|
116
|
+
`noop`, `not-member`, or `error`.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
grok s groups add-members Chemists alice.mendel bob.curie
|
|
120
|
+
grok s groups add-members Chemists alice.mendel --admin # promote / add as admin
|
|
121
|
+
grok s groups add-members Admins analysts # nest a group inside a group
|
|
122
|
+
grok s groups add-members Chemists alice.mendel --user # force personal-group lookup
|
|
123
|
+
grok s groups remove-members Chemists alice.mendel
|
|
124
|
+
grok s groups list-members Chemists # all members
|
|
125
|
+
grok s groups list-members Chemists --admin # admin members only
|
|
126
|
+
grok s groups list-members Chemists --no-admin # non-admin members only
|
|
127
|
+
grok s groups list-memberships alice.mendel # groups a user belongs to
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
When a name is ambiguous the command prints every matching group and exits non-zero —
|
|
131
|
+
pass a UUID to disambiguate, or add `--user` to restrict lookup to personal groups.
|
|
132
|
+
|
|
133
|
+
## User administration
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
grok s users block alice.mendel # accept login, UUID, or namespace:name
|
|
137
|
+
grok s users unblock alice.mendel
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Both commands resolve the argument to a full user record first, so the server receives
|
|
141
|
+
the entire object (matching the Python client's `grok.users.block(user)`). Blocking
|
|
142
|
+
flips `status` to `Blocked` and terminates active sessions; unblocking restores `Active`.
|
|
143
|
+
|
|
144
|
+
## Sharing entities
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
grok s shares add "MyUser:MyConnection" Chemists,Biologists --access Edit
|
|
148
|
+
grok s shares list <entity-uuid>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The entity argument accepts either a UUID or an `"author:name"` pair. `--access`
|
|
152
|
+
defaults to `View`.
|
|
153
|
+
|
|
154
|
+
## Running functions
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
grok s functions run 'Chem:smilesToMw("CCO")' # positional args
|
|
158
|
+
grok s functions run 'Pkg:fn({smiles:"CCO", radius:2})' # named args
|
|
159
|
+
grok s functions run Pkg:fn --json params.json # big input from a file
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Listing functions with filters
|
|
163
|
+
|
|
164
|
+
`grok s functions list` composes a smart filter from flags so you don't have to hand-roll
|
|
165
|
+
`text=` expressions. All flags are optional and combine with `and`; `--filter` lets you
|
|
166
|
+
add any other smart-filter clause.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
grok s functions list --type script --language python # all python scripts
|
|
170
|
+
grok s functions list --type query --package Chembl # data-queries in one package
|
|
171
|
+
grok s functions list --package PowerPack --type package # package-bundled funcs only
|
|
172
|
+
grok s functions list --language r --limit 20 --output json
|
|
173
|
+
grok s functions list --type script --filter 'name contains "similarity"'
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Flag cheat sheet:
|
|
177
|
+
|
|
178
|
+
| Flag | Values | Notes |
|
|
179
|
+
|--------------|------------------------------------------------------------|-------------------------------------------------------------------|
|
|
180
|
+
| `--type` | `script`, `query` (alias for `data-query`), `function`, `package` | `function` matches both standalone and package-bundled funcs |
|
|
181
|
+
| `--language` | `python`, `r`, `julia`, `nodejs`, `octave`, `grok`, `javascript`, `pyodide` | Filters in the CLI (the server doesn't index Script.language); implies `--type script` when used alone |
|
|
182
|
+
| `--package` | package short name (e.g. `Chem`, `PowerPack`) | Maps to `package.shortName` in the smart filter |
|
|
183
|
+
| `--filter` | any smart-filter expression | Combined with the other flags via `and` |
|
|
184
|
+
| `--limit` / `--offset` | integers | Applied client-side — the public list endpoint returns the full match set |
|
|
185
|
+
|
|
186
|
+
## Files
|
|
187
|
+
|
|
188
|
+
Remote paths are `<connector>/<file-path>`, where `<connector>` is the connection's
|
|
189
|
+
full name — including any namespace — e.g. `System:DemoFiles/smiles.csv`. The
|
|
190
|
+
separator between connector and path is the first `/`; the connector part may
|
|
191
|
+
contain colons (the namespace separator).
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
grok s files list "System:AppData" -r # recursive
|
|
195
|
+
grok s files list "System:AppData/MyPlugin"
|
|
196
|
+
grok s files get "System:AppData/MyPlugin/config.json"
|
|
197
|
+
grok s files put ./smiles.csv "System:DemoFiles/smiles.csv" # upload local file
|
|
198
|
+
grok s files delete "System:AppData/MyPlugin/old.csv"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`files put` streams the file as raw bytes (no base64), so it handles GB-scale uploads
|
|
202
|
+
without blowing up memory. Use `batch files.put` only when you want to bundle an
|
|
203
|
+
upload with other operations in a single round-trip (the batch path base64-encodes
|
|
204
|
+
the `source` before sending).
|
|
205
|
+
|
|
206
|
+
## Tables
|
|
207
|
+
|
|
208
|
+
CSV-level table I/O — the shell counterpart to Python's `grok.tables.upload/download`.
|
|
209
|
+
Unlike `files put`, this registers a proper Datagrok table entity (returns `{ID, ...}`).
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
grok s tables upload MyTable ./data.csv # CSV → table
|
|
213
|
+
grok s tables upload MyTable ./data.csv --output json # get ID and markup back
|
|
214
|
+
grok s tables download MyTable # CSV to stdout (pipe-friendly)
|
|
215
|
+
grok s tables download MyTable -O ./data.csv # CSV to a local file
|
|
216
|
+
grok s tables download <uuid> # UUID or namespace:name both work
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Upload streams raw bytes with `Content-Type: text/csv` so it handles large tables
|
|
220
|
+
without loading the whole file into a JSON envelope. `-O` / `--output-file` avoids
|
|
221
|
+
colliding with the format flag (`--output table|json|csv|quiet`), which still controls
|
|
222
|
+
how the upload result is printed.
|
|
223
|
+
|
|
224
|
+
## Server health
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
grok s healthcheck # full per-module health
|
|
228
|
+
grok s healthcheck --module scripting # filter to one module
|
|
229
|
+
grok s healthcheck --output json # machine-readable
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Hits `GET /public/v1/healthcheck`. Response:
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"status": "ok",
|
|
237
|
+
"server": "https://public.datagrok.ai",
|
|
238
|
+
"version": "1.27",
|
|
239
|
+
"time": "2026-04-19T19:20:00.000Z",
|
|
240
|
+
"services": [
|
|
241
|
+
{ "key": "scripting", "type": "Service", "name": "...", "status": "Running", "started": true, "enabled": true, "time": "..." }
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`services` is the same payload as `/admin/health` (per-`GrokServiceInfo` records).
|
|
247
|
+
Requires a valid dev key (standard `grok s` auth). For an anonymous liveness probe —
|
|
248
|
+
load balancer, k8s readiness — hit `/admin/health` directly; it's on the server's
|
|
249
|
+
unauthenticated allowlist.
|
|
250
|
+
|
|
251
|
+
## Raw API access
|
|
252
|
+
|
|
253
|
+
When no dedicated subcommand exists, fall through to `grok s raw`:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
grok s raw GET /api/users/current
|
|
257
|
+
grok s raw GET /api/packages/dev/MyPlugin
|
|
258
|
+
grok s raw POST /api/admin/reload-settings
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
On **Windows Git Bash**, prefix raw paths with `MSYS_NO_PATHCONV=1` to stop the shell
|
|
262
|
+
from rewriting POSIX paths into Windows paths:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
MSYS_NO_PATHCONV=1 grok s raw GET /api/users/current
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Batch operations
|
|
269
|
+
|
|
270
|
+
Apply the same verb to many items in one round-trip:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Inline args
|
|
274
|
+
grok s batch files delete "System:AppData/old1.txt" "System:AppData/old2.txt"
|
|
275
|
+
|
|
276
|
+
# From a JSON array
|
|
277
|
+
grok s batch users save --json users.json # [{...user1}, {...user2}, ...]
|
|
278
|
+
|
|
279
|
+
# Full workflow manifest — mixed actions, optional transaction / stopOnError
|
|
280
|
+
grok s batch manifest.json
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Manifest shape:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"stopOnError": true,
|
|
288
|
+
"transaction": false,
|
|
289
|
+
"operations": [
|
|
290
|
+
{ "id": "op1", "action": "users.save", "params": {"login": "alice", "firstName": "Alice"} },
|
|
291
|
+
{ "id": "op2", "action": "groups.save", "params": {"name": "Chemists"} }
|
|
292
|
+
]
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
For `files.put`, add `"source": "<local-path>"` and the CLI base64-encodes the file
|
|
297
|
+
into `content` before sending.
|
|
298
|
+
|
|
299
|
+
## Scripting pattern
|
|
300
|
+
|
|
301
|
+
`--output quiet` and `--output json` make `grok s` safe to compose with standard
|
|
302
|
+
shell tooling:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# Pipe IDs
|
|
306
|
+
grok s users list --filter "status = 'active'" --output quiet \
|
|
307
|
+
| xargs -I{} grok s users get {}
|
|
308
|
+
|
|
309
|
+
# Filter with jq
|
|
310
|
+
grok s connections list --output json \
|
|
311
|
+
| jq '.[] | select(.dataSource=="Postgres") | .name'
|
|
312
|
+
|
|
313
|
+
# Generate JSON on the fly
|
|
314
|
+
for login in alice bob carol; do
|
|
315
|
+
printf '{"#type":"User","login":"%s","firstName":"%s","status":"active"}\n' \
|
|
316
|
+
"$login" "${login^}" > user.json
|
|
317
|
+
grok s users save --json user.json
|
|
318
|
+
done
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Worked example: seed users and populate groups
|
|
322
|
+
|
|
323
|
+
Create a group, bulk-create users, and drop each into the right group without touching
|
|
324
|
+
the UI:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# 1. Create the group (idempotent if you include the existing id)
|
|
328
|
+
cat > /tmp/g.json <<'EOF'
|
|
329
|
+
{ "#type": "UserGroup", "name": "Chemists", "friendlyName": "Chemists" }
|
|
330
|
+
EOF
|
|
331
|
+
grok s groups save --json /tmp/g.json
|
|
332
|
+
|
|
333
|
+
# 2. Create 8 users
|
|
334
|
+
for row in \
|
|
335
|
+
"alice.mendeleev:Alice:Mendeleev" \
|
|
336
|
+
"bob.curie:Bob:Curie" \
|
|
337
|
+
"carol.pauling:Carol:Pauling" ; do
|
|
338
|
+
IFS=: read -r login first last <<<"$row"
|
|
339
|
+
cat > /tmp/u.json <<EOF
|
|
340
|
+
{ "#type": "User", "login": "$login", "firstName": "$first", "lastName": "$last", "status": "active" }
|
|
341
|
+
EOF
|
|
342
|
+
grok s users save --json /tmp/u.json --output quiet
|
|
343
|
+
done
|
|
344
|
+
|
|
345
|
+
# 3. Add them all to the group in one call (resolves logins to personal groups)
|
|
346
|
+
grok s groups add-members Chemists alice.mendeleev bob.curie carol.pauling --user
|
|
347
|
+
|
|
348
|
+
# 4. Verify
|
|
349
|
+
grok s groups list-members Chemists --no-admin
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Every subcommand above is idempotent — re-running the whole block is safe.
|
|
353
|
+
|
|
354
|
+
## Implementation notes
|
|
355
|
+
|
|
356
|
+
- Source: `public/tools/bin/commands/server.ts`, `public/tools/bin/utils/node-dapi.ts`.
|
|
357
|
+
- The Node client talks directly to `/public/v1/` — no Dart interop, no browser, no
|
|
358
|
+
logged-in session required. Authentication uses the developer key from the config.
|
|
359
|
+
- If `grok s` is not working, start by running `grok s healthcheck` — it verifies the
|
|
360
|
+
URL, the key, and basic connectivity, and returns per-module status if the server is
|
|
361
|
+
reachable. Fall back to `grok s raw GET /api/users/current` to isolate auth issues.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _build = require("../commands/build");
|
|
5
|
+
(0, _vitest.describe)('getNestedValue', () => {
|
|
6
|
+
(0, _vitest.it)('returns value for a simple key', () => {
|
|
7
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
8
|
+
name: 'Chem'
|
|
9
|
+
}, 'name')).toBe('Chem');
|
|
10
|
+
});
|
|
11
|
+
(0, _vitest.it)('returns value for a nested path', () => {
|
|
12
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
13
|
+
a: {
|
|
14
|
+
b: {
|
|
15
|
+
c: 42
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}, 'a.b.c')).toBe(42);
|
|
19
|
+
});
|
|
20
|
+
(0, _vitest.it)('returns undefined for a missing key', () => {
|
|
21
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
22
|
+
name: 'Chem'
|
|
23
|
+
}, 'version')).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
(0, _vitest.it)('returns undefined when a mid-path segment is null', () => {
|
|
26
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
27
|
+
a: null
|
|
28
|
+
}, 'a.b')).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
(0, _vitest.it)('returns undefined when a mid-path segment is missing', () => {
|
|
31
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
32
|
+
a: {}
|
|
33
|
+
}, 'a.b.c')).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
(0, _vitest.it)('returns undefined for an empty path (splits to empty string key)', () => {
|
|
36
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
37
|
+
x: 1
|
|
38
|
+
}, '')).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
const pkg = overrides => ({
|
|
42
|
+
dir: '/tmp/pkg',
|
|
43
|
+
name: overrides.name ?? 'test-pkg',
|
|
44
|
+
friendlyName: overrides.friendlyName ?? overrides.name ?? 'Test Pkg',
|
|
45
|
+
version: overrides.version ?? '1.0.0',
|
|
46
|
+
packageJson: overrides
|
|
47
|
+
});
|
|
48
|
+
(0, _vitest.describe)('applyFilter', () => {
|
|
49
|
+
const packages = [pkg({
|
|
50
|
+
name: 'Chem',
|
|
51
|
+
version: '1.5.0',
|
|
52
|
+
category: 'Cheminformatics'
|
|
53
|
+
}), pkg({
|
|
54
|
+
name: 'Bio',
|
|
55
|
+
version: '2.0.0',
|
|
56
|
+
category: 'Bioinformatics'
|
|
57
|
+
}), pkg({
|
|
58
|
+
name: 'PowerGrid',
|
|
59
|
+
version: '1.5.0',
|
|
60
|
+
category: 'Viewers'
|
|
61
|
+
})];
|
|
62
|
+
(0, _vitest.it)('returns all packages when filter matches all', () => {
|
|
63
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:.')).toHaveLength(3);
|
|
64
|
+
});
|
|
65
|
+
(0, _vitest.it)('filters by exact name match', () => {
|
|
66
|
+
const result = (0, _build.applyFilter)(packages, 'name:^Chem$');
|
|
67
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
68
|
+
(0, _vitest.expect)(result[0].name).toBe('Chem');
|
|
69
|
+
});
|
|
70
|
+
(0, _vitest.it)('filters by partial name (regex substring)', () => {
|
|
71
|
+
const result = (0, _build.applyFilter)(packages, 'name:Bio');
|
|
72
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
73
|
+
(0, _vitest.expect)(result[0].name).toBe('Bio');
|
|
74
|
+
});
|
|
75
|
+
(0, _vitest.it)('returns empty array when nothing matches', () => {
|
|
76
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:NOMATCH')).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
(0, _vitest.it)('filters by version', () => {
|
|
79
|
+
const result = (0, _build.applyFilter)(packages, 'version:^1\\.5');
|
|
80
|
+
(0, _vitest.expect)(result).toHaveLength(2);
|
|
81
|
+
(0, _vitest.expect)(result.map(p => p.name)).toEqual(_vitest.expect.arrayContaining(['Chem', 'PowerGrid']));
|
|
82
|
+
});
|
|
83
|
+
(0, _vitest.it)('applies && conjunction (both conditions must match)', () => {
|
|
84
|
+
const result = (0, _build.applyFilter)(packages, 'name:Chem && version:1\\.5');
|
|
85
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
86
|
+
(0, _vitest.expect)(result[0].name).toBe('Chem');
|
|
87
|
+
});
|
|
88
|
+
(0, _vitest.it)('returns empty when one part of && conjunction fails', () => {
|
|
89
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:Chem && version:^2')).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
(0, _vitest.it)('filters by nested field', () => {
|
|
92
|
+
const withNested = [pkg({
|
|
93
|
+
name: 'A',
|
|
94
|
+
datagrok: {
|
|
95
|
+
apiVersion: '1.0'
|
|
96
|
+
}
|
|
97
|
+
}), pkg({
|
|
98
|
+
name: 'B',
|
|
99
|
+
datagrok: {
|
|
100
|
+
apiVersion: '2.0'
|
|
101
|
+
}
|
|
102
|
+
})];
|
|
103
|
+
const result = (0, _build.applyFilter)(withNested, 'datagrok.apiVersion:^1');
|
|
104
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
105
|
+
(0, _vitest.expect)(result[0].name).toBe('A');
|
|
106
|
+
});
|
|
107
|
+
(0, _vitest.it)('returns empty when field does not exist', () => {
|
|
108
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'nonexistent:anything')).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
(0, _vitest.it)('treats filter with no colon as field name with match-all pattern', () => {
|
|
111
|
+
// No colon → field = whole string, pattern = /./ (matches any value)
|
|
112
|
+
// The function returns packages where the field exists and is non-empty
|
|
113
|
+
const result = (0, _build.applyFilter)(packages, 'name');
|
|
114
|
+
(0, _vitest.expect)(result).toHaveLength(3);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {describe, it, expect} from 'vitest';
|
|
2
|
+
import {getNestedValue, applyFilter} from '../commands/build';
|
|
3
|
+
|
|
4
|
+
describe('getNestedValue', () => {
|
|
5
|
+
it('returns value for a simple key', () => {
|
|
6
|
+
expect(getNestedValue({name: 'Chem'}, 'name')).toBe('Chem');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns value for a nested path', () => {
|
|
10
|
+
expect(getNestedValue({a: {b: {c: 42}}}, 'a.b.c')).toBe(42);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns undefined for a missing key', () => {
|
|
14
|
+
expect(getNestedValue({name: 'Chem'}, 'version')).toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns undefined when a mid-path segment is null', () => {
|
|
18
|
+
expect(getNestedValue({a: null}, 'a.b')).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns undefined when a mid-path segment is missing', () => {
|
|
22
|
+
expect(getNestedValue({a: {}}, 'a.b.c')).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns undefined for an empty path (splits to empty string key)', () => {
|
|
26
|
+
expect(getNestedValue({x: 1}, '')).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const pkg = (overrides: Record<string, any>) => ({
|
|
31
|
+
dir: '/tmp/pkg',
|
|
32
|
+
name: overrides.name ?? 'test-pkg',
|
|
33
|
+
friendlyName: overrides.friendlyName ?? overrides.name ?? 'Test Pkg',
|
|
34
|
+
version: overrides.version ?? '1.0.0',
|
|
35
|
+
packageJson: overrides,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('applyFilter', () => {
|
|
39
|
+
const packages = [
|
|
40
|
+
pkg({name: 'Chem', version: '1.5.0', category: 'Cheminformatics'}),
|
|
41
|
+
pkg({name: 'Bio', version: '2.0.0', category: 'Bioinformatics'}),
|
|
42
|
+
pkg({name: 'PowerGrid', version: '1.5.0', category: 'Viewers'}),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
it('returns all packages when filter matches all', () => {
|
|
46
|
+
expect(applyFilter(packages, 'name:.')).toHaveLength(3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('filters by exact name match', () => {
|
|
50
|
+
const result = applyFilter(packages, 'name:^Chem$');
|
|
51
|
+
expect(result).toHaveLength(1);
|
|
52
|
+
expect(result[0].name).toBe('Chem');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('filters by partial name (regex substring)', () => {
|
|
56
|
+
const result = applyFilter(packages, 'name:Bio');
|
|
57
|
+
expect(result).toHaveLength(1);
|
|
58
|
+
expect(result[0].name).toBe('Bio');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns empty array when nothing matches', () => {
|
|
62
|
+
expect(applyFilter(packages, 'name:NOMATCH')).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('filters by version', () => {
|
|
66
|
+
const result = applyFilter(packages, 'version:^1\\.5');
|
|
67
|
+
expect(result).toHaveLength(2);
|
|
68
|
+
expect(result.map((p) => p.name)).toEqual(expect.arrayContaining(['Chem', 'PowerGrid']));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('applies && conjunction (both conditions must match)', () => {
|
|
72
|
+
const result = applyFilter(packages, 'name:Chem && version:1\\.5');
|
|
73
|
+
expect(result).toHaveLength(1);
|
|
74
|
+
expect(result[0].name).toBe('Chem');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns empty when one part of && conjunction fails', () => {
|
|
78
|
+
expect(applyFilter(packages, 'name:Chem && version:^2')).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('filters by nested field', () => {
|
|
82
|
+
const withNested = [
|
|
83
|
+
pkg({name: 'A', datagrok: {apiVersion: '1.0'}}),
|
|
84
|
+
pkg({name: 'B', datagrok: {apiVersion: '2.0'}}),
|
|
85
|
+
];
|
|
86
|
+
const result = applyFilter(withNested, 'datagrok.apiVersion:^1');
|
|
87
|
+
expect(result).toHaveLength(1);
|
|
88
|
+
expect(result[0].name).toBe('A');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns empty when field does not exist', () => {
|
|
92
|
+
expect(applyFilter(packages, 'nonexistent:anything')).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('treats filter with no colon as field name with match-all pattern', () => {
|
|
96
|
+
// No colon → field = whole string, pattern = /./ (matches any value)
|
|
97
|
+
// The function returns packages where the field exists and is non-empty
|
|
98
|
+
const result = applyFilter(packages, 'name');
|
|
99
|
+
expect(result).toHaveLength(3);
|
|
100
|
+
});
|
|
101
|
+
});
|