datagrok-tools 6.5.5 → 6.5.7
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/.devcontainer/docker-compose.yaml +68 -24
- package/CHANGELOG.md +13 -5
- package/CLAUDE.md +10 -0
- package/GROK_S.md +52 -0
- package/bin/commands/help.js +10 -0
- package/bin/commands/report.js +68 -19
- package/bin/commands/server.js +250 -23
- package/bin/grok.js +2 -0
- package/bin/utils/node-dapi.js +97 -2
- package/bin/utils/playwright-runner.js +52 -38
- package/package.json +1 -1
|
@@ -8,6 +8,29 @@
|
|
|
8
8
|
# On Windows, use `grok claude` instead — it handles path conversion
|
|
9
9
|
# and Docker socket mapping automatically.
|
|
10
10
|
|
|
11
|
+
# Shared by every jkg_* gateway below. They differ only in image and network alias.
|
|
12
|
+
x-jkg: &jkg
|
|
13
|
+
platform: linux/amd64
|
|
14
|
+
environment:
|
|
15
|
+
GROK_PARAMETERS: |
|
|
16
|
+
{
|
|
17
|
+
"queueSettings": {
|
|
18
|
+
"useQueue": true,
|
|
19
|
+
"amqpHost": "rabbitmq",
|
|
20
|
+
"amqpUser": "guest",
|
|
21
|
+
"amqpPassword": "guest",
|
|
22
|
+
"amqpPort": 5672,
|
|
23
|
+
"pipeHost": "grok_pipe",
|
|
24
|
+
"pipePort": 3000,
|
|
25
|
+
"pipeKey": "test-key",
|
|
26
|
+
"maxConcurrentCalls": 16
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
restart: unless-stopped
|
|
30
|
+
depends_on:
|
|
31
|
+
rabbitmq:
|
|
32
|
+
condition: service_started
|
|
33
|
+
|
|
11
34
|
services:
|
|
12
35
|
postgres:
|
|
13
36
|
image: pgvector/pgvector:pg17
|
|
@@ -118,33 +141,54 @@ services:
|
|
|
118
141
|
restart: unless-stopped
|
|
119
142
|
profiles: ["full"]
|
|
120
143
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
GROK_PARAMETERS: |
|
|
129
|
-
{
|
|
130
|
-
"queueSettings": {
|
|
131
|
-
"useQueue": true,
|
|
132
|
-
"amqpHost": "rabbitmq",
|
|
133
|
-
"amqpUser": "guest",
|
|
134
|
-
"amqpPassword": "guest",
|
|
135
|
-
"amqpPort": 5672,
|
|
136
|
-
"pipeHost": "grok_pipe",
|
|
137
|
-
"pipePort": 3000,
|
|
138
|
-
"pipeKey": "test-key",
|
|
139
|
-
"maxConcurrentCalls": 16
|
|
140
|
-
}
|
|
141
|
-
}
|
|
144
|
+
# Per-language kernel gateways, which replaced the single all-languages
|
|
145
|
+
# datagrok/jupyter_kernel_gateway image. They version with the Scripting package, not with
|
|
146
|
+
# the platform, so they take their own JKG_VERSION. `scripting`/`full` starts Python and R;
|
|
147
|
+
# add `jkg_octave`, `jkg_julia` or `jkg_nodejs` when a package needs those languages.
|
|
148
|
+
jkg_python:
|
|
149
|
+
<<: *jkg
|
|
150
|
+
image: datagrok/jkg_python:${JKG_VERSION:-latest}
|
|
142
151
|
networks:
|
|
143
152
|
dg:
|
|
144
153
|
aliases:
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
- jkg_python
|
|
155
|
+
profiles: ["scripting", "full", "jkg_python"]
|
|
156
|
+
|
|
157
|
+
jkg_r:
|
|
158
|
+
<<: *jkg
|
|
159
|
+
image: datagrok/jkg_r:${JKG_VERSION:-latest}
|
|
160
|
+
networks:
|
|
161
|
+
dg:
|
|
162
|
+
aliases:
|
|
163
|
+
- jkg_r
|
|
164
|
+
profiles: ["scripting", "full", "jkg_r"]
|
|
165
|
+
|
|
166
|
+
jkg_octave:
|
|
167
|
+
<<: *jkg
|
|
168
|
+
image: datagrok/jkg_octave:${JKG_VERSION:-latest}
|
|
169
|
+
networks:
|
|
170
|
+
dg:
|
|
171
|
+
aliases:
|
|
172
|
+
- jkg_octave
|
|
173
|
+
profiles: ["jkg_octave"]
|
|
174
|
+
|
|
175
|
+
jkg_julia:
|
|
176
|
+
<<: *jkg
|
|
177
|
+
image: datagrok/jkg_julia:${JKG_VERSION:-latest}
|
|
178
|
+
networks:
|
|
179
|
+
dg:
|
|
180
|
+
aliases:
|
|
181
|
+
- jkg_julia
|
|
182
|
+
profiles: ["jkg_julia"]
|
|
183
|
+
|
|
184
|
+
jkg_nodejs:
|
|
185
|
+
<<: *jkg
|
|
186
|
+
image: datagrok/jkg_nodejs:${JKG_VERSION:-latest}
|
|
187
|
+
networks:
|
|
188
|
+
dg:
|
|
189
|
+
aliases:
|
|
190
|
+
- jkg_nodejs
|
|
191
|
+
profiles: ["jkg_nodejs"]
|
|
148
192
|
|
|
149
193
|
world:
|
|
150
194
|
image: datagrok/demo_db_postgres:12-1.1.0
|
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
-
## 6.5.
|
|
3
|
+
## 6.5.7 (2026-08-28)
|
|
4
4
|
|
|
5
|
+
* GROK-20789: `grok s packages` — full package lifecycle from the CLI: `install <name>... [--version <v>]` (server pulls released versions from the configured package repository / npm, `latest` by default), `uninstall`, `update <name>... | --all` (preserves `latest` auto-update tracking; pins pinned packages to the concrete registry-latest), `outdated`, `versions`, `set-version`, and `share`. Multi-package install/update runs sequentially with a per-package status table; a nonexistent package or version is a per-package `error` and exit code 1. Also fixed the generic `packages delete`, which was hitting a nonexistent `/public/v1` endpoint.
|
|
6
|
+
* `grok s` — handler errors now print a clean `{"error": ...}` and exit 1 instead of a raw stack trace with exit 255 (handler dispatch was not awaited, so rejections escaped the error boundary).
|
|
7
|
+
* `--version` values are no longer numerically coerced (`--version 1.10` used to arrive as `1.1`; affected `grok claude` too).
|
|
8
|
+
* `grok test` — a Playwright pass that fails before running specs now reports a failing test instead of nothing. It returned an empty CSV, and the caller drops an empty Playwright result whenever the Puppeteer pass produced rows, so a suite whose config would not load (or that collected zero specs) was indistinguishable from a clean run — Build-Deploy #1277 printed "Playwright: no JSON report produced" and then "Tests passed", exit 0, for six packages. Every early exit now writes one failing row to `test-report-playwright.csv` and the merged report, and a report containing no specs is treated as a failure rather than a pass.
|
|
9
|
+
|
|
10
|
+
## 6.5.6 (2026-08-20)
|
|
11
|
+
|
|
12
|
+
* `grok report` (`ticket`, `comment`, `label`, `attach`) — support Atlassian **service-account** tokens alongside user API tokens, chosen by the token itself. A user token (`ATATT...`) keeps HTTP Basic with `JIRA_USER` + `JIRA_TOKEN` against the site. A service-account token (`ATSTT...`, minted at admin.atlassian.com) is a scoped token that the site host will not accept — Basic returns 401 and Bearer returns 403 `"Failed to parse Connect Session Auth Token"` — so it is sent as Bearer to the API gateway at `api.atlassian.com/ex/jira/<cloudId>`, with the cloud id read once from the site's public `/_edge/tenant_info` (override with `$JIRA_CLOUD_ID`). `$JIRA_URL` and `--jira-url` still mean the SITE; the gateway address is derived from it, never substituted for it. A service token needs no `JIRA_USER`, so the credential checks no longer demand one.
|
|
13
|
+
|
|
14
|
+
## 6.5.5 (2026-08-12)
|
|
15
|
+
|
|
16
|
+
* `grok test` — added a Node (browserless) pass: tests annotated `{node: true}` run headless under the js-api Node runtime before the browser launches; the browser pass excludes them and is skipped entirely when nothing browser-only matches. New flags: `--skip-node`, `--node-only`. Packages opt in by exporting `testNode()` from `package-test.ts`; others keep the previous behavior.
|
|
5
17
|
* GROK-20298: `grok api --ui` — a relation-bearing table's `<Table>Ui` handle carries `<Table>Update` as its fifth generic too (`<Table>Ui.client`, `.table()` and the `<Schema>UiDb` property), so `<table>Ui.client.update(id, {labels: [...]})` compiles instead of failing the excess-property check.
|
|
6
18
|
* GROK-20298: `grok api` — typed many-to-many relations: a table's `relations` block is validated with the server's rules (via/target declared in the same manifest, junction distinct from both sides, self-referential relations explicit, FK sides unique-or-explicit, junction `businessKey` covering both) and emitted as an expand-map entry `'<name>': {<name>?: {id: string; name: string}[]}`, a `<name>?: string[]` link set on `<Table>Insert`, and a new `<Table>Update` type (`Partial<<Table>Row>` plus the link sets) used by the transaction op union and passed as the client's fifth generic, so `update()` takes the link sets directly. Manifests without relations generate byte-identically.
|
|
7
19
|
* GROK-20298: `grok api --ui` — also emits the typed SCHEMA handle: `export function <schema>UiDb(): Promise<<Schema>UiDb>` (a typed `domains.db(...)`), and the `<Schema>UiDb` interface types each table property (`const db = await gritUiDb(); db.issues.form({values: {...}})` is compile-checked).
|
|
@@ -28,10 +40,6 @@
|
|
|
28
40
|
|
|
29
41
|
* `grok test` — the Node-pass report now merges into the browser report by column name. The line-wise merge assumed identical column order, so node rows landed misaligned (string values in the integer `ms` column) and the CI test-report upload failed with `invalid input syntax for type integer`.
|
|
30
42
|
|
|
31
|
-
## 6.5.5 (WIP)
|
|
32
|
-
|
|
33
|
-
* `grok test` — added a Node (browserless) pass: tests annotated `{node: true}` run headless under the js-api Node runtime before the browser launches; the browser pass excludes them and is skipped entirely when nothing browser-only matches. New flags: `--skip-node`, `--node-only`. Packages opt in by exporting `testNode()` from `package-test.ts`; others keep the previous behavior.
|
|
34
|
-
|
|
35
43
|
## 6.4.8 (2026-07-22)
|
|
36
44
|
|
|
37
45
|
* GROK-20452: `grok publish` — always `docker build` the tools-generated worker dirs (`dockerfiles/queue`, `dockerfiles/celery`, `dockerfiles/<app>-celery`) instead of reusing a cached local tag. The "found local image" shortcut froze the worker at whatever stock base the daemon had when the tag was first built (CI ran day-old worker code with a fresh base available); the layer cache keeps an unchanged rebuild near-instant.
|
package/CLAUDE.md
CHANGED
|
@@ -251,6 +251,16 @@ grok s connections test --json conn.json # test a connectio
|
|
|
251
251
|
grok s functions run 'Chem:smilesToMw("ccc")'
|
|
252
252
|
grok s functions run 'Pkg:fn({a:5,b:22})'
|
|
253
253
|
|
|
254
|
+
# Manage packages (server pulls released versions from the package repository / npm)
|
|
255
|
+
grok s packages install Chem Bio PowerGrid # install latest of each
|
|
256
|
+
grok s packages install Chem --version 1.14.0 # pin a version
|
|
257
|
+
grok s packages outdated # installed vs registry-latest
|
|
258
|
+
grok s packages update --all # upgrade everything outdated
|
|
259
|
+
grok s packages versions Chem # published versions + current/latest flags
|
|
260
|
+
grok s packages set-version Chem 1.13.0 # activate a specific version
|
|
261
|
+
grok s packages uninstall Chem # repo entry stays installable
|
|
262
|
+
grok s packages share Chem Chemists --access View
|
|
263
|
+
|
|
254
264
|
# Browse file storage
|
|
255
265
|
grok s files list "System:AppData" -r # list files recursively
|
|
256
266
|
grok s files list "System:AppData/MyPlugin"
|
package/GROK_S.md
CHANGED
|
@@ -29,6 +29,8 @@ 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
|
+
| 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` |
|
|
32
34
|
| Hit any undocumented endpoint | `grok s raw GET /api/users/current` |
|
|
33
35
|
| Check server + per-module health | `grok s healthcheck [--module <name>]` |
|
|
34
36
|
| Bulk operations in one round-trip | `grok s batch <entity> <verb> --json items.json` |
|
|
@@ -226,6 +228,56 @@ table|json|csv|quiet`), which still controls how the upload result is printed.
|
|
|
226
228
|
Download is CSV-only — the server reads the stored d42 blob and converts. If you
|
|
227
229
|
need the raw d42 bytes, hit `/tables/data/<id>` directly via `grok s raw`.
|
|
228
230
|
|
|
231
|
+
## Packages
|
|
232
|
+
|
|
233
|
+
Manage server plugins the same way the Package Manager UI does — the server pulls
|
|
234
|
+
published packages from its configured package repository (npm). This is for
|
|
235
|
+
installing **released** packages on a running server; publishing your own local
|
|
236
|
+
package sources is still `grok publish`.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
grok s packages install Chem Bio PowerGrid # install latest of each, one line
|
|
240
|
+
grok s packages install Chem --version 1.14.0 # pin a specific version (single package only)
|
|
241
|
+
grok s packages outdated # installed vs registry-latest
|
|
242
|
+
grok s packages update Chem Bio # upgrade named packages to latest
|
|
243
|
+
grok s packages update --all # upgrade everything outdated
|
|
244
|
+
grok s packages versions Chem # published versions w/ current/latest/debug flags
|
|
245
|
+
grok s packages set-version Chem 1.13.0 # activate a specific version (pulls it if needed)
|
|
246
|
+
grok s packages uninstall Chem # remove; repository entry stays installable
|
|
247
|
+
grok s packages share Chem Chemists --access View
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Semantics worth knowing:
|
|
251
|
+
|
|
252
|
+
- **`install` without `--version` sets the package to `latest`**, which is an
|
|
253
|
+
auto-update intent: the server re-resolves it against the registry every ~15
|
|
254
|
+
minutes, so the package keeps itself current. A pinned `--version` (and `update`,
|
|
255
|
+
which pins the concrete latest version) stays put until you change it.
|
|
256
|
+
- **Install is synchronous** — the call returns once the version is downloaded,
|
|
257
|
+
published, and made current. A heavy first-time install (e.g. Chem) can take
|
|
258
|
+
minutes; if the HTTP call times out, just re-run it — install is idempotent, and
|
|
259
|
+
a version that finished installing server-side is activated instantly on retry.
|
|
260
|
+
- Multi-package `install`/`update` runs sequentially and prints a per-package
|
|
261
|
+
status table (`installed` / `noop` / `error`); a partial failure sets exit code 1.
|
|
262
|
+
- **`update` preserves auto-update tracking**: a package whose desired version is
|
|
263
|
+
`latest` is updated by re-resolving `latest` (it keeps auto-updating); a pinned
|
|
264
|
+
package gets pinned to the concrete registry-latest version. Note that a
|
|
265
|
+
locally-published dev build counts as outdated once the registry moves past its
|
|
266
|
+
base version, so `update --all` will replace it with the registry release.
|
|
267
|
+
- Status semantics: `noop` means "already at the requested latest"; installing an
|
|
268
|
+
explicit `--version` reports `installed` (with the published-version id) even
|
|
269
|
+
when that version was already current — the server re-activates it.
|
|
270
|
+
- **`uninstall` of a repository-backed package keeps the package entry** (it shows
|
|
271
|
+
up as installable again); only locally-published packages are deleted outright.
|
|
272
|
+
It also clears the desired version, so the package drops out of `outdated` until
|
|
273
|
+
reinstalled. Neither path cleans up package credentials or package DB schemas.
|
|
274
|
+
- `versions`, `outdated`, and name resolution read the repository catalog, so they
|
|
275
|
+
need a configured package repository — check with `grok s raw GET /api/packages/repos`.
|
|
276
|
+
- `share` targets the package's project under the hood (same as sharing from the UI),
|
|
277
|
+
so the whole package — functions, queries, connections — is shared at once. For the
|
|
278
|
+
same reason `grok s shares list <package-uuid>` won't show the grant — it lives on
|
|
279
|
+
the package's project, not the package entity itself.
|
|
280
|
+
|
|
229
281
|
## Server health
|
|
230
282
|
|
|
231
283
|
```bash
|
package/bin/commands/help.js
CHANGED
|
@@ -424,6 +424,14 @@ Special commands:
|
|
|
424
424
|
grok s connections test --json conn.json Test connectivity of a connection defined in JSON
|
|
425
425
|
grok s tables upload <name> <file.csv> Upload a CSV as a Datagrok table
|
|
426
426
|
grok s tables download <name-or-id> [-O <file>] Download a table as CSV (stdout by default)
|
|
427
|
+
grok s packages install <name>... [--version <v>] Install latest (or pinned) versions from the registry
|
|
428
|
+
grok s packages uninstall <name> Uninstall a package
|
|
429
|
+
grok s packages update <name>... | --all Upgrade to the latest registry version
|
|
430
|
+
grok s packages outdated Show installed vs latest registry versions
|
|
431
|
+
grok s packages versions <name> List published versions
|
|
432
|
+
grok s packages set-version <name> <version> Activate a specific published version
|
|
433
|
+
grok s packages share <name> <group> [--access View|Edit]
|
|
434
|
+
Share a package with groups
|
|
427
435
|
grok s raw <METHOD> <path> Hit any API endpoint
|
|
428
436
|
grok s describe <entity-type> Show entity JSON schema
|
|
429
437
|
|
|
@@ -449,6 +457,8 @@ Examples:
|
|
|
449
457
|
grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
|
|
450
458
|
grok s shares list <entity-uuid>
|
|
451
459
|
grok s functions run 'Chem:smilesToMw("ccc")'
|
|
460
|
+
grok s packages install Chem Bio PowerGrid
|
|
461
|
+
grok s packages update --all
|
|
452
462
|
grok s files list "System:AppData" -r
|
|
453
463
|
grok s raw GET /api/users/current
|
|
454
464
|
grok s describe connections
|
package/bin/commands/report.js
CHANGED
|
@@ -367,10 +367,10 @@ async function handleTicket(args) {
|
|
|
367
367
|
const issueType = args['type'] || 'Bug';
|
|
368
368
|
const auth = jiraAuthHeader();
|
|
369
369
|
if (auth == null) {
|
|
370
|
-
color.error('JIRA_USER
|
|
370
|
+
color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report ticket`.');
|
|
371
371
|
return false;
|
|
372
372
|
}
|
|
373
|
-
const jiraBase = resolveJiraBase(args);
|
|
373
|
+
const jiraBase = await resolveJiraBase(args);
|
|
374
374
|
try {
|
|
375
375
|
const {
|
|
376
376
|
url,
|
|
@@ -444,10 +444,23 @@ async function handleTicket(args) {
|
|
|
444
444
|
|
|
445
445
|
// ─── JIRA REST helpers (used by `grok report comment` / `grok report label`) ─
|
|
446
446
|
//
|
|
447
|
-
// These talk DIRECTLY to Atlassian Cloud REST v2 (not Datagrok).
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
447
|
+
// These talk DIRECTLY to Atlassian Cloud REST v2 (not Datagrok). Two auth schemes, picked from
|
|
448
|
+
// the token itself:
|
|
449
|
+
//
|
|
450
|
+
// * user API token (`ATATT...`, from id.atlassian.com/manage-profile/security/api-tokens) —
|
|
451
|
+
// HTTP Basic with `JIRA_USER` + `JIRA_TOKEN`, against the site. The original path.
|
|
452
|
+
// * service-account token (`ATSTT...`, from admin.atlassian.com) — a SCOPED token. The site
|
|
453
|
+
// host will not take it: Basic gives 401, and Bearer gives 403 "Failed to parse Connect
|
|
454
|
+
// Session Auth Token" because Jira tries to read it as a Connect session. Only the API
|
|
455
|
+
// gateway accepts it, addressed by cloud id, with Bearer. No JIRA_USER is involved.
|
|
456
|
+
//
|
|
457
|
+
// Both are supported on purpose. Automation moved to a service account when its bot mailbox
|
|
458
|
+
// became a Google group (a group holds no Atlassian identity and cannot mint tokens), while
|
|
459
|
+
// release-notes CI and every developer's local setup still use user tokens.
|
|
460
|
+
//
|
|
461
|
+
// Base URL defaults to the Datagrok org instance; override via --jira-url or $JIRA_URL. That
|
|
462
|
+
// value stays the SITE — the gateway address is derived from it, never substituted for it, so
|
|
463
|
+
// human-facing links elsewhere keep working.
|
|
451
464
|
//
|
|
452
465
|
// Why v2 and not v3: v3 requires comment bodies in ADF (Atlassian Document
|
|
453
466
|
// Format) JSON, which is much heavier to construct. v2 accepts a plain string
|
|
@@ -455,15 +468,49 @@ async function handleTicket(args) {
|
|
|
455
468
|
// becomes a top-level ordered-list item, ` ` shows up literally, etc.
|
|
456
469
|
// `markdownToJiraWiki` below bridges the gap for Markdown-emitting callers.
|
|
457
470
|
|
|
458
|
-
|
|
471
|
+
const JIRA_SERVICE_TOKEN_RE = /^ATSTT/;
|
|
472
|
+
function isJiraServiceToken() {
|
|
473
|
+
return JIRA_SERVICE_TOKEN_RE.test(process.env.JIRA_TOKEN || '');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// The site, as configured. Human-facing links are built from this.
|
|
477
|
+
function resolveJiraSite(args) {
|
|
459
478
|
const cli = args['jira-url'] || '';
|
|
460
479
|
const env = process.env.JIRA_URL || '';
|
|
461
480
|
return (cli || env || 'https://reddata.atlassian.net').replace(/\/+$/, '');
|
|
462
481
|
}
|
|
482
|
+
let cloudIdCache = null;
|
|
483
|
+
|
|
484
|
+
// The gateway addresses a site by cloud id rather than hostname. Public endpoint, no credentials
|
|
485
|
+
// needed, cached for the process.
|
|
486
|
+
async function resolveJiraCloudId(site) {
|
|
487
|
+
if (process.env.JIRA_CLOUD_ID) return process.env.JIRA_CLOUD_ID.trim();
|
|
488
|
+
if (cloudIdCache !== null) return cloudIdCache;
|
|
489
|
+
try {
|
|
490
|
+
const r = await fetch(`${site}/_edge/tenant_info`);
|
|
491
|
+
const j = r.ok ? await r.json() : {};
|
|
492
|
+
cloudIdCache = (j.cloudId || '').trim();
|
|
493
|
+
} catch {
|
|
494
|
+
cloudIdCache = '';
|
|
495
|
+
}
|
|
496
|
+
return cloudIdCache;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Where REST calls go. A service token needs the gateway; anything else keeps the site. If the
|
|
500
|
+
// cloud id cannot be resolved, fall back to the site rather than failing outright — the request
|
|
501
|
+
// then reports a normal auth error instead of a confusing lookup one.
|
|
502
|
+
async function resolveJiraBase(args) {
|
|
503
|
+
const site = resolveJiraSite(args);
|
|
504
|
+
if (!isJiraServiceToken()) return site;
|
|
505
|
+
const cloudId = await resolveJiraCloudId(site);
|
|
506
|
+
return cloudId ? `https://api.atlassian.com/ex/jira/${cloudId}` : site;
|
|
507
|
+
}
|
|
463
508
|
function jiraAuthHeader() {
|
|
464
|
-
const user = process.env.JIRA_USER;
|
|
465
509
|
const token = process.env.JIRA_TOKEN;
|
|
466
|
-
if (!
|
|
510
|
+
if (!token) return null;
|
|
511
|
+
if (isJiraServiceToken()) return `Bearer ${token}`;
|
|
512
|
+
const user = process.env.JIRA_USER;
|
|
513
|
+
if (!user) return null;
|
|
467
514
|
return 'Basic ' + Buffer.from(`${user}:${token}`).toString('base64');
|
|
468
515
|
}
|
|
469
516
|
|
|
@@ -544,7 +591,7 @@ async function handleComment(args) {
|
|
|
544
591
|
}
|
|
545
592
|
const auth = jiraAuthHeader();
|
|
546
593
|
if (auth == null) {
|
|
547
|
-
color.error('JIRA_USER
|
|
594
|
+
color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report comment`.');
|
|
548
595
|
return false;
|
|
549
596
|
}
|
|
550
597
|
let body;
|
|
@@ -566,7 +613,7 @@ async function handleComment(args) {
|
|
|
566
613
|
color.error('Comment body is empty (use --body, --body-file, or pipe to stdin).');
|
|
567
614
|
return false;
|
|
568
615
|
}
|
|
569
|
-
const base = resolveJiraBase(args);
|
|
616
|
+
const base = await resolveJiraBase(args);
|
|
570
617
|
const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/comment`;
|
|
571
618
|
// Callers (especially the dg-fix-reports M2 handoff) emit Markdown, but JIRA
|
|
572
619
|
// REST v2 renders the body as wiki markup. Convert before posting so headings,
|
|
@@ -614,13 +661,15 @@ async function handleAttach(args) {
|
|
|
614
661
|
color.error(`File not found: ${filePath}`);
|
|
615
662
|
return false;
|
|
616
663
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
664
|
+
|
|
665
|
+
// Through the same helper as every other call: a service token authenticates with Bearer and
|
|
666
|
+
// has no JIRA_USER at all, so checking for one here would refuse a working setup.
|
|
667
|
+
const auth = jiraAuthHeader();
|
|
668
|
+
if (auth == null) {
|
|
669
|
+
color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report attach`.');
|
|
621
670
|
return false;
|
|
622
671
|
}
|
|
623
|
-
const base = resolveJiraBase(args);
|
|
672
|
+
const base = await resolveJiraBase(args);
|
|
624
673
|
const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/attachments`;
|
|
625
674
|
|
|
626
675
|
// node-fetch v2 has no built-in FormData and the codebase doesn't depend on
|
|
@@ -630,7 +679,7 @@ async function handleAttach(args) {
|
|
|
630
679
|
const {
|
|
631
680
|
spawnSync
|
|
632
681
|
} = require('child_process');
|
|
633
|
-
const r = spawnSync('curl', ['-sS', '-X', 'POST', '-
|
|
682
|
+
const r = spawnSync('curl', ['-sS', '-X', 'POST', '-H', `Authorization: ${auth}`, '-H', 'X-Atlassian-Token: no-check', '-F', `file=@${filePath}`, '-w', '\n%{http_code}\n', url], {
|
|
634
683
|
encoding: 'utf8',
|
|
635
684
|
timeout: 120_000,
|
|
636
685
|
maxBuffer: 4 * 1024 * 1024
|
|
@@ -671,10 +720,10 @@ async function handleLabel(args) {
|
|
|
671
720
|
}
|
|
672
721
|
const auth = jiraAuthHeader();
|
|
673
722
|
if (auth == null) {
|
|
674
|
-
color.error('JIRA_USER
|
|
723
|
+
color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report label`.');
|
|
675
724
|
return false;
|
|
676
725
|
}
|
|
677
|
-
const base = resolveJiraBase(args);
|
|
726
|
+
const base = await resolveJiraBase(args);
|
|
678
727
|
const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}`;
|
|
679
728
|
const update = {
|
|
680
729
|
labels: labels.map(l => ({
|
package/bin/commands/server.js
CHANGED
|
@@ -41,13 +41,15 @@ async function server(argv) {
|
|
|
41
41
|
}
|
|
42
42
|
const dapi = new _nodeDapi.NodeDapi(client);
|
|
43
43
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (entity === '
|
|
47
|
-
if (entity === '
|
|
48
|
-
if (entity === '
|
|
49
|
-
if (entity === '
|
|
50
|
-
if (entity === '
|
|
44
|
+
// await each handler so a rejection lands in the catch below instead of
|
|
45
|
+
// escaping to grok.js as an unhandled rejection (stack trace + exit 255)
|
|
46
|
+
if (entity === 'batch') return await handleBatch(dapi, argv, verb, rest, output);
|
|
47
|
+
if (entity === 'raw') return await handleRaw(dapi, verb, rest, output);
|
|
48
|
+
if (entity === 'describe') return await handleDescribe(dapi, verb ?? rest[0], output);
|
|
49
|
+
if (entity === 'healthcheck') return await handleHealthcheck(dapi, argv, output);
|
|
50
|
+
if (entity === 'sync') return await handleSync(dapi, verb, rest, argv, output);
|
|
51
|
+
if (entity === 'functions' && verb === 'run') return await handleFuncRun(dapi, rest, argv, output);
|
|
52
|
+
if (entity === 'functions' && verb === 'list') return await handleFunctionsList(dapi, argv, limit, offset, filter, output);
|
|
51
53
|
if (entity === 'files' && verb === 'list') {
|
|
52
54
|
const path = rest[0] ?? '';
|
|
53
55
|
const result = await dapi.files.list(path, recursive);
|
|
@@ -64,21 +66,28 @@ async function server(argv) {
|
|
|
64
66
|
if (output !== 'quiet') console.log(`Deleted ${rest[0]}`);
|
|
65
67
|
return true;
|
|
66
68
|
}
|
|
67
|
-
if (entity === 'files' && verb === 'put') return handleFilesPut(dapi, rest, output);
|
|
68
|
-
if (entity === 'shares' && verb === 'add') return handleSharesAdd(dapi, rest, argv, output);
|
|
69
|
-
if (entity === 'shares' && verb === 'list') return handleSharesList(dapi, rest, output);
|
|
70
|
-
if (entity === 'users' && verb === 'save') return handleUserSave(dapi, argv, output);
|
|
71
|
-
if (entity === 'groups' && verb === 'save') return handleGroupSave(dapi, argv, output);
|
|
72
|
-
if (entity === 'connections' && verb === 'save') return handleConnSave(dapi, argv, output);
|
|
73
|
-
if (entity === 'connections' && verb === 'test') return handleConnTest(dapi, rest, argv, output);
|
|
74
|
-
if (entity === 'groups' && verb === 'add-members') return handleGroupAddMembers(dapi, rest, argv, output);
|
|
75
|
-
if (entity === 'groups' && verb === 'remove-members') return handleGroupRemoveMembers(dapi, rest, argv, output);
|
|
76
|
-
if (entity === 'groups' && verb === 'list-members') return handleGroupListMembers(dapi, rest, argv, output);
|
|
77
|
-
if (entity === 'groups' && verb === 'list-memberships') return handleGroupListMemberships(dapi, rest, argv, output);
|
|
78
|
-
if (entity === 'users' && verb === 'block') return handleUserBlock(dapi, rest, output);
|
|
79
|
-
if (entity === 'users' && verb === 'unblock') return handleUserUnblock(dapi, rest, output);
|
|
80
|
-
if (entity === 'tables' && verb === 'download') return handleTablesDownload(dapi, rest, argv, output);
|
|
81
|
-
if (entity === 'tables' && verb === 'upload') return handleTablesUpload(dapi, rest, output);
|
|
69
|
+
if (entity === 'files' && verb === 'put') return await handleFilesPut(dapi, rest, output);
|
|
70
|
+
if (entity === 'shares' && verb === 'add') return await handleSharesAdd(dapi, rest, argv, output);
|
|
71
|
+
if (entity === 'shares' && verb === 'list') return await handleSharesList(dapi, rest, output);
|
|
72
|
+
if (entity === 'users' && verb === 'save') return await handleUserSave(dapi, argv, output);
|
|
73
|
+
if (entity === 'groups' && verb === 'save') return await handleGroupSave(dapi, argv, output);
|
|
74
|
+
if (entity === 'connections' && verb === 'save') return await handleConnSave(dapi, argv, output);
|
|
75
|
+
if (entity === 'connections' && verb === 'test') return await handleConnTest(dapi, rest, argv, output);
|
|
76
|
+
if (entity === 'groups' && verb === 'add-members') return await handleGroupAddMembers(dapi, rest, argv, output);
|
|
77
|
+
if (entity === 'groups' && verb === 'remove-members') return await handleGroupRemoveMembers(dapi, rest, argv, output);
|
|
78
|
+
if (entity === 'groups' && verb === 'list-members') return await handleGroupListMembers(dapi, rest, argv, output);
|
|
79
|
+
if (entity === 'groups' && verb === 'list-memberships') return await handleGroupListMemberships(dapi, rest, argv, output);
|
|
80
|
+
if (entity === 'users' && verb === 'block') return await handleUserBlock(dapi, rest, output);
|
|
81
|
+
if (entity === 'users' && verb === 'unblock') return await handleUserUnblock(dapi, rest, output);
|
|
82
|
+
if (entity === 'tables' && verb === 'download') return await handleTablesDownload(dapi, rest, argv, output);
|
|
83
|
+
if (entity === 'tables' && verb === 'upload') return await handleTablesUpload(dapi, rest, output);
|
|
84
|
+
if (entity === 'packages' && verb === 'install') return await handlePackagesInstall(dapi, rest, argv, output);
|
|
85
|
+
if (entity === 'packages' && verb === 'uninstall') return await handlePackagesUninstall(dapi, rest, output);
|
|
86
|
+
if (entity === 'packages' && verb === 'share') return await handlePackagesShare(dapi, rest, argv, output);
|
|
87
|
+
if (entity === 'packages' && verb === 'versions') return await handlePackagesVersions(dapi, rest, output);
|
|
88
|
+
if (entity === 'packages' && verb === 'set-version') return await handlePackagesSetVersion(dapi, rest, output);
|
|
89
|
+
if (entity === 'packages' && verb === 'outdated') return await handlePackagesOutdated(dapi, output);
|
|
90
|
+
if (entity === 'packages' && verb === 'update') return await handlePackagesUpdate(dapi, rest, argv, output);
|
|
82
91
|
const source = dapi[entity];
|
|
83
92
|
if (!source || !ENTITIES.includes(entity)) {
|
|
84
93
|
(0, _serverOutput.printError)(new Error(`Unknown entity type: '${entity}'. Valid: ${ENTITIES.join(', ')}`));
|
|
@@ -112,7 +121,8 @@ async function server(argv) {
|
|
|
112
121
|
if (output !== 'quiet') console.log(`Deleted ${entity}/${rest[0]}`);
|
|
113
122
|
return true;
|
|
114
123
|
}
|
|
115
|
-
|
|
124
|
+
const extraVerbs = entity === 'packages' ? ', install, uninstall, update, outdated, versions, set-version, share' : '';
|
|
125
|
+
(0, _serverOutput.printError)(new Error(`Unknown verb: '${verb}'. Valid: ${VERBS.join(', ')}${extraVerbs}`));
|
|
116
126
|
return false;
|
|
117
127
|
} catch (err) {
|
|
118
128
|
(0, _serverOutput.printError)(err);
|
|
@@ -168,6 +178,208 @@ async function handleTablesDownload(dapi, rest, argv, output) {
|
|
|
168
178
|
} else process.stdout.write(csv);
|
|
169
179
|
return true;
|
|
170
180
|
}
|
|
181
|
+
async function installPackages(dapi, targets, output) {
|
|
182
|
+
const results = [];
|
|
183
|
+
for (const t of targets) {
|
|
184
|
+
let name = t.name;
|
|
185
|
+
try {
|
|
186
|
+
const pkg = await dapi.packages.resolve(t.name);
|
|
187
|
+
if (pkg) name = pkg.name;
|
|
188
|
+
if (output === 'table') console.log(`Installing ${name} (${t.version})...`);
|
|
189
|
+
const id = await dapi.packages.install(name, t.version);
|
|
190
|
+
// the server returns the published id for any existing explicit version
|
|
191
|
+
// (even an already-current one), so null means the version doesn't exist
|
|
192
|
+
if (!id && t.version !== 'latest') results.push({
|
|
193
|
+
package: name,
|
|
194
|
+
version: t.version,
|
|
195
|
+
status: 'error',
|
|
196
|
+
error: `Version '${t.version}' not found`
|
|
197
|
+
});else results.push({
|
|
198
|
+
package: name,
|
|
199
|
+
version: t.version,
|
|
200
|
+
status: id ? 'installed' : 'noop',
|
|
201
|
+
id: id ?? undefined
|
|
202
|
+
});
|
|
203
|
+
} catch (err) {
|
|
204
|
+
results.push({
|
|
205
|
+
package: name,
|
|
206
|
+
version: t.version,
|
|
207
|
+
status: 'error',
|
|
208
|
+
error: (err?.message ?? String(err)).replace(/\s*\n\s*/g, '; ')
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return results;
|
|
213
|
+
}
|
|
214
|
+
function printPackageOps(results, output) {
|
|
215
|
+
if (output === 'quiet') {
|
|
216
|
+
for (const r of results) if (r.id) console.log(r.id);
|
|
217
|
+
} else (0, _serverOutput.printOutput)(results, output);
|
|
218
|
+
if (results.some(r => r.status === 'error')) process.exitCode = 1;
|
|
219
|
+
}
|
|
220
|
+
async function handlePackagesInstall(dapi, rest, argv, output) {
|
|
221
|
+
if (!rest.length) {
|
|
222
|
+
(0, _serverOutput.printError)(new Error('Usage: grok s packages install <name> [<name> ...] [--version <v>]'));
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (argv.version !== undefined && rest.length > 1) {
|
|
226
|
+
(0, _serverOutput.printError)(new Error('--version applies to a single package'));
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const version = argv.version !== undefined ? String(argv.version) : 'latest';
|
|
230
|
+
const results = await installPackages(dapi, rest.map(r => ({
|
|
231
|
+
name: String(r),
|
|
232
|
+
version
|
|
233
|
+
})), output);
|
|
234
|
+
printPackageOps(results, output);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
async function handlePackagesUninstall(dapi, rest, output) {
|
|
238
|
+
if (!rest[0]) {
|
|
239
|
+
(0, _serverOutput.printError)(new Error('Usage: grok s packages uninstall <name-or-id>'));
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
const result = await dapi.packages.uninstall(rest[0]);
|
|
243
|
+
if (output === 'json' || output === 'csv') (0, _serverOutput.printOutput)(result, output);else if (output === 'quiet') console.log(result.id);else console.log(result.repoBacked ? `Uninstalled ${result.name} (package entry kept — reinstall with 'grok s packages install ${result.name}')` : `Deleted local package ${result.name}`);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
async function handlePackagesShare(dapi, rest, argv, output) {
|
|
247
|
+
const [name, ...groupArgs] = rest;
|
|
248
|
+
if (!name || !groupArgs.length) {
|
|
249
|
+
(0, _serverOutput.printError)(new Error('Usage: grok s packages share <name-or-id> <group>[,<group>...] [--access View|Edit]'));
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const groups = groupArgs.flatMap(g => g.split(',')).map(g => g.trim()).filter(Boolean).join(',');
|
|
253
|
+
const access = typeof argv.access === 'string' ? argv.access : 'View';
|
|
254
|
+
if (access !== 'View' && access !== 'Edit') {
|
|
255
|
+
(0, _serverOutput.printError)(new Error(`Invalid --access '${access}'. Use 'View' or 'Edit'.`));
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
const pkg = await dapi.packages.resolve(name);
|
|
259
|
+
if (!pkg) {
|
|
260
|
+
(0, _serverOutput.printError)(new Error(`Package '${name}' not found`));
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const result = await dapi.shares.share(pkg.id, groups, access);
|
|
264
|
+
(0, _serverOutput.printOutput)(result, output);
|
|
265
|
+
if (result?.status === 'failed') process.exitCode = 1;
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
async function handlePackagesVersions(dapi, rest, output) {
|
|
269
|
+
if (!rest[0]) {
|
|
270
|
+
(0, _serverOutput.printError)(new Error('Usage: grok s packages versions <name-or-id>'));
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const versions = await dapi.packages.versions(rest[0]);
|
|
274
|
+
const flat = versions.map(v => ({
|
|
275
|
+
version: v?.version ?? '',
|
|
276
|
+
current: v?.isCurrent ?? false,
|
|
277
|
+
latest: v?.isLatest ?? false,
|
|
278
|
+
local: v?.isLocal ?? false,
|
|
279
|
+
remote: v?.isRemote ?? false,
|
|
280
|
+
debug: v?.debug ?? false,
|
|
281
|
+
id: v?.id ?? ''
|
|
282
|
+
}));
|
|
283
|
+
(0, _serverOutput.printOutput)(flat, output);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
async function handlePackagesSetVersion(dapi, rest, output) {
|
|
287
|
+
if (!rest[0] || rest[1] === undefined) {
|
|
288
|
+
(0, _serverOutput.printError)(new Error('Usage: grok s packages set-version <name> <version>'));
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
const version = String(rest[1]);
|
|
292
|
+
const pkg = await dapi.packages.resolve(String(rest[0]));
|
|
293
|
+
const name = pkg?.name ?? String(rest[0]);
|
|
294
|
+
const id = await dapi.packages.install(name, version);
|
|
295
|
+
if (!id) {
|
|
296
|
+
(0, _serverOutput.printError)(new Error(`Version '${version}' not found for package '${name}'`));
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (output === 'quiet') console.log(id);else if (output === 'json' || output === 'csv') (0, _serverOutput.printOutput)({
|
|
300
|
+
package: name,
|
|
301
|
+
version,
|
|
302
|
+
id
|
|
303
|
+
}, output);else console.log(`${name} -> ${version} (published package ${id})`);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
async function handlePackagesOutdated(dapi, output) {
|
|
307
|
+
const rows = await dapi.packages.outdated();
|
|
308
|
+
if (!rows.length && output === 'table') {
|
|
309
|
+
console.log('All packages up to date.');
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
(0, _serverOutput.printOutput)(rows, output);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
async function handlePackagesUpdate(dapi, rest, argv, output) {
|
|
316
|
+
const all = argv.all === true;
|
|
317
|
+
if (!all && !rest.length) {
|
|
318
|
+
(0, _serverOutput.printError)(new Error('Usage: grok s packages update <name> [<name> ...] | --all'));
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
const outdated = await dapi.packages.outdated();
|
|
322
|
+
const results = [];
|
|
323
|
+
// a package tracking 'latest' keeps that auto-update intent; a pinned one gets
|
|
324
|
+
// pinned to the concrete registry-latest version
|
|
325
|
+
const targetVersion = o => o.desiredVersion === 'latest' ? 'latest' : o.latest;
|
|
326
|
+
let targets;
|
|
327
|
+
if (all) {
|
|
328
|
+
targets = outdated.map(o => ({
|
|
329
|
+
name: o.name,
|
|
330
|
+
version: targetVersion(o)
|
|
331
|
+
}));
|
|
332
|
+
if (!targets.length && output === 'table') {
|
|
333
|
+
console.log('All packages up to date.');
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
targets = [];
|
|
338
|
+
for (const r of rest) {
|
|
339
|
+
const raw = String(r);
|
|
340
|
+
let pkg = null;
|
|
341
|
+
try {
|
|
342
|
+
pkg = await dapi.packages.resolve(raw);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
results.push({
|
|
345
|
+
package: raw,
|
|
346
|
+
version: '',
|
|
347
|
+
status: 'error',
|
|
348
|
+
error: (err?.message ?? String(err)).replace(/\s*\n\s*/g, '; ')
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (!pkg) {
|
|
353
|
+
results.push({
|
|
354
|
+
package: raw,
|
|
355
|
+
version: '',
|
|
356
|
+
status: 'error',
|
|
357
|
+
error: 'Package not found'
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const o = outdated.find(x => x.name === pkg.name);
|
|
362
|
+
if (o) {
|
|
363
|
+
targets.push({
|
|
364
|
+
name: pkg.name,
|
|
365
|
+
version: targetVersion(o)
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const versions = Array.isArray(pkg.publishedVersions) ? pkg.publishedVersions : [];
|
|
370
|
+
const installed = versions.some(v => v?.isCurrent && !v?.debug);
|
|
371
|
+
results.push({
|
|
372
|
+
package: pkg.name,
|
|
373
|
+
version: '',
|
|
374
|
+
status: 'noop',
|
|
375
|
+
note: installed ? 'already up to date' : 'not installed'
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
results.push(...(await installPackages(dapi, targets, output)));
|
|
380
|
+
printPackageOps(results, output);
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
171
383
|
async function handleTablesUpload(dapi, rest, output) {
|
|
172
384
|
const [name, localPath] = rest;
|
|
173
385
|
if (!name || !localPath) {
|
|
@@ -699,6 +911,14 @@ Special commands:
|
|
|
699
911
|
grok s users unblock <id-or-login> Unblock a previously blocked user
|
|
700
912
|
grok s tables upload <name> <file.csv|file.d42> Upload a CSV or d42 binary as a Datagrok table
|
|
701
913
|
grok s tables download <name-or-id> [-O <file>] Download a table as CSV (stdout by default)
|
|
914
|
+
grok s packages install <name>... [--version <v>] Install latest (or pinned) versions from the registry
|
|
915
|
+
grok s packages uninstall <name> Uninstall a package (repository entry is kept)
|
|
916
|
+
grok s packages update <name>... | --all Upgrade to the latest registry version
|
|
917
|
+
grok s packages outdated Show installed vs latest registry versions
|
|
918
|
+
grok s packages versions <name> List published versions (current/latest/debug flags)
|
|
919
|
+
grok s packages set-version <name> <version> Activate a specific published version
|
|
920
|
+
grok s packages share <name> <group>[,...] [--access View|Edit]
|
|
921
|
+
Share a package with one or more groups
|
|
702
922
|
grok s batch <entity> <verb> arg1 [arg2 ...] Batch operation (one round-trip)
|
|
703
923
|
grok s batch <entity> <verb> --json params.json Batch from JSON array
|
|
704
924
|
grok s batch manifest.json Run a workflow manifest
|
|
@@ -719,6 +939,8 @@ Options:
|
|
|
719
939
|
--type <t> Function discriminator: script | query | function | package
|
|
720
940
|
--language <lang> Script language: python, r, julia, nodejs, octave, grok
|
|
721
941
|
--package <name> Restrict to functions belonging to a package (by short name)
|
|
942
|
+
--version <v> Package version for 'packages install' (default: latest)
|
|
943
|
+
--all For 'packages update': upgrade every outdated package
|
|
722
944
|
|
|
723
945
|
Batch manifest options (in manifest.json):
|
|
724
946
|
stopOnError Stop on first failure (default: true)
|
|
@@ -745,6 +967,11 @@ Examples:
|
|
|
745
967
|
grok s functions list --package PowerPack --type package --filter 'name contains "grid"'
|
|
746
968
|
grok s files list "System:AppData" -r
|
|
747
969
|
grok s files put ./smiles.csv "System:DemoFiles/smiles.csv"
|
|
970
|
+
grok s packages install Chem Bio PowerGrid
|
|
971
|
+
grok s packages install Chem --version 1.14.0
|
|
972
|
+
grok s packages update --all
|
|
973
|
+
grok s packages versions Chem
|
|
974
|
+
grok s packages share Chem Chemists --access View
|
|
748
975
|
grok s raw GET /api/users/current
|
|
749
976
|
grok s describe connections
|
|
750
977
|
grok s users list --host dev
|
package/bin/grok.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
const argv = require('minimist')(process.argv.slice(2), {
|
|
3
3
|
alias: {k: 'key', h: 'help', r: 'recursive'},
|
|
4
4
|
boolean: ['dartium'],
|
|
5
|
+
// keep versions verbatim — minimist would coerce '1.10' to the number 1.1
|
|
6
|
+
string: ['version'],
|
|
5
7
|
});
|
|
6
8
|
// minimist maps `--no-retry` to `{retry: false}`, so the `args['no-retry']` checks in
|
|
7
9
|
// test.ts / playwright-runner.ts never fired and `--no-retry` was silently ignored
|
package/bin/utils/node-dapi.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.NodeUsersDataSource = exports.NodeTablesDataSource = exports.NodeSharesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = void 0;
|
|
6
|
+
exports.NodeUsersDataSource = exports.NodeTablesDataSource = exports.NodeSharesDataSource = exports.NodePackagesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = void 0;
|
|
7
7
|
exports.ensureBodyId = ensureBodyId;
|
|
8
8
|
var _crypto = require("crypto");
|
|
9
9
|
/// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
|
|
@@ -400,6 +400,101 @@ function tryParseJson(s) {
|
|
|
400
400
|
return null;
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
|
+
class NodePackagesDataSource extends NodeHttpDataSource {
|
|
404
|
+
constructor(client) {
|
|
405
|
+
super(client, 'packages');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** GET /packages (internal API) — includes publishedVersions and repository. */
|
|
409
|
+
async listFull(text) {
|
|
410
|
+
return this.client.get(`/packages${buildQuery({
|
|
411
|
+
text
|
|
412
|
+
})}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Resolve by UUID, name, or friendlyName (case-insensitive). Returns null when
|
|
416
|
+
not found so callers can pass the raw string through and let the server's own
|
|
417
|
+
'Package not found' surface. */
|
|
418
|
+
async resolve(idOrName) {
|
|
419
|
+
const all = await this.listFull();
|
|
420
|
+
if (UUID_RE.test(idOrName)) return all.find(p => p?.id === idOrName) ?? null;
|
|
421
|
+
const want = idOrName.toLowerCase();
|
|
422
|
+
const matches = all.filter(p => (p?.name ?? '').toLowerCase() === want || (p?.friendlyName ?? '').toLowerCase() === want);
|
|
423
|
+
if (matches.length > 1) {
|
|
424
|
+
const list = matches.map(p => ` ${p.id} ${p.name}`).join('\n');
|
|
425
|
+
throw new Error(`Multiple packages match '${idOrName}':\n${list}\nUse the ID to disambiguate.`);
|
|
426
|
+
}
|
|
427
|
+
return matches[0] ?? null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Install/activate via the DeployPackageVersion server func: pulls the version
|
|
431
|
+
from the package repository (npm) when needed, then makes it current.
|
|
432
|
+
'latest' marks the package for server-side auto-update. Synchronous — returns
|
|
433
|
+
the new published-package id, or null when nothing changed. */
|
|
434
|
+
async install(name, desiredVersion = 'latest') {
|
|
435
|
+
const result = await new NodeFuncsDataSource(this.client, 'functions').run('DeployPackageVersion', {
|
|
436
|
+
name,
|
|
437
|
+
desiredVersion
|
|
438
|
+
});
|
|
439
|
+
const value = result && typeof result === 'object' ? result.result ?? result.id ?? null : result;
|
|
440
|
+
const text = typeof value === 'string' ? value.replace(/^"|"$/g, '') : '';
|
|
441
|
+
return text && text !== 'null' ? text : null;
|
|
442
|
+
}
|
|
443
|
+
async uninstall(idOrName) {
|
|
444
|
+
const pkg = await this.resolve(idOrName);
|
|
445
|
+
if (!pkg) throw new Error(`Package '${idOrName}' not found`);
|
|
446
|
+
await this.client.del(`/packages/${encodeURIComponent(pkg.id)}`);
|
|
447
|
+
return {
|
|
448
|
+
id: pkg.id,
|
|
449
|
+
name: pkg.name,
|
|
450
|
+
repoBacked: !!pkg.repository
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
async versions(idOrName) {
|
|
454
|
+
const pkg = await this.resolve(idOrName);
|
|
455
|
+
if (!pkg) throw new Error(`Package '${idOrName}' not found`);
|
|
456
|
+
return Array.isArray(pkg.publishedVersions) ? pkg.publishedVersions : [];
|
|
457
|
+
}
|
|
458
|
+
async outdated() {
|
|
459
|
+
const all = await this.listFull();
|
|
460
|
+
const rows = [];
|
|
461
|
+
for (const p of all) {
|
|
462
|
+
const versions = Array.isArray(p?.publishedVersions) ? p.publishedVersions : [];
|
|
463
|
+
// isLocal is not reliably serialized, so 'installed' = has a current non-debug version
|
|
464
|
+
const current = versions.find(v => v?.isCurrent && !v?.debug);
|
|
465
|
+
const latest = versions.find(v => v?.isLatest);
|
|
466
|
+
// Numeric compare so locally published builds (1.2.3.X-suffix) don't count
|
|
467
|
+
// as outdated against an equal or older registry version.
|
|
468
|
+
if (current && latest && NodePackagesDataSource.compareVersions(latest.version, current.version) > 0) rows.push({
|
|
469
|
+
name: p.name,
|
|
470
|
+
installed: current.version,
|
|
471
|
+
latest: latest.version,
|
|
472
|
+
desiredVersion: p.desiredVersion ?? ''
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return rows;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Compare the leading numeric dot-groups of two version strings (suffixes like
|
|
479
|
+
'.X-4e32bb91' are ignored): positive when a > b. */
|
|
480
|
+
static compareVersions(a, b) {
|
|
481
|
+
const nums = s => (String(s).match(/^\d+(\.\d+)*/)?.[0] ?? '').split('.').map(Number);
|
|
482
|
+
const av = nums(a),
|
|
483
|
+
bv = nums(b);
|
|
484
|
+
for (let i = 0; i < Math.max(av.length, bv.length); i++) {
|
|
485
|
+
const d = (av[i] ?? 0) - (bv[i] ?? 0);
|
|
486
|
+
if (d !== 0) return d;
|
|
487
|
+
}
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** The public API has no DELETE for packages — route to the internal endpoint. */
|
|
492
|
+
async delete(idOrEntity) {
|
|
493
|
+
const id = typeof idOrEntity === 'string' ? idOrEntity : idOrEntity?.id ?? '';
|
|
494
|
+
await this.client.del(`/packages/${encodeURIComponent(id)}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
exports.NodePackagesDataSource = NodePackagesDataSource;
|
|
403
498
|
class NodeFilesDataSource {
|
|
404
499
|
constructor(client) {
|
|
405
500
|
this.client = client;
|
|
@@ -538,7 +633,7 @@ class NodeDapi {
|
|
|
538
633
|
return new NodeHttpDataSource(this.client, 'scripts');
|
|
539
634
|
}
|
|
540
635
|
get packages() {
|
|
541
|
-
return new
|
|
636
|
+
return new NodePackagesDataSource(this.client);
|
|
542
637
|
}
|
|
543
638
|
get reports() {
|
|
544
639
|
return new NodeHttpDataSource(this.client, 'reports');
|
|
@@ -82,6 +82,49 @@ function rowsToCsv(rows) {
|
|
|
82
82
|
data: rows.map(r => header.map(h => r[h]))
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
|
+
function writePlaywrightCsv(pkgDir, csv) {
|
|
86
|
+
// Persist a Playwright-only CSV so the pipeline can ship it to the Datlas
|
|
87
|
+
// 'playwright' bucket, separate from the merged Puppeteer+Playwright
|
|
88
|
+
// test-report.csv that feeds the legacy 'package' bucket and JUnit.
|
|
89
|
+
try {
|
|
90
|
+
_fs.default.writeFileSync(_path.default.join(pkgDir, 'test-report-playwright.csv'), csv, 'utf-8');
|
|
91
|
+
} catch (e) {
|
|
92
|
+
color.warn(`Playwright: failed to write test-report-playwright.csv: ${e.message || e}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A pass that dies before running specs still has to report a failure. Returning an empty CSV
|
|
97
|
+
// let the caller drop it whenever the Puppeteer pass had rows, so a suite that never launched
|
|
98
|
+
// (a config that fails to load, an unreachable host) was reported as a clean run.
|
|
99
|
+
function runnerFailure(pkgDir, args, message) {
|
|
100
|
+
const rows = [{
|
|
101
|
+
date: new Date().toISOString(),
|
|
102
|
+
category: 'playwright',
|
|
103
|
+
name: 'Playwright suite',
|
|
104
|
+
success: false,
|
|
105
|
+
result: message,
|
|
106
|
+
ms: 0,
|
|
107
|
+
skipped: false,
|
|
108
|
+
logs: '',
|
|
109
|
+
owner: '',
|
|
110
|
+
package: process.env.TARGET_PACKAGE || args.package || '',
|
|
111
|
+
widgetsDifference: '',
|
|
112
|
+
flaking: false
|
|
113
|
+
}];
|
|
114
|
+
const csv = rowsToCsv(rows);
|
|
115
|
+
writePlaywrightCsv(pkgDir, csv);
|
|
116
|
+
color.error(`Playwright: ${message}`);
|
|
117
|
+
return {
|
|
118
|
+
failed: true,
|
|
119
|
+
passedAmount: 0,
|
|
120
|
+
failedAmount: 1,
|
|
121
|
+
skippedAmount: 0,
|
|
122
|
+
verbosePassed: '',
|
|
123
|
+
verboseSkipped: '',
|
|
124
|
+
verboseFailed: `Playwright: ${message}\n`,
|
|
125
|
+
csv: csv
|
|
126
|
+
};
|
|
127
|
+
}
|
|
85
128
|
async function runPlaywrightTests(pkgDir, testDir, args, hostKey) {
|
|
86
129
|
const empty = {
|
|
87
130
|
failed: false,
|
|
@@ -101,25 +144,13 @@ async function runPlaywrightTests(pkgDir, testDir, args, hostKey) {
|
|
|
101
144
|
key
|
|
102
145
|
} = testUtils.getDevKey(hostKey));
|
|
103
146
|
} catch (e) {
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
...empty,
|
|
107
|
-
failed: true,
|
|
108
|
-
failedAmount: 1,
|
|
109
|
-
verboseFailed: `Playwright: ${e.message || e}\n`
|
|
110
|
-
};
|
|
147
|
+
return runnerFailure(pkgDir, args, `cannot resolve host '${hostKey}': ${e.message || e}`);
|
|
111
148
|
}
|
|
112
149
|
let token;
|
|
113
150
|
try {
|
|
114
151
|
token = await testUtils.getToken(url, key);
|
|
115
152
|
} catch (e) {
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
...empty,
|
|
119
|
-
failed: true,
|
|
120
|
-
failedAmount: 1,
|
|
121
|
-
verboseFailed: `Playwright: ${e.message || e}\n`
|
|
122
|
-
};
|
|
153
|
+
return runnerFailure(pkgDir, args, `cannot exchange dev key for token: ${e.message || e}`);
|
|
123
154
|
}
|
|
124
155
|
let webUrl;
|
|
125
156
|
try {
|
|
@@ -137,15 +168,7 @@ async function runPlaywrightTests(pkgDir, testDir, args, hostKey) {
|
|
|
137
168
|
}
|
|
138
169
|
}
|
|
139
170
|
const configPath = _path.default.join(testDir, 'playwright.config.ts');
|
|
140
|
-
if (!_fs.default.existsSync(configPath)) {
|
|
141
|
-
color.error(`Playwright: ${configPath} not found.`);
|
|
142
|
-
return {
|
|
143
|
-
...empty,
|
|
144
|
-
failed: true,
|
|
145
|
-
failedAmount: 1,
|
|
146
|
-
verboseFailed: 'Playwright: missing playwright.config.ts\n'
|
|
147
|
-
};
|
|
148
|
-
}
|
|
171
|
+
if (!_fs.default.existsSync(configPath)) return runnerFailure(pkgDir, args, `${configPath} not found`);
|
|
149
172
|
const reportFile = _path.default.join(pkgDir, 'test-playwright-report.json');
|
|
150
173
|
if (_fs.default.existsSync(reportFile)) _fs.default.unlinkSync(reportFile);
|
|
151
174
|
const cliArgs = ['--no-install', 'playwright', 'test', `--config=${configPath}`];
|
|
@@ -219,20 +242,18 @@ async function runPlaywrightTests(pkgDir, testDir, args, hostKey) {
|
|
|
219
242
|
} catch {/* ignore */}
|
|
220
243
|
}
|
|
221
244
|
if (!report) {
|
|
222
|
-
color.error('Playwright: no JSON report produced.');
|
|
223
245
|
const tail = Buffer.concat(stderrChunks).toString('utf-8').slice(-2000);
|
|
224
|
-
return {
|
|
225
|
-
...empty,
|
|
226
|
-
failed: true,
|
|
227
|
-
failedAmount: 1,
|
|
228
|
-
verboseFailed: `Playwright: no JSON report. stderr tail:\n${tail}\n`
|
|
229
|
-
};
|
|
246
|
+
return runnerFailure(pkgDir, args, `no JSON report produced — the suite never ran. stderr tail:\n${tail}`);
|
|
230
247
|
}
|
|
231
248
|
const pkgJson = JSON.parse(_fs.default.readFileSync(_path.default.join(pkgDir, 'package.json'), 'utf-8'));
|
|
232
249
|
const owner = pkgJson.author && (pkgJson.author.email || pkgJson.author) || '';
|
|
233
250
|
const pkgName = process.env.TARGET_PACKAGE || args.package || pkgJson.name || '';
|
|
234
251
|
const rows = [];
|
|
235
252
|
flattenSuites(report.suites, testDir, pkgName, typeof owner === 'string' ? owner : '', args.verbose === true, rows);
|
|
253
|
+
|
|
254
|
+
// A report with no specs is the same non-event as no report at all: the config loaded but
|
|
255
|
+
// collection found nothing, or died on a spec that cannot be imported.
|
|
256
|
+
if (rows.length === 0) return runnerFailure(pkgDir, args, 'the run produced a report with no specs — check the spec imports and testMatch');
|
|
236
257
|
let passedAmount = 0;
|
|
237
258
|
let failedAmount = 0;
|
|
238
259
|
let skippedAmount = 0;
|
|
@@ -253,14 +274,7 @@ async function runPlaywrightTests(pkgDir, testDir, args, hostKey) {
|
|
|
253
274
|
}
|
|
254
275
|
}
|
|
255
276
|
const csv = rowsToCsv(rows);
|
|
256
|
-
|
|
257
|
-
// 'playwright' bucket, separate from the merged Puppeteer+Playwright
|
|
258
|
-
// test-report.csv that feeds the legacy 'package' bucket and JUnit.
|
|
259
|
-
try {
|
|
260
|
-
_fs.default.writeFileSync(_path.default.join(pkgDir, 'test-report-playwright.csv'), csv, 'utf-8');
|
|
261
|
-
} catch (e) {
|
|
262
|
-
color.warn(`Playwright: failed to write test-report-playwright.csv: ${e.message || e}`);
|
|
263
|
-
}
|
|
277
|
+
writePlaywrightCsv(pkgDir, csv);
|
|
264
278
|
return {
|
|
265
279
|
failed: failedAmount > 0,
|
|
266
280
|
passedAmount: passedAmount,
|
package/package.json
CHANGED