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.
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.HELP_SERVER = void 0;
6
7
  exports.buildInlineManifest = buildInlineManifest;
7
8
  exports.parseFuncCall = parseFuncCall;
8
9
  exports.resolveManifestSources = resolveManifestSources;
@@ -12,17 +13,29 @@ var path = _interopRequireWildcard(require("path"));
12
13
  var _nodeDapi = require("../utils/node-dapi");
13
14
  var _serverClient = require("../utils/server-client");
14
15
  var _serverOutput = require("../utils/server-output");
16
+ var _serverMigrate = require("./server-migrate");
17
+ var _serverDomains = require("./server-domains");
18
+ var _registry = require("../utils/migrate/registry");
19
+ var _walker = require("../utils/migrate/walker");
15
20
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
16
21
  /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
17
22
 
23
+ /** `queries|scripts get <nqName>` resolves through `/entities`, which needs the entity type. */
24
+ const ENTITY_TYPES = {
25
+ queries: 'DataQuery',
26
+ scripts: 'Script',
27
+ reports: 'UserReport'
28
+ };
18
29
  const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files', 'tables'];
19
- const VERBS = ['list', 'get', 'delete'];
30
+ const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle'];
31
+ const VERBS = ['list', 'count', 'get', 'delete'];
20
32
  async function server(argv) {
21
33
  const args = argv['_'].slice(1);
22
34
  const entity = args[0];
23
35
  const verb = args[1];
24
36
  const rest = args.slice(2);
25
37
  const output = argv.output ?? argv.o ?? 'table';
38
+ (0, _serverOutput.setOutputFormat)(output);
26
39
  const limit = Number(argv.limit ?? argv.l ?? 50);
27
40
  const offset = Number(argv.offset ?? 0);
28
41
  const filter = argv.filter ?? argv.f ?? '';
@@ -34,24 +47,40 @@ async function server(argv) {
34
47
  }
35
48
  let client;
36
49
  try {
37
- client = await (0, _serverClient.createClient)(host);
50
+ client = await (0, _serverClient.createClient)(host, !!argv.admin);
38
51
  } catch (err) {
52
+ // a bad alias, URL or key is not a usage error: no help dump, just the reason
39
53
  (0, _serverOutput.printError)(err);
40
- return false;
54
+ process.exitCode = 1;
55
+ return true;
41
56
  }
42
57
  const dapi = new _nodeDapi.NodeDapi(client);
43
58
  try {
44
- if (entity === 'batch') return handleBatch(dapi, argv, verb, rest, output);
45
- if (entity === 'raw') return handleRaw(dapi, verb, rest, output);
46
- if (entity === 'describe') return handleDescribe(dapi, verb ?? rest[0], output);
47
- if (entity === 'healthcheck') return handleHealthcheck(dapi, argv, output);
48
- if (entity === 'sync') return handleSync(dapi, verb, rest, argv, output);
49
- if (entity === 'functions' && verb === 'run') return handleFuncRun(dapi, rest, argv, output);
50
- if (entity === 'functions' && verb === 'list') return handleFunctionsList(dapi, argv, limit, offset, filter, output);
59
+ // await each handler so a rejection lands in the catch below instead of
60
+ // escaping to grok.js as an unhandled rejection (stack trace + exit 255)
61
+ if (['pull', 'push', 'migrate', 'diff', 'bundle'].includes(entity)) return await (0, _serverMigrate.handleMigrate)(dapi, entity, [verb, ...rest].filter(Boolean), argv, output);
62
+ if (entity === 'domains') return await (0, _serverDomains.handleDomains)(dapi, verb, rest, argv, output);
63
+ if (entity === 'batch') return await handleBatch(dapi, argv, verb, rest, output);
64
+ if (entity === 'raw') return await handleRaw(dapi, verb, rest, argv, output);
65
+ if (entity === 'describe') return await handleDescribe(dapi, verb ?? rest[0], output);
66
+ if (entity === 'healthcheck') return await handleHealthcheck(dapi, argv, output);
67
+ if (entity === 'sync') return await handleSync(dapi, verb, rest, argv, output);
68
+ if (entity === 'functions' && verb === 'run') return await handleFuncRun(dapi, rest, argv, output);
69
+ if (entity === 'functions' && verb === 'list') return await handleFunctionsList(dapi, argv, limit, offset, filter, output);
51
70
  if (entity === 'files' && verb === 'list') {
52
- const path = rest[0] ?? '';
53
- const result = await dapi.files.list(path, recursive);
54
- (0, _serverOutput.printOutput)(result, output);
71
+ if (!rest[0]) {
72
+ (0, _serverOutput.printError)(new Error('Usage: grok s files list <connector>[/<path>] [-r]\n e.g. grok s files list "System:AppData" -r'));
73
+ return false;
74
+ }
75
+ const files = await dapi.files.list(String(rest[0]), recursive);
76
+ if (output === 'json') (0, _serverOutput.printOutput)(files, output);else if (output === 'quiet') {
77
+ for (const f of files) console.log(f.path);
78
+ } else (0, _serverOutput.printOutput)(files.map(f => ({
79
+ path: f.path,
80
+ kind: f.isFile ? 'file' : 'dir',
81
+ size: f.isFile ? f.size ?? '' : '',
82
+ updatedOn: f.updatedOn ?? ''
83
+ })), output);
55
84
  return true;
56
85
  }
57
86
  if (entity === 'files' && verb === 'get') {
@@ -64,24 +93,30 @@ async function server(argv) {
64
93
  if (output !== 'quiet') console.log(`Deleted ${rest[0]}`);
65
94
  return true;
66
95
  }
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);
96
+ if (entity === 'files' && verb === 'put') return await handleFilesPut(dapi, rest, output);
97
+ if (entity === 'shares' && verb === 'add') return await handleSharesAdd(dapi, rest, argv, output);
98
+ if (entity === 'shares' && verb === 'list') return await handleSharesList(dapi, rest, output);
99
+ if (entity === 'users' && verb === 'save') return await handleUserSave(dapi, argv, output);
100
+ if (entity === 'groups' && verb === 'save') return await handleGroupSave(dapi, argv, output);
101
+ if (entity === 'connections' && verb === 'save') return await handleConnSave(dapi, argv, output);
102
+ if (entity === 'connections' && verb === 'test') return await handleConnTest(dapi, rest, argv, output);
103
+ if (entity === 'groups' && verb === 'add-members') return await handleGroupAddMembers(dapi, rest, argv, output);
104
+ if (entity === 'groups' && verb === 'remove-members') return await handleGroupRemoveMembers(dapi, rest, argv, output);
105
+ if (entity === 'groups' && (verb === 'list-members' || verb === 'list-memberships')) return await handleGroupListMembers(dapi, verb, rest, argv, output);
106
+ if (entity === 'users' && verb === 'block') return await handleUserBlock(dapi, rest, output);
107
+ if (entity === 'users' && verb === 'unblock') return await handleUserUnblock(dapi, rest, output);
108
+ if (entity === 'tables' && verb === 'download') return await handleTablesDownload(dapi, rest, argv, output);
109
+ if (entity === 'tables' && verb === 'upload') return await handleTablesUpload(dapi, rest, output);
110
+ if (entity === 'packages' && verb === 'install') return await handlePackagesInstall(dapi, rest, argv, output);
111
+ if (entity === 'packages' && verb === 'uninstall') return await handlePackagesUninstall(dapi, rest, output);
112
+ if (entity === 'packages' && verb === 'share') return await handlePackagesShare(dapi, rest, argv, output);
113
+ if (entity === 'packages' && verb === 'versions') return await handlePackagesVersions(dapi, rest, output);
114
+ if (entity === 'packages' && verb === 'set-version') return await handlePackagesSetVersion(dapi, rest, output);
115
+ if (entity === 'packages' && verb === 'outdated') return await handlePackagesOutdated(dapi, output);
116
+ if (entity === 'packages' && verb === 'update') return await handlePackagesUpdate(dapi, rest, argv, output);
82
117
  const source = dapi[entity];
83
118
  if (!source || !ENTITIES.includes(entity)) {
84
- (0, _serverOutput.printError)(new Error(`Unknown entity type: '${entity}'. Valid: ${ENTITIES.join(', ')}`));
119
+ (0, _serverOutput.printError)(new Error(`Unknown command '${entity}'. Entities: ${ENTITIES.join(', ')}. Other commands: ${COMMANDS.join(', ')}. See grok s --help`));
85
120
  return false;
86
121
  }
87
122
  if (!verb) {
@@ -89,17 +124,30 @@ async function server(argv) {
89
124
  return true;
90
125
  }
91
126
  if (verb === 'list') {
92
- const page = Math.floor(offset / limit);
93
- const results = await source.filter(filter).by(limit).page(page).list();
127
+ const results = source instanceof _nodeDapi.InternalDataSource ? await source.list({
128
+ text: filter || undefined,
129
+ limit,
130
+ page: Math.floor(offset / limit) + 1
131
+ }) : await source.filter(filter).by(limit).page(Math.floor(offset / limit)).list();
94
132
  (0, _serverOutput.printOutput)(results, output);
95
133
  return true;
96
134
  }
135
+ if (verb === 'count') {
136
+ if (entity === 'reports') throw new Error('The server has no count endpoint for reports; use `grok s reports list --output quiet | wc -l`');
137
+ const n = source instanceof _nodeDapi.InternalDataSource ? await source.count({
138
+ text: filter || undefined
139
+ }) : await source.filter(filter).count();
140
+ (0, _serverOutput.printOutput)(n, output);
141
+ return true;
142
+ }
97
143
  if (verb === 'get') {
98
144
  if (!rest[0]) {
99
145
  (0, _serverOutput.printError)(new Error('Usage: grok s <entity> get <id>'));
100
146
  return false;
101
147
  }
102
- const result = await source.find(rest[0]);
148
+ // The internal routers find by id only; a name has to be resolved through /entities first.
149
+ const id = source instanceof _nodeDapi.InternalDataSource && !(0, _registry.isUuid)(rest[0]) ? (await (0, _walker.resolveEntity)(dapi, rest[0], ENTITY_TYPES[entity])).id : rest[0];
150
+ const result = await source.find(id);
103
151
  (0, _serverOutput.printOutput)(result, output);
104
152
  return true;
105
153
  }
@@ -112,11 +160,17 @@ async function server(argv) {
112
160
  if (output !== 'quiet') console.log(`Deleted ${entity}/${rest[0]}`);
113
161
  return true;
114
162
  }
115
- (0, _serverOutput.printError)(new Error(`Unknown verb: '${verb}'. Valid: ${VERBS.join(', ')}`));
163
+ const extraVerbs = entity === 'packages' ? ', install, uninstall, update, outdated, versions, set-version, share' : '';
164
+ (0, _serverOutput.printError)(new Error(`Unknown verb: '${verb}'. Valid: ${VERBS.join(', ')}${extraVerbs}`));
116
165
  return false;
117
166
  } catch (err) {
118
- (0, _serverOutput.printError)(err);
119
- return false;
167
+ // A runtime failure is not a usage error: report it and exit non-zero without
168
+ // making grok.js dump the help block (which it does for every `false` result).
169
+ (0, _serverOutput.printError)(err, {
170
+ verbose: !!argv.verbose
171
+ });
172
+ process.exitCode = 1;
173
+ return true;
120
174
  }
121
175
  }
122
176
  async function handleFilesPut(dapi, rest, output) {
@@ -168,6 +222,208 @@ async function handleTablesDownload(dapi, rest, argv, output) {
168
222
  } else process.stdout.write(csv);
169
223
  return true;
170
224
  }
225
+ async function installPackages(dapi, targets, output) {
226
+ const results = [];
227
+ for (const t of targets) {
228
+ let name = t.name;
229
+ try {
230
+ const pkg = await dapi.packages.resolve(t.name);
231
+ if (pkg) name = pkg.name;
232
+ if (output === 'table') console.log(`Installing ${name} (${t.version})...`);
233
+ const id = await dapi.packages.install(name, t.version);
234
+ // the server returns the published id for any existing explicit version
235
+ // (even an already-current one), so null means the version doesn't exist
236
+ if (!id && t.version !== 'latest') results.push({
237
+ package: name,
238
+ version: t.version,
239
+ status: 'error',
240
+ error: `Version '${t.version}' not found`
241
+ });else results.push({
242
+ package: name,
243
+ version: t.version,
244
+ status: id ? 'installed' : 'noop',
245
+ id: id ?? undefined
246
+ });
247
+ } catch (err) {
248
+ results.push({
249
+ package: name,
250
+ version: t.version,
251
+ status: 'error',
252
+ error: (err?.message ?? String(err)).replace(/\s*\n\s*/g, '; ')
253
+ });
254
+ }
255
+ }
256
+ return results;
257
+ }
258
+ function printPackageOps(results, output) {
259
+ if (output === 'quiet') {
260
+ for (const r of results) if (r.id) console.log(r.id);
261
+ } else (0, _serverOutput.printOutput)(results, output);
262
+ if (results.some(r => r.status === 'error')) process.exitCode = 1;
263
+ }
264
+ async function handlePackagesInstall(dapi, rest, argv, output) {
265
+ if (!rest.length) {
266
+ (0, _serverOutput.printError)(new Error('Usage: grok s packages install <name> [<name> ...] [--version <v>]'));
267
+ return false;
268
+ }
269
+ if (argv.version !== undefined && rest.length > 1) {
270
+ (0, _serverOutput.printError)(new Error('--version applies to a single package'));
271
+ return false;
272
+ }
273
+ const version = argv.version !== undefined ? String(argv.version) : 'latest';
274
+ const results = await installPackages(dapi, rest.map(r => ({
275
+ name: String(r),
276
+ version
277
+ })), output);
278
+ printPackageOps(results, output);
279
+ return true;
280
+ }
281
+ async function handlePackagesUninstall(dapi, rest, output) {
282
+ if (!rest[0]) {
283
+ (0, _serverOutput.printError)(new Error('Usage: grok s packages uninstall <name-or-id>'));
284
+ return false;
285
+ }
286
+ const result = await dapi.packages.uninstall(rest[0]);
287
+ 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}`);
288
+ return true;
289
+ }
290
+ async function handlePackagesShare(dapi, rest, argv, output) {
291
+ const [name, ...groupArgs] = rest;
292
+ if (!name || !groupArgs.length) {
293
+ (0, _serverOutput.printError)(new Error('Usage: grok s packages share <name-or-id> <group>[,<group>...] [--access View|Edit]'));
294
+ return false;
295
+ }
296
+ const groups = groupArgs.flatMap(g => g.split(',')).map(g => g.trim()).filter(Boolean).join(',');
297
+ const access = typeof argv.access === 'string' ? argv.access : 'View';
298
+ if (access !== 'View' && access !== 'Edit') {
299
+ (0, _serverOutput.printError)(new Error(`Invalid --access '${access}'. Use 'View' or 'Edit'.`));
300
+ return false;
301
+ }
302
+ const pkg = await dapi.packages.resolve(name);
303
+ if (!pkg) {
304
+ (0, _serverOutput.printError)(new Error(`Package '${name}' not found`));
305
+ return false;
306
+ }
307
+ const result = await dapi.shares.share(pkg.id, groups, access);
308
+ (0, _serverOutput.printOutput)(result, output);
309
+ if (result?.status === 'failed') process.exitCode = 1;
310
+ return true;
311
+ }
312
+ async function handlePackagesVersions(dapi, rest, output) {
313
+ if (!rest[0]) {
314
+ (0, _serverOutput.printError)(new Error('Usage: grok s packages versions <name-or-id>'));
315
+ return false;
316
+ }
317
+ const versions = await dapi.packages.versions(rest[0]);
318
+ const flat = versions.map(v => ({
319
+ version: v?.version ?? '',
320
+ current: v?.isCurrent ?? false,
321
+ latest: v?.isLatest ?? false,
322
+ local: v?.isLocal ?? false,
323
+ remote: v?.isRemote ?? false,
324
+ debug: v?.debug ?? false,
325
+ id: v?.id ?? ''
326
+ }));
327
+ (0, _serverOutput.printOutput)(flat, output);
328
+ return true;
329
+ }
330
+ async function handlePackagesSetVersion(dapi, rest, output) {
331
+ if (!rest[0] || rest[1] === undefined) {
332
+ (0, _serverOutput.printError)(new Error('Usage: grok s packages set-version <name> <version>'));
333
+ return false;
334
+ }
335
+ const version = String(rest[1]);
336
+ const pkg = await dapi.packages.resolve(String(rest[0]));
337
+ const name = pkg?.name ?? String(rest[0]);
338
+ const id = await dapi.packages.install(name, version);
339
+ if (!id) {
340
+ (0, _serverOutput.printError)(new Error(`Version '${version}' not found for package '${name}'`));
341
+ return false;
342
+ }
343
+ if (output === 'quiet') console.log(id);else if (output === 'json' || output === 'csv') (0, _serverOutput.printOutput)({
344
+ package: name,
345
+ version,
346
+ id
347
+ }, output);else console.log(`${name} -> ${version} (published package ${id})`);
348
+ return true;
349
+ }
350
+ async function handlePackagesOutdated(dapi, output) {
351
+ const rows = await dapi.packages.outdated();
352
+ if (!rows.length && output === 'table') {
353
+ console.log('All packages up to date.');
354
+ return true;
355
+ }
356
+ (0, _serverOutput.printOutput)(rows, output);
357
+ return true;
358
+ }
359
+ async function handlePackagesUpdate(dapi, rest, argv, output) {
360
+ const all = argv.all === true;
361
+ if (!all && !rest.length) {
362
+ (0, _serverOutput.printError)(new Error('Usage: grok s packages update <name> [<name> ...] | --all'));
363
+ return false;
364
+ }
365
+ const outdated = await dapi.packages.outdated();
366
+ const results = [];
367
+ // a package tracking 'latest' keeps that auto-update intent; a pinned one gets
368
+ // pinned to the concrete registry-latest version
369
+ const targetVersion = o => o.desiredVersion === 'latest' ? 'latest' : o.latest;
370
+ let targets;
371
+ if (all) {
372
+ targets = outdated.map(o => ({
373
+ name: o.name,
374
+ version: targetVersion(o)
375
+ }));
376
+ if (!targets.length && output === 'table') {
377
+ console.log('All packages up to date.');
378
+ return true;
379
+ }
380
+ } else {
381
+ targets = [];
382
+ for (const r of rest) {
383
+ const raw = String(r);
384
+ let pkg = null;
385
+ try {
386
+ pkg = await dapi.packages.resolve(raw);
387
+ } catch (err) {
388
+ results.push({
389
+ package: raw,
390
+ version: '',
391
+ status: 'error',
392
+ error: (err?.message ?? String(err)).replace(/\s*\n\s*/g, '; ')
393
+ });
394
+ continue;
395
+ }
396
+ if (!pkg) {
397
+ results.push({
398
+ package: raw,
399
+ version: '',
400
+ status: 'error',
401
+ error: 'Package not found'
402
+ });
403
+ continue;
404
+ }
405
+ const o = outdated.find(x => x.name === pkg.name);
406
+ if (o) {
407
+ targets.push({
408
+ name: pkg.name,
409
+ version: targetVersion(o)
410
+ });
411
+ continue;
412
+ }
413
+ const versions = Array.isArray(pkg.publishedVersions) ? pkg.publishedVersions : [];
414
+ const installed = versions.some(v => v?.isCurrent && !v?.debug);
415
+ results.push({
416
+ package: pkg.name,
417
+ version: '',
418
+ status: 'noop',
419
+ note: installed ? 'already up to date' : 'not installed'
420
+ });
421
+ }
422
+ }
423
+ results.push(...(await installPackages(dapi, targets, output)));
424
+ printPackageOps(results, output);
425
+ return true;
426
+ }
171
427
  async function handleTablesUpload(dapi, rest, output) {
172
428
  const [name, localPath] = rest;
173
429
  if (!name || !localPath) {
@@ -226,13 +482,20 @@ async function handleFunctionsList(dapi, argv, limit, offset, userFilter, output
226
482
  (0, _serverOutput.printOutput)(results, output);
227
483
  return true;
228
484
  }
229
- async function handleRaw(dapi, method, rest, output) {
485
+ async function handleRaw(dapi, method, rest, argv, output) {
230
486
  if (!method || !rest[0]) {
231
- (0, _serverOutput.printError)(new Error('Usage: grok s raw <METHOD> <path>'));
487
+ (0, _serverOutput.printError)(new Error('Usage: grok s raw <METHOD> <path> [--json body.json | --data \'{"k": 1}\']\n e.g. grok s raw GET /users/current (paths are API-relative; a leading /api is accepted)'));
232
488
  return false;
233
489
  }
234
- const path = rest[0];
235
- const result = await dapi.raw(method, path);
490
+ let body;
491
+ if (argv.json) body = readJsonFile(argv.json);else if (argv.data !== undefined) {
492
+ try {
493
+ body = JSON.parse(String(argv.data));
494
+ } catch {
495
+ body = String(argv.data);
496
+ }
497
+ }
498
+ const result = await dapi.raw(method, String(rest[0]), body);
236
499
  (0, _serverOutput.printOutput)(result, output);
237
500
  return true;
238
501
  }
@@ -242,21 +505,15 @@ async function handleSync(dapi, subject, rest, argv, output) {
242
505
  // runs are read-only here; `run` triggers an actual push and prints
243
506
  // the per-item summary. Full reference in
244
507
  // 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
508
  const verb = rest[0];
509
+ // a 1.27 server has no /sync routes at all: report that as "not found", not as a failure
251
510
  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;
511
+ try {
512
+ return await dapi.raw(method, path, body);
513
+ } catch (err) {
514
+ if (err?.apiError?.errorCode === 404) return null;
515
+ throw err;
258
516
  }
259
- return null;
260
517
  };
261
518
  if (subject === 'pairs' && verb === 'list') {
262
519
  const status = argv.status ? `?status=${encodeURIComponent(argv.status)}` : '';
@@ -321,8 +578,9 @@ async function handleSync(dapi, subject, rest, argv, output) {
321
578
  }
322
579
  async function handleHealthcheck(dapi, argv, output) {
323
580
  const module = argv.module;
324
- const path = module ? `/api/public/v1/healthcheck?module=${encodeURIComponent(module)}` : '/api/public/v1/healthcheck';
325
- const result = await dapi.raw('GET', path);
581
+ const path = module ? `/public/v1/healthcheck?module=${encodeURIComponent(module)}` : '/public/v1/healthcheck';
582
+ const result = await dapi.client.get(path);
583
+ const services = Array.isArray(result?.services) ? result.services : [];
326
584
  if (output === 'json') {
327
585
  (0, _serverOutput.printOutput)(result, output);
328
586
  return true;
@@ -334,16 +592,25 @@ async function handleHealthcheck(dapi, argv, output) {
334
592
  console.log(`time: ${result?.time ?? ''}`);
335
593
  console.log('');
336
594
  }
337
- (0, _serverOutput.printOutput)(result?.services ?? [], output);
595
+ if (services.length) (0, _serverOutput.printOutput)(services, output);else if (module) throw new Error(`Module '${module}' is not reported by the server`);else if (output !== 'quiet') console.log('(no services reported)');
338
596
  return true;
339
597
  }
340
598
  async function handleDescribe(dapi, entityType, output) {
341
599
  if (!entityType) {
342
- (0, _serverOutput.printError)(new Error('Usage: grok s describe <entity-type>'));
600
+ (0, _serverOutput.printError)(new Error('Usage: grok s describe <entity|type> e.g. grok s describe connections, grok s describe Project'));
343
601
  return false;
344
602
  }
345
- const result = await dapi.describe(entityType);
346
- (0, _serverOutput.printOutput)(result, output);
603
+ const result = await dapi.describe(String(entityType));
604
+ if (output === 'json') {
605
+ (0, _serverOutput.printOutput)(result, output);
606
+ return true;
607
+ }
608
+ if (output !== 'quiet' && result.type) console.log(`${result.type.name}${result.type.friendlyName && result.type.friendlyName !== result.type.name ? ` (${result.type.friendlyName})` : ''} id ${result.type.id}${result.type.isPackageEntity ? ' package entity' : ''}\n`);
609
+ if (!result.fields.length) {
610
+ if (output !== 'quiet') console.log('(no entity of this type exists on the server to derive the fields from)');
611
+ return true;
612
+ }
613
+ (0, _serverOutput.printOutput)(output === 'quiet' ? result.fields.map(f => f.field) : result.fields, output);
347
614
  return true;
348
615
  }
349
616
  async function handleFuncRun(dapi, rest, argv, output) {
@@ -365,6 +632,7 @@ async function handleFuncRun(dapi, rest, argv, output) {
365
632
  const parsed = parseFuncCall(funcName);
366
633
  funcName = parsed.name;
367
634
  params = parsed.params;
635
+ if (Object.keys(params).some(k => /^\d+$/.test(k))) params = (0, _nodeDapi.mapPositionalParams)(params, (await dapi.functions.find(funcName))?.parameterInfos, funcName);
368
636
  }
369
637
  const result = await dapi.functions.run(funcName, params);
370
638
  (0, _serverOutput.printOutput)(result, output);
@@ -399,18 +667,25 @@ async function handleSharesAdd(dapi, rest, argv, output) {
399
667
  return true;
400
668
  }
401
669
  async function handleSharesList(dapi, rest, output) {
402
- const entityId = rest[0];
403
- if (!entityId) {
404
- (0, _serverOutput.printError)(new Error('Usage: grok s shares list <entity-id> (entity id must be a UUID)'));
670
+ if (!rest[0]) {
671
+ (0, _serverOutput.printError)(new Error('Usage: grok s shares list <entity-id-or-name> e.g. grok s shares list "Admin:MyConnection"'));
405
672
  return false;
406
673
  }
674
+ const entityId = (0, _registry.isUuid)(String(rest[0])) ? String(rest[0]) : (await (0, _walker.resolveEntity)(dapi, String(rest[0]))).id;
407
675
  const perms = await dapi.shares.list(entityId);
408
- const flat = (Array.isArray(perms) ? perms : []).map(p => ({
409
- group: p?.userGroup?.friendlyName ?? p?.userGroup?.name ?? p?.userGroup?.id ?? '',
410
- groupId: p?.userGroup?.id ?? '',
411
- access: p?.permission?.name ?? p?.permission?.friendlyName ?? '',
412
- personal: p?.userGroup?.personal ?? false
413
- }));
676
+ // the permissions route returns group ids only; a share touches a handful of groups
677
+ const groups = new Map();
678
+ for (const id of new Set(perms.map(p => p?.userGroup?.id).filter(Boolean))) groups.set(id, await dapi.groups.find(id));
679
+ const flat = perms.map(p => {
680
+ const g = groups.get(p?.userGroup?.id) ?? p?.userGroup ?? {};
681
+ return {
682
+ group: g.friendlyName ?? g.name ?? g.id ?? '',
683
+ groupId: g.id ?? '',
684
+ access: p?.permission?.name ?? p?.permission?.friendlyName ?? '',
685
+ personal: g.personal ?? false,
686
+ inherited: p?.inheritedByLink === true
687
+ };
688
+ });
414
689
  (0, _serverOutput.printOutput)(flat, output);
415
690
  return true;
416
691
  }
@@ -502,23 +777,14 @@ async function handleGroupRemoveMembers(dapi, rest, argv, output) {
502
777
  if (anyError) process.exitCode = 1;
503
778
  return true;
504
779
  }
505
- async function handleGroupListMembers(dapi, rest, argv, output) {
506
- if (!rest[0]) {
507
- (0, _serverOutput.printError)(new Error('Usage: grok s groups list-members <group> [--admin | --no-admin]'));
508
- return false;
509
- }
510
- const admin = typeof argv.admin === 'boolean' ? argv.admin : undefined;
511
- const result = await dapi.groups.getMembers(rest[0], admin);
512
- (0, _serverOutput.printOutput)(result, output);
513
- return true;
514
- }
515
- async function handleGroupListMemberships(dapi, rest, argv, output) {
780
+ async function handleGroupListMembers(dapi, verb, rest, argv, output) {
516
781
  if (!rest[0]) {
517
- (0, _serverOutput.printError)(new Error('Usage: grok s groups list-memberships <group> [--admin | --no-admin]'));
782
+ (0, _serverOutput.printError)(new Error(`Usage: grok s groups ${verb} <group-or-login> [--admin | --no-admin] [--user]`));
518
783
  return false;
519
784
  }
520
785
  const admin = typeof argv.admin === 'boolean' ? argv.admin : undefined;
521
- const result = await dapi.groups.getMemberships(rest[0], admin);
786
+ const personalOnly = argv.user === true;
787
+ const result = verb === 'list-members' ? await dapi.groups.getMembers(String(rest[0]), admin, personalOnly) : await dapi.groups.getMemberships(String(rest[0]), admin, personalOnly);
522
788
  (0, _serverOutput.printOutput)(result, output);
523
789
  return true;
524
790
  }
@@ -655,7 +921,7 @@ function parseFuncCall(expr) {
655
921
  params: Object.fromEntries(positional.map((v, i) => [String(i), v]))
656
922
  };
657
923
  }
658
- const HELP_SERVER = `
924
+ const HELP_SERVER = exports.HELP_SERVER = `
659
925
  Usage: grok server <entity> <verb> [args] [options]
660
926
  grok s <entity> <verb> [args] [options]
661
927
 
@@ -663,27 +929,29 @@ Manage a Datagrok server from the command line.
663
929
 
664
930
  Entities:
665
931
  users, groups, functions, connections, queries, scripts, packages, reports, files, tables
932
+ (plus domains, shares, batch, raw, describe, healthcheck, sync, pull/push/migrate/diff/bundle below)
666
933
 
667
934
  Verbs:
668
- list List entities
935
+ list List entities (--filter, --limit, --offset)
936
+ count Count entities (--filter)
669
937
  get Get a single entity by ID or name
670
- delete Delete an entity by ID
938
+ delete Delete an entity by ID or name (users cannot be deleted: block them; functions: scripts and queries only)
671
939
 
672
940
  Special commands:
673
- grok s functions run <Name:func(args)> Call a function
941
+ grok s functions run <Name:func(args)> Call a function (positional args map onto its inputs)
674
942
  grok s functions list [--type <t>] [--language <l>] [--package <p>] [--filter <expr>]
675
943
  Type: script|query|function|package
676
944
  Language applies to scripts (python, r, julia, nodejs, octave, grok)
677
- grok s files list <path> [-r] List files (recursive with -r)
945
+ grok s files list <connector>[/<path>] [-r] List a share (recursive with -r): path, kind, size
678
946
  grok s files get <path> Download a file (returns bytes)
679
947
  grok s files delete <path> Delete a file
680
948
  grok s files put <local> <remote> Upload a local file
681
- grok s raw <METHOD> <path> Hit any API endpoint
682
- grok s describe <entity-type> Show entity JSON schema
949
+ grok s raw <METHOD> <path> [--json f | --data j] Any API endpoint; path is API-relative (/users/current), /api prefix optional
950
+ grok s describe <entity|type> Fields of an entity type (registry record + a live sample)
683
951
  grok s healthcheck [--module <name>] Check server + per-module health
684
952
  grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
685
953
  Share an entity with one or more groups
686
- grok s shares list <entity-id> List who an entity (UUID) is shared with
954
+ grok s shares list <entity-id-or-name> List who an entity is shared with
687
955
  grok s users save --json user.json Create or update a user from a JSON file
688
956
  grok s groups save --json group.json [--save-relations]
689
957
  Create or update a group from a JSON file
@@ -693,12 +961,47 @@ Special commands:
693
961
  grok s connections test --json conn.json Test connectivity of a connection defined in JSON
694
962
  grok s groups add-members <group> <m>... [--admin] Add one or more users/groups as members
695
963
  grok s groups remove-members <group> <m>... Remove members (no-op if not a member)
696
- grok s groups list-members <group> [--admin] List members (optionally filter by admin)
697
- grok s groups list-memberships <group> [--admin] List parent groups
964
+ grok s groups list-members <group> [--admin] [--user]
965
+ List members (optionally filter by admin; --user: personal group)
966
+ grok s groups list-memberships <group> [--admin] [--user]
967
+ List parent groups
698
968
  grok s users block <id-or-login> Block a user from the platform
699
969
  grok s users unblock <id-or-login> Unblock a previously blocked user
700
970
  grok s tables upload <name> <file.csv|file.d42> Upload a CSV or d42 binary as a Datagrok table
701
- grok s tables download <name-or-id> [-O <file>] Download a table as CSV (stdout by default)
971
+ grok s tables download <name|Project:Table|id> [-O <file>]
972
+ Download a table as CSV (stdout by default)
973
+ grok s packages install <name>... [--version <v>] Install latest (or pinned) versions from the registry
974
+ grok s packages uninstall <name> Uninstall a package (repository entry is kept)
975
+ grok s packages update <name>... | --all Upgrade to the latest registry version
976
+ grok s packages outdated Show installed vs latest registry versions
977
+ grok s packages versions <name> List published versions (current/latest/debug flags)
978
+ grok s packages set-version <name> <version> Activate a specific published version
979
+ grok s packages share <name> <group>[,...] [--access View|Edit]
980
+ Share a package with one or more groups
981
+ grok s pull [<nqName|id>...] --out <dir> Export entities into a bundle directory
982
+ grok s push <bundle-dir> [--dry-run] Import a bundle into the target server
983
+ grok s migrate <selection> --from <a> --to <b> Pull from one instance and push into another
984
+ grok s diff <bundle-dir> What a push would change (read-only, plans with skip)
985
+ grok s bundle ls <bundle-dir> List what a bundle contains
986
+ grok s domains list [<schema>] [--filter <text>] Domain schemas, or the tables of one schema
987
+ grok s domains get <schema>|<schema.table> [<id>] Manifest, a table's columns, or one row
988
+ grok s domains query <schema.table> [--filter <expr>] [--columns a,b] [--sort 'a,!b'] [--expand x] [--limit n] [--offset n]
989
+ grok s domains count <schema.table> [--filter <expr>]
990
+ grok s domains insert <schema.table> --json rows.json | col=value ... [--error-on-duplicate]
991
+ grok s domains update <schema.table> <id> --json values.json | col=value ... [--version n]
992
+ grok s domains delete <schema.table> <id> | --filter <expr> [--limit n]
993
+ grok s domains delete <schema> --force Purge a user-managed schema with its data
994
+ grok s domains upload <schema.table> <file.csv|.d42|.json> [--upsert] [--no-all-or-nothing] [--error-on-duplicate]
995
+ grok s domains download <schema.table> [-O out.csv|out.d42] [--filter <expr>] [--columns a,b] [--sort s]
996
+ grok s domains aggregate <schema.table> --measures 'count,sum(x) as t' [--group-by a,b] [--filter <expr>]
997
+ grok s domains transaction <schema> --json ops.json Ordered insert/update/delete ops, atomically
998
+ grok s domains audit <schema>|<schema.table> [<id>] [--limit n]
999
+ grok s domains capabilities <schema.table> What the current user may do on the table
1000
+ grok s domains grants <schema>|<schema.table> Direct permission grants on a schema or table
1001
+ grok s domains grant <schema>|<schema.table> <group>[,...] [--access View|Edit|Delete|Share|Extend]
1002
+ grok s domains revoke <schema>|<schema.table> <group>[,...] [--access <permission>]
1003
+ grok s domains create <name> [--friendly-name <t>] [--description <t>]
1004
+ grok s domains apply <schema> --json manifest.json [--dry-run] [--confirm-destructive] [--if-version <v>]
702
1005
  grok s batch <entity> <verb> arg1 [arg2 ...] Batch operation (one round-trip)
703
1006
  grok s batch <entity> <verb> --json params.json Batch from JSON array
704
1007
  grok s batch manifest.json Run a workflow manifest
@@ -707,18 +1010,53 @@ Special commands:
707
1010
  grok s sync setup get <setup-id> Inspect a setup (selections, direction, last run)
708
1011
  grok s sync run <setup-id> Trigger a push run; prints per-item outcome
709
1012
 
1013
+ Pull / push / migrate options:
1014
+ --out <dir> Bundle directory to write (pull; merges into an existing bundle)
1015
+ --type <t,t> conn, query, script, project, dashboard, space, view, layout, table,
1016
+ file, group, job, notebook, model
1017
+ --name <glob> Entity name glob: 'Cereal*' matches by prefix, '*demo*' anywhere
1018
+ --namespace <ns> Everything under a namespace, recursively
1019
+ --space <nqName> The space itself plus everything under it
1020
+ --author <login> Entities authored by a login
1021
+ --tag <tag> Entities carrying a tag
1022
+ --since <2w|date> Entities updated since ('2w' means '-2w'; also --since=-2w)
1023
+ --filter <expr> Smart-filter expression ANDed with the other flags
1024
+ --no-deps Do not follow dependencies (never-travels rules still apply)
1025
+ --no-include-data Do not pull table data (.d42) for pulled tables
1026
+ --include-files Also pull the bytes of pulled files
1027
+ --replace Clear the bundle directory before writing
1028
+ --dry-run Push: print the plan and stop
1029
+ --on-conflict <p> Push: what to do when the name is taken on the target by another id —
1030
+ fail (default) aborts before any write, skip leaves the target alone
1031
+ (and fails whatever depends on it), adopt writes into the twin and
1032
+ records idmap.json, duplicate creates the bundle entity under its own
1033
+ id next to the twin (a UserGroup cannot be duplicated — group names
1034
+ are unique, so that row fails)
1035
+ --creds <file.yaml> Push: target-side connection secrets, 'Ns:Conn:' then ' password: \${VAR}'
1036
+ (a covered connection is always written, so a push rotates the secret)
1037
+ --from <alias|url> Migrate: source instance (pulled from, never written to)
1038
+ --to <alias|url> Migrate: target instance
1039
+ --keep Migrate: keep the temporary bundle and print its path on stderr
1040
+
710
1041
  Options:
711
1042
  --host <alias|url> Server alias from config or full URL
1043
+ --admin Ask the server for an admin session, so the run sees entities the key's
1044
+ own account cannot (other people's spaces). Refused unless the account
1045
+ may start one; lasts for this command only
712
1046
  --output <format> Output format: table (default), json, csv, quiet
713
1047
  --filter <text> Smart filter expression
714
1048
  --limit <n> Page size (default: 50)
715
1049
  --offset <n> Start offset (default: 0)
716
1050
  -r, --recursive Recursive (for files list)
717
- --json <file> Read function parameters or batch params from JSON file
1051
+ --verbose Print the stack of a runtime failure, not just its message
1052
+ --json <file> Read a JSON body from a file (save, functions run, batch, raw)
1053
+ --data '<json>' Inline JSON body for raw
718
1054
  -O, --output-file Write table download to a file instead of stdout
719
1055
  --type <t> Function discriminator: script | query | function | package
720
1056
  --language <lang> Script language: python, r, julia, nodejs, octave, grok
721
1057
  --package <name> Restrict to functions belonging to a package (by short name)
1058
+ --version <v> Package version for 'packages install' (default: latest)
1059
+ --all For 'packages update': upgrade every outdated package
722
1060
 
723
1061
  Batch manifest options (in manifest.json):
724
1062
  stopOnError Stop on first failure (default: true)
@@ -728,6 +1066,7 @@ Batch manifest options (in manifest.json):
728
1066
  Examples:
729
1067
  grok s users list
730
1068
  grok s users list --output json --limit 10
1069
+ grok s users count --filter 'status = "active"'
731
1070
  grok s users save --json user.json
732
1071
  grok s groups save --json group.json --save-relations
733
1072
  grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
@@ -745,14 +1084,38 @@ Examples:
745
1084
  grok s functions list --package PowerPack --type package --filter 'name contains "grid"'
746
1085
  grok s files list "System:AppData" -r
747
1086
  grok s files put ./smiles.csv "System:DemoFiles/smiles.csv"
748
- grok s raw GET /api/users/current
1087
+ grok s packages install Chem Bio PowerGrid
1088
+ grok s packages install Chem --version 1.14.0
1089
+ grok s packages update --all
1090
+ grok s packages versions Chem
1091
+ grok s packages share Chem Chemists --access View
1092
+ grok s raw GET /users/current
1093
+ grok s raw POST /public/v1/functions/Sin/call --data '{"x": 1}'
749
1094
  grok s describe connections
1095
+ grok s tables download MyTable -O ./my-table.csv
750
1096
  grok s users list --host dev
751
1097
  grok s users list --host "https://mygrok.com/api"
752
1098
  grok s groups add-members Admins alice bob --admin
753
1099
  grok s groups remove-members Admins alice
754
1100
  grok s groups list-members Admins --admin
755
1101
  grok s groups list-memberships alice
1102
+ grok s pull Admin:CerealDemog --out ./bundle --host dev
1103
+ grok s pull Chemists --out ./bundle --host dev
1104
+ grok s pull --type script --author alice --no-deps --out ./bundle
1105
+ grok s bundle ls ./bundle
1106
+ grok s push ./bundle --host local --dry-run
1107
+ grok s push ./bundle --host local --on-conflict adopt
1108
+ grok s pull --type space --namespace MySpace --include-files --out ./bundle
1109
+ grok s push ./bundle --host prod --creds ./creds.yaml
1110
+ grok s migrate Chem:TargetDashboard --from dev --to prod --dry-run
1111
+ grok s diff ./bundle --host local
1112
+ grok s domains list
1113
+ grok s domains get grit.issue
1114
+ grok s domains query grit.issue --filter 'status = "open"' --sort '!created_on' --limit 20
1115
+ grok s domains insert grit.issue title="Crash on save" status=open
1116
+ grok s domains upload grit.issue ./issues.csv --upsert
1117
+ grok s domains download grit.issue -O ./issues.csv
1118
+ grok s domains grant grit.issue Chemists --access Edit
756
1119
  grok s batch files delete "System:AppData/old.txt" "System:DemoFiles/tmp.txt"
757
1120
  grok s batch users create --json users.json
758
1121
  grok s batch manifest.json