datagrok-tools 6.5.6 → 6.5.8

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/GROK_S.md CHANGED
@@ -21,7 +21,7 @@ browser or a logged-in session.
21
21
  | List members of a group | `grok s groups list-members <group>` |
22
22
  | List the groups a user belongs to | `grok s groups list-memberships <user>` |
23
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>` |
24
+ | See who an entity is shared with | `grok s shares list <entity-id-or-name>` |
25
25
  | Create / update a connection | `grok s connections save --json conn.json` |
26
26
  | Test a connection | `grok s connections test <id-or-name>` |
27
27
  | Run a registered function | `grok s functions run 'Pkg:fn(arg1,arg2)'` |
@@ -29,9 +29,16 @@ browser or a logged-in session.
29
29
  | List / browse files in a file share | `grok s files list "System:AppData" -r` |
30
30
  | Upload / download a table (CSV or d42) | `grok s tables upload <name> file.csv\|file.d42` / `tables download <name> -O out.csv` |
31
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` |
32
+ | Install / update / uninstall server plugins | `grok s packages install Chem Bio` / `packages update --all` / `packages uninstall Chem` |
33
+ | See installed vs latest plugin versions | `grok s packages outdated` / `packages versions Chem` |
34
+ | Count entities | `grok s users count --filter 'status = "active"'` |
35
+ | See what fields an entity type has | `grok s describe connections` |
36
+ | Hit any undocumented endpoint | `grok s raw GET /users/current` / `raw POST <path> --data '{...}'` |
33
37
  | Check server + per-module health | `grok s healthcheck [--module <name>]` |
34
38
  | Bulk operations in one round-trip | `grok s batch <entity> <verb> --json items.json` |
39
+ | Move entities dev to prod (bundle, or instance to instance) | `grok s pull ... --out ./bundle` / `grok s migrate ... --from dev --to prod` |
40
+ | Browse / query / edit domain-table rows | `grok s domains query grit.issue --filter 'status = "open"'` / `domains insert` / `domains upload` |
41
+ | Manage domain schemas (manifest, apply, grants) | `grok s domains get grit` / `domains apply` / `domains grant grit.issue Chemists --access Edit` |
35
42
 
36
43
  ## Configuration
37
44
 
@@ -48,21 +55,25 @@ servers:
48
55
  key: <developer-key>
49
56
  ```
50
57
 
51
- - `grok config --server --alias <name> --server <url> --key <key>` writes a new entry.
58
+ - `grok config add --alias <name> --server <url> --key <key>` writes a new entry.
52
59
  - Add `--default` to make it the active server.
53
- - Every `grok s ...` command accepts `--host <alias-or-url>` to override the default.
60
+ - Every `grok s ...` command accepts `--host <alias-or-url>` to override the default. The URL
61
+ is the API base (`https://host/api`, or `http://host:8082` for a bare Datlas).
54
62
 
55
63
  ## Entity operations
56
64
 
57
- ### List / get / delete
65
+ ### List / count / get / delete
58
66
 
59
67
  ```bash
60
68
  grok s users list # default: table, 50 rows
61
69
  grok s users list --filter "login = 'admin'" # smart filter
70
+ grok s users list --limit 20 --offset 40 # third page of 20
71
+ grok s users count --filter 'status = "active"'
62
72
  grok s connections list --output json
63
73
  grok s packages list --filter "name:MyPlugin" # table shows friendlyName
64
74
  grok s groups get <id-or-name>
65
- grok s connections delete <id>
75
+ grok s users get alice.mendel # users: id or login
76
+ grok s connections delete "Admin:MyConnection" # id or namespace:name
66
77
  ```
67
78
 
68
79
  Options that work on every entity:
@@ -75,6 +86,14 @@ Options that work on every entity:
75
86
  | `--offset <n>` | Skip first `n` rows |
76
87
  | `--host <alias\|url>` | Target a specific server from your config |
77
88
 
89
+ A missing entity, a rejected save (duplicate login or group name), or any other server-side
90
+ failure prints one line on stderr and exits 1 — never an error object on stdout.
91
+
92
+ `delete` is not available for every entity: **users cannot be deleted** (Datagrok has no user
93
+ deletion; `users delete` refuses and points at `users block`), and `functions delete` covers
94
+ scripts and queries only (package functions go away with their package). `count` has no server
95
+ endpoint for `reports`.
96
+
78
97
  ### Save from JSON
79
98
 
80
99
  `grok s users save` and `grok s groups save` accept the same JSON shape that the
@@ -125,10 +144,13 @@ grok s groups list-members Chemists # all members
125
144
  grok s groups list-members Chemists --admin # admin members only
126
145
  grok s groups list-members Chemists --no-admin # non-admin members only
127
146
  grok s groups list-memberships alice.mendel # groups a user belongs to
147
+ grok s groups list-memberships admin --user # the personal group, not "Administrators"
128
148
  ```
129
149
 
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.
150
+ Names resolve by substring, an exact `name`/`friendlyName` match wins. When a name is still
151
+ ambiguous the command prints every matching group and exits non-zero pass a UUID to
152
+ disambiguate, or add `--user` (accepted by all four membership verbs) to restrict lookup to
153
+ personal groups.
132
154
 
133
155
  ## User administration
134
156
 
@@ -145,11 +167,12 @@ flips `status` to `Blocked` and terminates active sessions; unblocking restores
145
167
 
146
168
  ```bash
