datagrok-tools 6.5.2 → 6.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Datagrok-tools changelog
2
2
 
3
+ ## 6.5.4 (2026-08-07)
4
+
5
+ * func-gen-plugin — `#meta.comparison` in an `.ivp` model now lands as the `comparison` option on the generated dataframe output (run comparison reads index/split/mode/units defaults from there) instead of function-level meta.
6
+
7
+ ## 6.5.3 (2026-08-04)
8
+
9
+ * GROK-18695: Security — bumped `adm-zip` to 0.6.0 (CVE-2026-39244). Below 0.6.0 it sized a buffer from the ZIP header's declared uncompressed size before validating it, so a ~120-byte crafted archive forced a multi-GB allocation; `grok report` opens archives fetched from a server. The APIs it uses are unchanged. Also refreshed the lockfile to clear five more high-severity audit findings (`brace-expansion`, `fast-uri`, `ip-address`, `js-yaml`, `postcss`), all within existing ranges.
10
+
3
11
  ## 6.5.2 (2026-07-27)
4
12
 
5
13
  * `grok publish` — a failed `docker push` is no longer reported as a successful one. `image.json` claimed the tag regardless, so the server recorded a container image that was never published and the spawner failed validation forever ("Image ... not found in any registry") with no recovery short of another publish. The push failure now falls back to an image that is actually in the registry, and says so.