147
169
  grok s shares add "MyUser:MyConnection" Chemists,Biologists --access Edit
148
- grok s shares list <entity-uuid>
170
+ grok s shares list "MyUser:MyConnection"
149
171
  ```
150
172
 
151
173
  The entity argument accepts either a UUID or an `"author:name"` pair. `--access`
152
- defaults to `View`.
174
+ defaults to `View`. `shares list` prints one row per group and permission, including the
175
+ grants inherited through the entity's project links (`inherited: true`).
153
176
 
154
177
  ## Running functions
155
178
 
@@ -159,6 +182,10 @@ grok s functions run 'Pkg:fn({smiles:"CCO", radius:2})' # named args
159
182
  grok s functions run Pkg:fn --json params.json # big input from a file
160
183
  ```
161
184
 
185
+ The server binds arguments by parameter name, so positional values are mapped onto the
186
+ function's inputs in declared order (one extra lookup of the function); more values than inputs
187
+ is an error. Named arguments are sent as they are.
188
+
162
189
  ## Listing functions with filters
163
190
 
164
191
  `grok s functions list` composes a smart filter from flags so you don't have to hand-roll
@@ -192,7 +219,8 @@ contain colons (the namespace separator).
192
219
 
193
220
  ```bash
194
221
  grok s files list "System:AppData" -r # recursive
195
- grok s files list "System:AppData/MyPlugin"
222
+ grok s files list "System:AppData/MyPlugin" # path, kind (file|dir), size, updatedOn
223
+ grok s files list "System:DemoFiles" --output quiet # paths only, pipe-friendly
196
224
  grok s files get "System:AppData/MyPlugin/config.json"
197
225
  grok s files put ./smiles.csv "System:DemoFiles/smiles.csv" # upload local file
198
226
  grok s files delete "System:AppData/MyPlugin/old.csv"
@@ -214,9 +242,16 @@ grok s tables upload MyTable ./data.d42 # d42 binary →
214
242
  grok s tables upload MyTable ./data.csv --output json # get ID and markup back
215
243
  grok s tables download MyTable # CSV to stdout (pipe-friendly)
216
244
  grok s tables download MyTable -O ./data.csv # CSV to a local file
217
- grok s tables download <uuid> # UUID or namespace:name both work
245
+ grok s tables download "Admin:MyTable:MyTable" # full name, or the UUID
246
+ grok s tables list --filter MyTable # registered tables
247
+ grok s tables get MyTable # the TableInfo record
248
+ grok s tables delete MyTable
218
249
  ```
219
250
 
251
+ A table lives under a project namespace (the upload prints its full name,
252
+ `Admin:MyTable:MyTable`); the bare name works while only one table carries it, otherwise the
253
+ CLI lists the candidates and asks for the full name or the id.
254
+
220
255
  Upload streams raw bytes — `Content-Type: text/csv` for `.csv` and
221
256
  `application/octet-stream` for `.d42` (auto-detected from the file extension). Both
222
257
  formats handle large tables without loading the whole file into a JSON envelope.
@@ -226,6 +261,170 @@ table|json|csv|quiet`), which still controls how the upload result is printed.
226
261
  Download is CSV-only — the server reads the stored d42 blob and converts. If you
227
262
  need the raw d42 bytes, hit `/tables/data/<id>` directly via `grok s raw`.
228
263
 
264
+ ## Packages
265
+
266
+ Manage server plugins the same way the Package Manager UI does — the server pulls
267
+ published packages from its configured package repository (npm). This is for
268
+ installing **released** packages on a running server; publishing your own local
269
+ package sources is still `grok publish`.
270
+
271
+ ```bash
272
+ grok s packages install Chem Bio PowerGrid # install latest of each, one line
273
+ grok s packages install Chem --version 1.14.0 # pin a specific version (single package only)
274
+ grok s packages outdated # installed vs registry-latest
275
+ grok s packages update Chem Bio # upgrade named packages to latest
276
+ grok s packages update --all # upgrade everything outdated
277
+ grok s packages versions Chem # published versions w/ current/latest/debug flags
278
+ grok s packages set-version Chem 1.13.0 # activate a specific version (pulls it if needed)
279
+ grok s packages uninstall Chem # remove; repository entry stays installable
280
+ grok s packages share Chem Chemists --access View
281
+ ```
282
+
283
+ Semantics worth knowing:
284
+
285
+ - **`install` without `--version` sets the package to `latest`**, which is an
286
+ auto-update intent: the server re-resolves it against the registry every ~15
287
+ minutes, so the package keeps itself current. A pinned `--version` (and `update`,
288
+ which pins the concrete latest version) stays put until you change it.
289
+ - **Install is synchronous** — the call returns once the version is downloaded,
290
+ published, and made current. A heavy first-time install (e.g. Chem) can take
291
+ minutes; if the HTTP call times out, just re-run it — install is idempotent, and
292
+ a version that finished installing server-side is activated instantly on retry.
293
+ - Multi-package `install`/`update` runs sequentially and prints a per-package
294
+ status table (`installed` / `noop` / `error`); a partial failure sets exit code 1.
295
+ - **`update` preserves auto-update tracking**: a package whose desired version is
296
+ `latest` is updated by re-resolving `latest` (it keeps auto-updating); a pinned
297
+ package gets pinned to the concrete registry-latest version. Note that a
298
+ locally-published dev build counts as outdated once the registry moves past its
299
+ base version, so `update --all` will replace it with the registry release.
300
+ - Status semantics: `noop` means "already at the requested latest"; installing an
301
+ explicit `--version` reports `installed` (with the published-version id) even
302
+ when that version was already current — the server re-activates it.
303
+ - **`uninstall` of a repository-backed package keeps the package entry** (it shows
304
+ up as installable again); only locally-published packages are deleted outright.
305
+ It also clears the desired version, so the package drops out of `outdated` until
306
+ reinstalled. Neither path cleans up package credentials or package DB schemas.
307
+ - `versions`, `outdated`, and name resolution read the repository catalog, so they
308
+ need a configured package repository — check with `grok s raw GET /api/packages/repos`.
309
+ - `share` targets the package's project under the hood (same as sharing from the UI),
310
+ so the whole package — functions, queries, connections — is shared at once. For the
311
+ same reason `grok s shares list <package-uuid>` won't show the grant — it lives on
312
+ the package's project, not the package entity itself.
313
+
314
+ ## Domain schemas and rows
315
+
316
+ Entity-mapped domain tables — the schemas plugins declare in `databases/<schema>/schema.json`
317
+ and the user-managed ones created at runtime — are reachable through one entity, `domains`.
318
+ Every verb takes an address: a bare `<schema>` names a schema, `<schema>.<table>` names a
319
+ table, mirroring `grok.dapi.domains.schema('grit')` and `grok.dapi.domains.table('grit.issue')`.
320
+ Reads return only the rows and columns the key's user may see; writes are validated,
321
+ permission-checked and audited by the server exactly as they are from the UI.
322
+
323
+ ### Browsing
324
+
325
+ ```bash
326
+ grok s domains list # schemas: name, managedBy, version, table count
327
+ grok s domains list grit # tables of one schema: security mode, business key, ...
328
+ grok s domains get grit # the manifest, as JSON (doubles as an export)
329
+ grok s domains get grit.issue # the table's columns (--output json: its manifest section)
330
+ grok s domains get grit.issue <row-id> # one row
331
+ grok s domains capabilities grit.issue # what the current user may do on the table
332
+ ```
333
+
334
+ ### Querying
335
+
336
+ ```bash
337
+ grok s domains query grit.issue --filter 'status = "open"' --sort '!created_on' --limit 20
338
+ grok s domains query grit.issue --columns title,status --expand project_id --offset 100
339
+ grok s domains count grit.issue --filter 'status = "open"'
340
+ grok s domains aggregate grit.issue --measures 'count,avg(estimate) as mean' --group-by status
341
+ grok s domains aggregate grit.issue --json spec.json # any DomainAggregateSpec
342
+ grok s domains download grit.issue -O ./issues.csv --filter 'status = "open"'
343
+ grok s domains download grit.issue -O ./issues.d42 # typed DataFrame, 10M-row cap
344
+ ```
345
+
346
+ `--filter` is the domain smart-filter grammar (`status = "open"`, `title contains "crash"`,
347
+ `quantity > 10`); values are bound server-side. `--sort` is a comma list with `!` for
348
+ descending; `--expand` takes `<fk_column>`, `details:<child>` or a relation name. JSON output
349
+ and CSV downloads are capped at 10k rows by the server; a `.d42` download uses the DataFrame
350
+ path and goes up to 10M. `--limit` defaults to 50 for `query` and is unbounded for `download`.
351
+
352
+ ### Writing rows
353
+
354
+ ```bash
355
+ grok s domains insert grit.issue title="Crash on save" status=open project_id=<uuid>
356
+ grok s domains insert grit.issue --json rows.json # one object or an array
357
+ grok s domains update grit.issue <row-id> status=closed --version 3 # optimistic concurrency
358
+ grok s domains delete grit.issue <row-id>
359
+ grok s domains delete grit.issue --filter 'status = "closed"' --limit 500 # bulk, oldest first
360
+ grok s domains transaction grit --json ops.json # ordered ops, one transaction
361
+ ```
362
+
363
+ Inline `col=value` pairs are typed when the value parses as JSON (`quantity=5`,
364
+ `done=true`, `note=null`, `tags=["a","b"]`) and sent as strings otherwise. A business-key
365
+ duplicate is reported as `status: duplicate` with the existing id; add `--error-on-duplicate`
366
+ to fail instead. A validation failure prints one line per offending column and exits 1.
367
+ `delete --filter` removes at most 1000 rows per call and says when more remain.
368
+ `ops.json` is the `DomainsDataSource.transaction` ops list — `{op, table, ref?, values?, id?,
369
+ expectedVersion?}`, with `"$ref"` placeholders for earlier ops' ids.
370
+
371
+ ### Bulk upload
372
+
373
+ ```bash
374
+ grok s domains upload grit.issue ./issues.csv # insert
375
+ grok s domains upload grit.issue ./issues.csv --upsert # merge by business key
376
+ grok s domains upload grit.issue ./issues.d42 # d42 DataFrame
377
+ grok s domains upload grit.issue ./issues.json --no-all-or-nothing --error-on-duplicate
378
+ ```
379
+
380
+ The format follows the extension (`.csv`, `.d42`, `.json` — a row array, bare or under
381
+ `rows`). The whole batch is one transaction by default; `--no-all-or-nothing` applies the good
382
+ rows and reports the bad ones per row. The report prints `inserted / updated / skipped /
383
+ errors` plus a row table for the failures; any error sets exit code 1.
384
+
385
+ ### Schema lifecycle (user-managed schemas)
386
+
387
+ ```bash
388
+ grok s domains create inventory --friendly-name "Inventory" --description "Lab stock"
389
+ grok s domains apply inventory --json schema.json --dry-run # the change plan, no writes
390
+ grok s domains apply inventory --json schema.json # create / alter tables
391
+ grok s domains apply inventory --json schema.json --confirm-destructive --if-version 3
392
+ grok s domains audit inventory --limit 50 # DDL and row events, newest first
393
+ grok s domains delete inventory --force # purge: data, audit, registry, grants
394
+ ```
395
+
396
+ `schema.json` may be a full manifest or a partial apply body — only `tables`, `extend`,
397
+ `propertySchemas` and `dropTables` are sent; `name`, `version` and `description` are dropped.
398
+ Named tables replace their definition wholesale; untouched tables stay as the registry has
399
+ them. A plan that drops or narrows anything is refused until `--confirm-destructive` is
400
+ passed, and the plan is printed with the refusal. `--if-version` fails the apply when the
401
+ schema's apply counter (or `ext_version` on a package schema) has moved. Package-deployed
402
+ schemas cannot be applied to or deleted here — `apply` on one is the user-extension path
403
+ (needs `Extend`), and the manifest is owned by `grok publish`. `delete <schema>` requires
404
+ `--force` because it takes every row with it.
405
+
406
+ ### Grants
407
+
408
+ ```bash
409
+ grok s domains grants grit.issue # direct permission rows
410
+ grok s domains grant grit.issue Chemists,Biologists --access Edit
411
+ grok s domains revoke grit.issue Chemists --access Edit # omit --access to revoke all
412
+ grok s domains grant grit Chemists --access Extend # schema-level: may add own tables/columns
413
+ ```
414
+
415
+ Groups resolve by name the same way `groups add-members` does (a login resolves to the
416
+ personal group; ambiguous names list the candidates). Table grants gate row data; schema
417
+ grants gate schema operations (`Edit` to apply, `Delete` to purge, `Share`, `Extend`) and do
418
+ not reach rows. A grant on a table restricted by per-column sharing does not un-hide the
419
+ column — that path (`/domains/grants/column`) is `grok s raw` territory for now.
420
+
421
+ ### Not covered here
422
+
423
+ Watch/subscribe, facets, saved filters, per-column sharing and row promotion have no verb;
424
+ `grok s raw <METHOD> /api/domains/...` reaches them. Domain schemas and their data do not
425
+ take part in `pull` / `push` / `migrate` — install the package (or re-`apply` the manifest)
426
+ on the target and `upload` the rows.
427
+
229
428
  ## Server health
230
429
 
231
430
  ```bash