@@ -45,6 +45,7 @@ async function server(argv) {
45
45
  if (entity === 'raw') return handleRaw(dapi, verb, rest, output);
46
46
  if (entity === 'describe') return handleDescribe(dapi, verb ?? rest[0], output);
47
47
  if (entity === 'healthcheck') return handleHealthcheck(dapi, argv, output);
48
+ if (entity === 'sync') return handleSync(dapi, verb, rest, argv, output);
48
49
  if (entity === 'functions' && verb === 'run') return handleFuncRun(dapi, rest, argv, output);
49
50
  if (entity === 'functions' && verb === 'list') return handleFunctionsList(dapi, argv, limit, offset, filter, output);
50
51
  if (entity === 'files' && verb === 'list') {
@@ -235,6 +236,89 @@ async function handleRaw(dapi, method, rest, output) {
235
236
  (0, _serverOutput.printOutput)(result, output);
236
237
  return true;
237
238
  }
239
+ async function handleSync(dapi, subject, rest, argv, output) {
240
+ // `grok s sync` subcommands. Mirrors the API surface in
241
+ // core/server/datlas/lib/src/routers/sync.dart — pairs / setups /
242
+ // runs are read-only here; `run` triggers an actual push and prints
243
+ // the per-item summary. Full reference in
244
+ // core/docs/plans/instance-sync.md (Phase 7 / operational polish).
245
+ //
246
+ // `_callSync` does dual-path routing: tries `/api/sync/...` first
247
+ // (nginx-fronted deployments) and falls back to `/sync/...` (bare
248
+ // datlas on :8082). Same approach as the server-side handshake
249
+ // code so the CLI works against either layout.
250
+ const verb = rest[0];
251
+ const callSync = async (method, path, body) => {
252
+ for (const prefix of ['/api', '']) {
253
+ const r = body !== undefined ? await dapi.raw(method, `${prefix}${path}`, body) : await dapi.raw(method, `${prefix}${path}`);
254
+ // raw() returns `null` for 404, but the server returns HTML for
255
+ // 404 too — treat anything that isn't a sync-shaped object/array
256
+ // as a miss and fall through to the alternate prefix.
257
+ if (r && (Array.isArray(r) || typeof r === 'object') && r['#type'] !== 'ApiError') return r;
258
+ }
259
+ return null;
260
+ };
261
+ if (subject === 'pairs' && verb === 'list') {
262
+ const status = argv.status ? `?status=${encodeURIComponent(argv.status)}` : '';
263
+ const pairs = await callSync('GET', `/sync/pairs${status}`);
264
+ (0, _serverOutput.printOutput)(pairs, output);
265
+ return true;
266
+ }
267
+ if (subject === 'setups' && verb === 'list') {
268
+ const pairId = argv.pair ?? rest[1];
269
+ if (!pairId) {
270
+ (0, _serverOutput.printError)(new Error('Usage: grok s sync setups list --pair <pair-id>'));
271
+ return false;
272
+ }
273
+ const setups = await callSync('GET', `/sync/pairs/${encodeURIComponent(pairId)}/setups`);
274
+ (0, _serverOutput.printOutput)(setups, output);
275
+ return true;
276
+ }
277
+ if (subject === 'setup' && verb === 'get') {
278
+ const id = rest[1];
279
+ if (!id) {
280
+ (0, _serverOutput.printError)(new Error('Usage: grok s sync setup get <setup-id>'));
281
+ return false;
282
+ }
283
+ const setup = await callSync('GET', `/sync/setups/${encodeURIComponent(id)}`);
284
+ (0, _serverOutput.printOutput)(setup, output);
285
+ return true;
286
+ }
287
+ if (subject === 'run') {
288
+ // grok s sync run <setup-id>
289
+ const id = rest[1] ?? verb;
290
+ if (!id) {
291
+ (0, _serverOutput.printError)(new Error('Usage: grok s sync run <setup-id>'));
292
+ return false;
293
+ }
294
+ const result = await callSync('POST', `/sync/setups/${encodeURIComponent(id)}/run`, {});
295
+ if (output === 'json' || output === 'csv') {
296
+ (0, _serverOutput.printOutput)(result, output);
297
+ return true;
298
+ }
299
+ if (output === 'quiet') {
300
+ console.log(result?.runId ?? '');
301
+ return true;
302
+ }
303
+ if (!result) {
304
+ (0, _serverOutput.printError)(new Error(`Sync setup ${id} not found, or server has no /sync routes.`));
305
+ return false;
306
+ }
307
+ console.log(`Run ${result?.runId} → ${result?.remoteUrl} (${result?.status})`);
308
+ if (result?.counts) {
309
+ const cs = Object.keys(result.counts).filter(k => (result.counts[k] ?? 0) > 0).map(k => `${result.counts[k]} ${k}`).join(', ');
310
+ if (cs) console.log(` ${cs}`);
311
+ }
312
+ const items = Array.isArray(result?.items) ? result.items : [];
313
+ if (items.length) {
314
+ console.log('\nItems:');
315
+ (0, _serverOutput.printOutput)(items, 'table');
316
+ }
317
+ return true;
318
+ }
319
+ (0, _serverOutput.printError)(new Error('Usage:\n' + ' grok s sync pairs list [--status active|pending|revoked]\n' + ' grok s sync setups list --pair <pair-id>\n' + ' grok s sync setup get <setup-id>\n' + ' grok s sync run <setup-id>'));
320
+ return false;
321
+ }
238
322
  async function handleHealthcheck(dapi, argv, output) {
239
323
  const module = argv.module;
240
324
  const path = module ? `/api/public/v1/healthcheck?module=${encodeURIComponent(module)}` : '/api/public/v1/healthcheck';
@@ -618,6 +702,10 @@ Special commands:
618
702
  grok s batch <entity> <verb> arg1 [arg2 ...] Batch operation (one round-trip)
619
703
  grok s batch <entity> <verb> --json params.json Batch from JSON array
620
704
  grok s batch manifest.json Run a workflow manifest
705
+ grok s sync pairs list [--status <s>] List cross-instance sync pairs (status: active|pending|revoked)
706
+ grok s sync setups list --pair <pair-id> List the named sync setups under a pair
707
+ grok s sync setup get <setup-id> Inspect a setup (selections, direction, last run)
708
+ grok s sync run <setup-id> Trigger a push run; prints per-item outcome
621
709
 
622
710
  Options:
623
711
  --host <alias|url> Server alias from config or full URL
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.5.2",
3
+ "version": "6.5.4",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {
@@ -9,7 +9,7 @@
9
9
  "@babel/traverse": "^7.29.7",
10
10
  "@typescript-eslint/typescript-estree": "^8.61.1",
11
11
  "@typescript-eslint/visitor-keys": "^8.61.1",
12
- "adm-zip": "^0.5.17",
12
+ "adm-zip": "^0.6.0",
13
13
  "archiver": "^7.0.1",
14
14
  "datagrok-api": "^1.27.6",
15
15
  "estraverse": "^5.3.0",
@@ -84,17 +84,23 @@ function preparseIvpModel(parser, text) {
84
84
  const viewer = `Grid(block: 100) | Line chart(block: 100, multiAxis: "${multiAxis}",${segments} ` +
85
85
  `multiAxisLegendPosition: "RightCenter", autoLayout: "false", showAggrSelectors: "false")` +
86
86
  ` | DiffStudio Facet(block: 100${facetSegment})`;
87
- const outputAnnotation = `//output: dataframe df {caption: ${ivp.name}; viewer: ${viewer}}`;
88
-
89
87
  const metas = {runOnOpen: 'true', runOnInput: 'true', features: '{"sens-analysis": true, "fitting": true}'};
90
88
  let isModel = false;
89
+ let comparison = null;
91
90
  for (const meta of ivp.metas || []) {
92
91
  const m = /^(?:\/\/)?meta\.([\w-]+):\s*(.*)$/.exec(String(meta).trim());
93
92
  if (!m) continue;
94
93
  if (m[1] === 'role') {
95
94
  if (m[2].split(',').map((s) => s.trim()).includes('model')) isModel = true;
95
+ } else if (m[1] === 'comparison') {
96
+ // run comparison reads the {comparison: ...} JSON off the dataframe output's
97
+ // options, not off function-level meta
98
+ comparison = m[2];
96
99
  } else if (m[1] !== 'solver') metas[m[1]] = m[2];
97
100
  }
101
+
102
+ const outputAnnotation = `//output: dataframe df {caption: ${ivp.name}; ` +
103
+ `${comparison ? `comparison: ${comparison}; ` : ''}viewer: ${viewer}}`;
98
104
  // Convert a `#meta.inputs` lookup into a real `propagateChoice: all` string input. RFV renders
99
105
  // this natively (it ignores `meta.inputs`); selecting a row fills the matching inputs by name.
100
106
  // Name / brace parsing mirrors DiffStudio's getLookupsInfo (utils.ts): the name is the text