@@ -248,26 +447,50 @@ Hits `GET /public/v1/healthcheck`. Response:
248
447
  }
249
448
  ```
250
449
 
251
- `services` is the same payload as `/admin/health` (per-`GrokServiceInfo` records).
252
- Requires a valid dev key (standard `grok s` auth). For an anonymous liveness probe —
253
- load balancer, k8s readiness hit `/admin/health` directly; it's on the server's
254
- unauthenticated allowlist.
450
+ `services` is the same payload as `/admin/health` (per-`GrokServiceInfo` records). A server
451
+ that reports no services (a dev stack) prints `(no services reported)`; `--module` for a
452
+ module the server does not report exits 1. Requires a valid dev key (standard `grok s` auth).
453
+ For an anonymous liveness probe — load balancer, k8s readiness — hit `/admin/health` directly;
454
+ it's on the server's unauthenticated allowlist.
455
+
456
+ ## Describing an entity type
457
+
458
+ ```bash
459
+ grok s describe connections # fields of a DataConnection, with types and examples
460
+ grok s describe Project --output json # by type name; JSON carries the registry record and a sample
461
+ grok s describe users --output quiet # field names only
462
+ ```
463
+
464
+ The server publishes no JSON schema, so `describe` combines the entity-type registry record
465
+ with the top-level fields of one existing entity of that type (`field`, `type`, `example`).
466
+ Aliases: `users groups connections queries scripts functions packages reports tables projects
467
+ files`; any other registered type name (`Project`, `ViewLayout`) works when at least one
468
+ entity exists.
255
469
 
256
470
  ## Raw API access
257
471
 
258
472
  When no dedicated subcommand exists, fall through to `grok s raw`:
259
473
 
260
474
  ```bash
261
- grok s raw GET /api/users/current
262
- grok s raw GET /api/packages/dev/MyPlugin
263
- grok s raw POST /api/admin/reload-settings
475
+ grok s raw GET /users/current
476
+ grok s raw GET /packages/dev/MyPlugin
477
+ grok s raw POST /admin/reload-settings
478
+ grok s raw POST /public/v1/functions/Sin/call --data '{"x": 1}'
479
+ grok s raw POST /domains/grants/<id> --json grant.json
264
480
  ```
265
481
 
482
+ Paths are relative to the API base of the target server (`/users/current` becomes
483
+ `https://host/api/users/current`, or `http://host:8082/users/current` on a bare Datlas); a
484
+ leading `/api` is accepted and dropped, so old `/api/...` paths keep working on both host
485
+ shapes. A body comes from `--json <file>` or `--data '<json>'`. A non-2xx answer, or a
486
+ 200 carrying an `ApiError`, prints the message with the HTTP status on stderr and exits 1;
487
+ under `--output json` the stderr line is `{"error", "errorCode", "body"}`.
488
+
266
489
  On **Windows Git Bash**, prefix raw paths with `MSYS_NO_PATHCONV=1` to stop the shell
267
490
  from rewriting POSIX paths into Windows paths:
268
491
 
269
492
  ```bash
270
- MSYS_NO_PATHCONV=1 grok s raw GET /api/users/current
493
+ MSYS_NO_PATHCONV=1 grok s raw GET /users/current
271
494
  ```
272
495
 
273
496
  ## Batch operations
@@ -292,14 +515,17 @@ Manifest shape:
292
515
  "stopOnError": true,
293
516
  "transaction": false,
294
517
  "operations": [
295
- { "id": "op1", "action": "users.save", "params": {"login": "alice", "firstName": "Alice"} },
296
- { "id": "op2", "action": "groups.save", "params": {"name": "Chemists"} }
518
+ { "id": "op1", "action": "users.create", "params": {"login": "alice", "firstName": "Alice"} },
519
+ { "id": "op2", "action": "groups.create", "params": {"name": "Chemists"} }
297
520
  ]
298
521
  }
299
522
  ```
300
523
 
301
- For `files.put`, add `"source": "<local-path>"` and the CLI base64-encodes the file
302
- into `content` before sending.
524
+ Actions the server accepts: `create | get | delete` for `users`, `groups`, `connections`,
525
+ `functions`, `queries`, `scripts` (`get | delete` for `reports`), `functions.run`
526
+ (`{name, params}`), and `files.list | get | put | delete`. For `files.put`, add
527
+ `"source": "<local-path>"` and the CLI base64-encodes the file into `content` before sending.
528
+ `users.delete` removes the entity record only (see "List / count / get / delete").
303
529
 
304
530
  ## Scripting pattern
305
531
 
@@ -354,13 +580,323 @@ grok s groups add-members Chemists alice.mendeleev bob.curie carol.pauling --use
354
580
  grok s groups list-members Chemists --no-admin
355
581
  ```
356
582
 
357
- Every subcommand above is idempotent re-running the whole block is safe.
583
+ Steps 1 and 2 create; re-running them fails with "already exists" (exit 1) unless the JSON
584
+ carries the existing `id`. Step 3 is idempotent (`noop` on a re-run).
585
+
586
+ ## Migrating entities between instances (pull / push / migrate)
587
+
588
+ Entities built in the UI on one instance — connections, queries, scripts, dashboards,
589
+ spaces, layouts, tables, files, jobs, notebooks, models, and the groups and grants they
590
+ need — are promoted to another instance through a **bundle**: a directory of one JSON file
591
+ per entity that travels by any means (a commit, a PR, a USB stick).
592
+
593
+ ```bash
594
+ grok s pull <selection> --out ./bundle --host dev # instance → directory
595
+ grok s bundle ls ./bundle # what is in it
596
+ grok s diff ./bundle --host prod # what a push would change
597
+ grok s push ./bundle --host prod [--dry-run] # directory → instance
598
+ grok s migrate <selection> --from dev --to prod # pull + push, temp dir in between
599
+ ```
600
+
601
+ Every entity keeps the **same UUID** on both instances, so a push is idempotent: pushing an
602
+ unchanged bundle a second time writes nothing, and 1.28's built-in server-to-server sync
603
+ recognises what the CLI pushed as its own.
604
+
605
+ Moving a **whole instance** rather than one team's work needs `--admin` on both ends, so the
606
+ run sees and writes what the key's own account cannot: without it a pull collects only the
607
+ entities shared with that account, and the target's own users never get their content back.
608
+
609
+ ```bash
610
+ grok s pull --since 10y --type project,connection,query,script,group,layout --out ./bundle --host dev --admin
611
+ grok s push ./bundle --host prod --admin --on-conflict skip
612
+ ```
613
+
614
+ **Move a whole instance one space at a time.** Placement is exclusive on the server — one project
615
+ holding an entity takes it from every other — so a single bundle carrying every space has its
616
+ projects competing, and a stand-sized push spends its time taking rows back off each other. Scoped
617
+ to one space that contention stays inside it:
618
+
619
+ ```bash
620
+ grok s migrate --from dev --to prod --admin --by-namespace # every space, in turn
621
+ grok s migrate --from dev --to prod --admin --by-namespace --only Chem,Bio
622
+ ```
623
+
624
+ It checks the instance-level prerequisites once before writing anything — users and packages the
625
+ target lacks — and refuses a whole-instance run until they are fixed (`--force` overrides; a run
626
+ already scoped with `--only` is reported but allowed). `--skip` leaves a space out, and a full run
627
+ ends with an `(unowned)` sweep for leaf entities no space holds — a layout under no namespace would
628
+ otherwise never travel. `--no-sweep` turns that off. Each space is a separate pull and push, so a
629
+ failure costs one space rather than the run, and what finished is recorded in a state file
630
+ (`--state`), letting a re-run continue instead of repeating work.
631
+
632
+ Measured on 1.27.9: three spaces, 1,946 entities, 4m44s, one failure isolated to its space; the
633
+ re-run skipped all three in 8s. The same content as part of a 15,778-entity single bundle did not
634
+ finish at all.
635
+
636
+ **Name the content types; do not pull tables directly.** A long-lived stand accumulates a loose
637
+ `TableInfo` per ad-hoc import — dev holds 1.76 M of them against 21 K projects, and an admin
638
+ session sees every one. Selecting projects instead pulls each project's tables, views and layouts
639
+ as dependencies, so everything a dashboard needs still travels and the scratch rows stay behind.
640
+ Check what a stand is carrying before deciding:
641
+
642
+ ```bash
643
+ grok s tables list --host dev --admin --limit 1 --output json # `count` ignores --filter
644
+ ```
645
+
646
+ `--type` takes the aliases `conn`/`connection`, `query`, `script`, `project`/`dashboard`/`space`,
647
+ `view`, `layout`, `table`, `file`, `group`, `job`, `notebook`, `model`, or an exact type name
648
+ (`DataConnection`) — a lowercased type name is not an alias and is rejected.
649
+
650
+ ### Preparing the target
651
+
652
+ Four things belong to the instance, not to a bundle, and have to be in place before the push.
653
+ `grok s diff` reports missing users; missing packages are reported by the push itself, since the
654
+ check runs against what the push is about to write.
655
+
656
+ | | Why | How to check |
657
+ |---|---|---|
658
+ | **Users** | Content of a user who does not exist lands under the pushing account | `warn user_missing` |
659
+ | **Packages** | Package functions, scripts and connections carry a name-derived id that only exists once the package is *published* there — installing it from the repository is not enough | `warn package_not_installed` |
660
+ | **Platform shares** | `System:AppData` and `System:DemoFiles` are created by the deployment; entities that read from them need them present | `grok s connections list --host prod` |
661
+ | **Credentials** | Secrets never travel — set them on the target after the push, or pass `--creds` | `needs-credentials` rows |
662
+
663
+ A freshly deployed stand also stops at its setup wizard, and 1.27.x keeps the session token in
664
+ memory rather than a cookie — so that redirect logs you straight back out. Click the wizard
665
+ through once and normal login works.
666
+
667
+ Users are **not** part of a bundle. Create them on the target first — content that belongs to
668
+ a user who does not exist there lands under the pushing account instead of theirs.
669
+
670
+ ### Surviving a blip
671
+
672
+ A retriable answer (429, 502, 503, 504) or a dropped socket is retried with exponential backoff —
673
+ five attempts by default, about half a minute. That covers a busy moment, not a stand that steps
674
+ out for a restart, and a whole-instance walk is long enough to meet one:
675
+
676
+ ```bash
677
+ GROK_HTTP_RETRIES=9 grok s pull --out ./bundle --host dev --admin ... # ~90s of tolerance
678
+ ```
679
+
680
+ A pull that dies anyway is not wasted — pulls accumulate, so re-running without `--replace`
681
+ merges into the same bundle.
682
+
683
+ ### A bundle remembers where it was pushed
684
+
685
+ `--on-conflict adopt` records every adoption in the bundle's own `idmap.json`, so a re-push
686
+ writes into the same target rows instead of adopting again. That is what makes a repeated push
687
+ idempotent — but it also means the bundle is no longer neutral: pushed at a *different* target,
688
+ or re-pushed after an aborted attempt, it resolves entities by the ids it learned last time and
689
+ updates rows it would otherwise have created.
690
+
691
+ Pull into a fresh directory (or delete `idmap.json`) whenever a bundle is aimed at a different
692
+ instance than the one it last adopted against.
693
+
694
+ ### Conflict policies
695
+
696
+ Measured against a target holding 924 of the bundle's 1,162 names:
697
+
698
+ | `--on-conflict` | What lands | Cost |
699
+ |---|---|---|
700
+ | `fail` (default) | nothing; every conflict is listed first | safest way to see the damage before writing |
701
+ | `skip` | the non-conflicting entities only | most failures — whatever depended on a skipped entity fails too |
702
+ | `adopt` | conflicting names are written into the target's existing rows | keeps one copy; the usual choice for a real migration |
703
+ | `duplicate` | a second copy of everything that collides | target grows by the whole bundle, tables bring their columns |
704
+
705
+ ### References the source has already lost
706
+
707
+ A long-lived instance accumulates entities that point at something deleted out from under them —
708
+ most often a saved view whose table is gone. The pull resolves every outside reference against the
709
+ source, so a bundle records which of them the source itself can no longer answer:
710
+
711
+ ```
712
+ Bundle warn source_dangling_refs 50 reference(s) point at entities the source itself no longer has
713
+ ```
714
+
715
+ The push then reports whatever depends on one as `skip dead_on_source` rather than as a failure,
716
+ and does not fail the run over it — no target can supply what the source has already lost. A
717
+ reference the source *does* keep but the target lacks stays a real failure (`dependency_missing`),
718
+ because installing the package or widening the pull fixes it.
719
+
720
+ ### Bundle layout
721
+
722
+ ```
723
+ bundle/
724
+ manifest.json # source url + version, pull history, FK-safe order
725
+ DataConnection/Chem.Chembl.json # <Type>/<nqName with ':' and '/' as '.'>.json
726
+ DataQuery/Chem.CompoundsByTarget.json
727
+ Project/Chem.Dashboard.json
728
+ UserGroup/Chemists.json # bare group + member logins, never member ids
729
+ FileInfo/reports.readme.md.json # a file without a namespace is named by its path
730
+ tables/<id>.d42 # table data (on by default for pulled tables)
731
+ files/<id> # file bytes (only with --include-files)
732
+ shares/<encoded path> # share files a datasync table rebuilds itself from
733
+ idmap.json # sourceId -> targetId, written whenever the push resolves an
734
+ # entity to a different id on the target — an adoption, a
735
+ # package entity found by name, or a save that answered with
736
+ # an existing row's id
737
+ ```
738
+
739
+ **Pulls accumulate.** Pulling into an existing bundle merges: entities already there are
740
+ overwritten by id, new ones are added, nothing is removed, and `manifest.pulls[]` keeps one
741
+ entry per invocation. `--replace` clears the directory first.
742
+
743
+ ```bash
744
+ grok s pull Chem:TargetDashboard --out ./release --host dev
745
+ grok s pull --type script --author alice --no-deps --out ./release --host dev
746
+ grok s pull Chemists --out ./release --host dev
747
+ grok s push ./release --host prod
748
+ ```
749
+
750
+ ### Selection
751
+
752
+ | Flag | Selects |
753
+ |---|---|
754
+ | positional `Chem:Dashboard <uuid> ...` | exactly these entities, by nqName or id |
755
+ | `--type conn,query,script,project,dashboard,space,view,layout,table,file,group,job,notebook,model` | which types to list (default: all of them) |
756
+ | `--name <glob>` | free-text search, then a client-side glob on name and friendly name (`Cereal*`, `*demo*`) |
757
+ | `--namespace Chem` | everything **under** the namespace, recursively, plus the space `Chem` itself — without it nothing selected has anywhere to be placed |
758
+ | `--space Chem:Reports` | the same, named by nqName rather than by namespace |
759
+ | `--author alice` | entities authored by a login |
760
+ | `--tag demo` | entities carrying a tag (types whose router has no `tags` param are skipped with a warning) |
761
+ | `--since 2w` / `--since=-30d` / `--since 2026-08-01` | updated since (bare `2w` means `-2w`; the shell eats a leading `-` unless you use `=`) |
762
+ | `--filter "<expr>"` | a smart-filter expression, ANDed with the rest |
763
+ | `--no-deps` | do not follow dependencies (what never travels is still excluded — see below) |
764
+ | `--no-include-data` | do not pull the `.d42` data of the tables that were pulled (data is on by default) |
765
+ | `--include-files` | also pull the bytes of the files that were pulled (off by default) |
766
+ | `--verbose` | print the stack of a runtime failure instead of the one-line message |
767
+
768
+ `--type space` and `--type dashboard` are both `Project` with a different listing rule, so
769
+ they cannot be combined in one command (neither can `--type project,space`).
770
+
771
+ ### What the walker adds, and what never travels
772
+
773
+ A selected entity brings its dependencies with it: a project brings its relation children
774
+ (recursively), their views, layouts and tables; a query brings its connection; a datasync
775
+ table brings the connection its creation script opens; a notebook and a model bring their
776
+ tables; every entity brings the non-personal groups that hold grants on it, and a group
777
+ brings its parents and members. A **job** brings nothing — 1.27 does not persist the link
778
+ between a job and the queries it runs, so pull those explicitly.
779
+
780
+ The table below holds however the entity was chosen: `--no-deps` skips the walk, not these
781
+ rules, and `push` refuses the same connections even if a bundle was hand-edited to carry one
782
+ (`skip(platform_connection)` / `skip(personal_storage)` / `skip(space_files_connection)`).
783
+
784
+ | Never travels | Row on the report |
785
+ |---|---|
786
+ | Passwords and other password-class connection parameters | `needs-credentials` (see `--creds`) |
787
+ | Users — memberships are replayed by login and group name | `warn(member_not_found)` when a login is not on the target |
788
+ | Package-owned entities — install the package instead | `warn(package_entity)`, `warn(package_not_installed)` |
789
+ | `System:` connections, the personal `Home` share, a space's own `Files` connection | `info(platform_connection)` / `warn(personal_storage)` / `info(space_files_connection)` |
790
+ | A trained model blob | `info(model_blob_skipped)` |
791
+
792
+ ### Credentials: `--creds`
793
+
794
+ A pushed connection arrives without its secrets. Author a YAML file **for the target** and
795
+ pass it to `push` or `migrate`; the values are merged into the connection's parameters before
796
+ the save, and the server encrypts and masks them itself. Nothing is ever read back into the
797
+ bundle.
798
+
799
+ ```yaml
800
+ # creds.yaml — keys are connection nqNames as the bundle spells them
801
+ Chem:Chembl:
802
+ password: ${CHEMBL_PROD_PASSWORD}
803
+ Admin:Northwind:
804
+ password: ${NORTHWIND_PASSWORD}
805
+ ```
806
+
807
+ ```bash
808
+ CHEMBL_PROD_PASSWORD=... grok s push ./release --host prod --creds ./creds.yaml
809
+ ```
810
+
811
+ `${VAR}` is resolved from the environment exactly as `grok publish` resolves it in
812
+ `connections/*.json` (the file is parsed as YAML first, so a `${VAR}` written in flow style
813
+ needs quoting: `{password: "${VAR}"}`); a variable that is not set aborts the push before the
814
+ first write. A connection the file covers gets no `needs-credentials` row, and is **always**
815
+ written — a secret is invisible in the payload, so a connection that would otherwise be
816
+ `identical` is planned as `update` with reason `credentials`. That is how a password is
817
+ rotated: re-run the push with a new value.
818
+
819
+ ### Conflicts
820
+
821
+ An entity whose id is absent on the target but whose name is taken by another id is a
822
+ conflict. `--on-conflict` decides:
823
+
824
+ | Policy | What happens |
825
+ |---|---|
826
+ | `fail` (default) | nothing is written; every conflict is listed and the command exits 1 |
827
+ | `skip` | the twin is left alone; anything in the bundle that points at it is reported `failed(dependency_skipped)` |
828
+ | `adopt` | the bundle entity is written **into** the twin, and `idmap.json` records `sourceId -> targetId` so every later reference and every later push follows it |
829
+ | `duplicate` | the bundle entity is created under its own id next to the twin (the server renames it `Name_1`). A `UserGroup` cannot be duplicated — group names are unique on 1.27, so that row fails |
830
+
831
+ ### Reading the plan
832
+
833
+ `diff` and `--dry-run` print the plan without writing (`diff` plans with `skip`, so a
834
+ conflict does not abort it). Actions: `create`, `update` (with the changed top-level keys as
835
+ the detail), `identical` (no write), `skip`, `failed`, plus the `warn` / `info` /
836
+ `needs-credentials` notes. `--output json` emits the 1.28-compatible shape — the same one for
837
+ `diff`, `push` and `migrate`, with `detail` always present (`""` when there is nothing to
838
+ say):
839
+
840
+ ```json
841
+ {
842
+ "items": [{"name": "Chem:Chembl", "entityType": "DataConnection", "action": "create", "reason": "", "detail": ""}],
843
+ "counts": {"create": 1, "identical": 4},
844
+ "status": "ok",
845
+ "remoteUrl": "https://prod.datagrok.ai/api"
846
+ }
847
+ ```
848
+
849
+ A push exits 1 if any row is `failed`.
850
+
851
+ ### 1.27 limits worth knowing
852
+
853
+ - A **FileInfo that lives in a share** is a dead row on the target — only stand-alone blobs
854
+ migrate (`info(file_in_share_not_migratable)`), the same rule the 1.28 sync applies.
855
+ - `metaParams` do not survive a save on connections and queries, so metadata attached there
856
+ does not travel.
857
+ - `GET /projects/relations` fails on a project that links domain-table rows; the walker falls
858
+ back to the project's own `relations[]` and reports `warn(relations_degraded)`.
859
+ - Relations are **merged**, never replaced: a link that exists only on the target is kept, so
860
+ removing a relation from a bundle never unlinks it there (`info(relation_not_removed)`).
861
+ Unlink it in the UI on the target instead.
862
+ - A space delete leaves its `…:Files` connection behind, and it cannot be deleted through the
863
+ API. Deleting a group that still holds grants fails — revoke the grants first.
864
+
865
+ ### dev to prod, end to end
866
+
867
+ ```bash
868
+ # 1. see what would come over, from dev, without touching prod
869
+ grok s migrate Chem:TargetDashboard --from dev --to prod --dry-run
870
+
871
+ # 2. promote it, filling in the target's secrets
872
+ CHEMBL_PROD_PASSWORD=... grok s migrate Chem:TargetDashboard \
873
+ --from dev --to prod --creds ./creds.yaml --keep
874
+
875
+ # 3. re-run: everything is `identical`, nothing is written
876
+ grok s migrate Chem:TargetDashboard --from dev --to prod
877
+
878
+ # 4. or keep the bundle under version control instead
879
+ grok s pull Chem:TargetDashboard --out ./release --host dev
880
+ git add release && git commit -m "release: target dashboard"
881
+ grok s push ./release --host prod --creds ./creds.yaml
882
+ ```
883
+
884
+ `migrate` pulls into a temporary directory and deletes it afterwards; `--keep` keeps it and
885
+ prints its path on **stderr**, so `--output json` stays one parseable document. `--from` is
886
+ only ever read from.
358
887
 
359
888
  ## Implementation notes
360
889
 
361
- - Source: `public/tools/bin/commands/server.ts`, `public/tools/bin/utils/node-dapi.ts`.
890
+ - Source: `public/tools/bin/commands/server.ts`, `public/tools/bin/utils/node-dapi.ts`;
891
+ pull / push / migrate in `bin/commands/server-migrate.ts` + `bin/utils/migrate/`.
892
+ Domain schemas and rows in `bin/commands/server-domains.ts` (`NodeDomainsDataSource` in
893
+ `node-dapi.ts`); they call the internal `/domains/` router the browser uses.
362
894
  - The Node client talks directly to `/public/v1/` — no Dart interop, no browser, no
363
895
  logged-in session required. Authentication uses the developer key from the config.
364
896
  - If `grok s` is not working, start by running `grok s healthcheck` — it verifies the
365
897
  URL, the key, and basic connectivity, and returns per-module status if the server is
366
- reachable. Fall back to `grok s raw GET /api/users/current` to isolate auth issues.
898
+ reachable. Fall back to `grok s raw GET /users/current` to isolate auth issues. A `--host`
899
+ URL that is not the API base fails at login with the reason (`should end with /api`).
900
+ - Cross-instance sync (1.28 servers): `grok s sync pairs list`, `sync setups list --pair <id>`,
901
+ `sync setup get <id>`, `sync run <id>`; on 1.27 the routes do not exist and the commands
902
+ answer "not found".