datagrok-tools 6.5.7 → 6.5.9

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,26 +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
59
  // await each handler so a rejection lands in the catch below instead of
45
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);
46
63
  if (entity === 'batch') return await handleBatch(dapi, argv, verb, rest, output);
47
- if (entity === 'raw') return await handleRaw(dapi, verb, rest, output);
64
+ if (entity === 'raw') return await handleRaw(dapi, verb, rest, argv, output);
48
65
  if (entity === 'describe') return await handleDescribe(dapi, verb ?? rest[0], output);
49
66
  if (entity === 'healthcheck') return await handleHealthcheck(dapi, argv, output);
50
67
  if (entity === 'sync') return await handleSync(dapi, verb, rest, argv, output);
51
68
  if (entity === 'functions' && verb === 'run') return await handleFuncRun(dapi, rest, argv, output);
52
69
  if (entity === 'functions' && verb === 'list') return await handleFunctionsList(dapi, argv, limit, offset, filter, output);
53
70
  if (entity === 'files' && verb === 'list') {
54
- const path = rest[0] ?? '';
55
- const result = await dapi.files.list(path, recursive);
56
- (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);
57
84
  return true;
58
85
  }
59
86
  if (entity === 'files' && verb === 'get') {
@@ -75,8 +102,7 @@ async function server(argv) {
75
102
  if (entity === 'connections' && verb === 'test') return await handleConnTest(dapi, rest, argv, output);
76
103
  if (entity === 'groups' && verb === 'add-members') return await handleGroupAddMembers(dapi, rest, argv, output);
77
104
  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);
105
+ if (entity === 'groups' && (verb === 'list-members' || verb === 'list-memberships')) return await handleGroupListMembers(dapi, verb, rest, argv, output);
80
106
  if (entity === 'users' && verb === 'block') return await handleUserBlock(dapi, rest, output);
81
107
  if (entity === 'users' && verb === 'unblock') return await handleUserUnblock(dapi, rest, output);
82
108
  if (entity === 'tables' && verb === 'download') return await handleTablesDownload(dapi, rest, argv, output);
@@ -90,7 +116,7 @@ async function server(argv) {
90
116
  if (entity === 'packages' && verb === 'update') return await handlePackagesUpdate(dapi, rest, argv, output);
91
117
  const source = dapi[entity];
92
118
  if (!source || !ENTITIES.includes(entity)) {
93
- (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`));
94
120
  return false;
95
121
  }
96
122
  if (!verb) {
@@ -98,17 +124,30 @@ async function server(argv) {
98
124
  return true;
99
125
  }
100
126
  if (verb === 'list') {
101
- const page = Math.floor(offset / limit);
102
- 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();
103
132
  (0, _serverOutput.printOutput)(results, output);
104
133
  return true;
105
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
+ }
106
143
  if (verb === 'get') {
107
144
  if (!rest[0]) {
108
145
  (0, _serverOutput.printError)(new Error('Usage: grok s <entity> get <id>'));
109
146
  return false;
110
147
  }
111
- 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);
112
151
  (0, _serverOutput.printOutput)(result, output);
113
152
  return true;
114
153
  }
@@ -125,8 +164,13 @@ async function server(argv) {
125
164
  (0, _serverOutput.printError)(new Error(`Unknown verb: '${verb}'. Valid: ${VERBS.join(', ')}${extraVerbs}`));
126
165
  return false;
127
166
  } catch (err) {
128
- (0, _serverOutput.printError)(err);
129
- 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;
130
174
  }
131
175
  }
132
176
  async function handleFilesPut(dapi, rest, output) {
@@ -438,13 +482,20 @@ async function handleFunctionsList(dapi, argv, limit, offset, userFilter, output
438
482
  (0, _serverOutput.printOutput)(results, output);
439
483
  return true;
440
484
  }
441
- async function handleRaw(dapi, method, rest, output) {
485
+ async function handleRaw(dapi, method, rest, argv, output) {
442
486
  if (!method || !rest[0]) {
443
- (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)'));
444
488
  return false;
445
489
  }
446
- const path = rest[0];
447
- 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);
448
499
  (0, _serverOutput.printOutput)(result, output);
449
500
  return true;
450
501
  }
@@ -454,21 +505,15 @@ async function handleSync(dapi, subject, rest, argv, output) {
454
505
  // runs are read-only here; `run` triggers an actual push and prints
455
506
  // the per-item summary. Full reference in
456
507
  // core/docs/plans/instance-sync.md (Phase 7 / operational polish).
457
- //
458
- // `_callSync` does dual-path routing: tries `/api/sync/...` first
459
- // (nginx-fronted deployments) and falls back to `/sync/...` (bare
460
- // datlas on :8082). Same approach as the server-side handshake
461
- // code so the CLI works against either layout.
462
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
463
510
  const callSync = async (method, path, body) => {
464
- for (const prefix of ['/api', '']) {
465
- const r = body !== undefined ? await dapi.raw(method, `${prefix}${path}`, body) : await dapi.raw(method, `${prefix}${path}`);
466
- // raw() returns `null` for 404, but the server returns HTML for
467
- // 404 too treat anything that isn't a sync-shaped object/array
468
- // as a miss and fall through to the alternate prefix.
469
- 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;
470
516
  }
471
- return null;
472
517
  };
473
518
  if (subject === 'pairs' && verb === 'list') {
474
519
  const status = argv.status ? `?status=${encodeURIComponent(argv.status)}` : '';
@@ -533,8 +578,9 @@ async function handleSync(dapi, subject, rest, argv, output) {
533
578
  }
534
579
  async function handleHealthcheck(dapi, argv, output) {
535
580
  const module = argv.module;
536
- const path = module ? `/api/public/v1/healthcheck?module=${encodeURIComponent(module)}` : '/api/public/v1/healthcheck';
537
- 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 : [];
538
584
  if (output === 'json') {
539
585
  (0, _serverOutput.printOutput)(result, output);
540
586
  return true;
@@ -546,16 +592,25 @@ async function handleHealthcheck(dapi, argv, output) {
546
592
  console.log(`time: ${result?.time ?? ''}`);
547
593
  console.log('');
548
594
  }
549
- (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)');
550
596
  return true;
551
597
  }
552
598
  async function handleDescribe(dapi, entityType, output) {
553
599
  if (!entityType) {
554
- (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'));
555
601
  return false;
556
602
  }
557
- const result = await dapi.describe(entityType);
558
- (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);
559
614
  return true;
560
615
  }
561
616
  async function handleFuncRun(dapi, rest, argv, output) {
@@ -577,6 +632,7 @@ async function handleFuncRun(dapi, rest, argv, output) {
577
632
  const parsed = parseFuncCall(funcName);
578
633
  funcName = parsed.name;
579
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);
580
636
  }
581
637
  const result = await dapi.functions.run(funcName, params);
582
638
  (0, _serverOutput.printOutput)(result, output);
@@ -611,18 +667,25 @@ async function handleSharesAdd(dapi, rest, argv, output) {
611
667
  return true;
612
668
  }
613
669
  async function handleSharesList(dapi, rest, output) {
614
- const entityId = rest[0];
615
- if (!entityId) {
616
- (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"'));
617
672
  return false;
618
673
  }
674
+ const entityId = (0, _registry.isUuid)(String(rest[0])) ? String(rest[0]) : (await (0, _walker.resolveEntity)(dapi, String(rest[0]))).id;
619
675
  const perms = await dapi.shares.list(entityId);
620
- const flat = (Array.isArray(perms) ? perms : []).map(p => ({
621
- group: p?.userGroup?.friendlyName ?? p?.userGroup?.name ?? p?.userGroup?.id ?? '',
622
- groupId: p?.userGroup?.id ?? '',
623
- access: p?.permission?.name ?? p?.permission?.friendlyName ?? '',
624
- personal: p?.userGroup?.personal ?? false
625
- }));
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
+ });
626
689
  (0, _serverOutput.printOutput)(flat, output);
627
690
  return true;
628
691
  }
@@ -714,23 +777,14 @@ async function handleGroupRemoveMembers(dapi, rest, argv, output) {
714
777
  if (anyError) process.exitCode = 1;
715
778
  return true;
716
779
  }
717
- async function handleGroupListMembers(dapi, rest, argv, output) {
780
+ async function handleGroupListMembers(dapi, verb, rest, argv, output) {
718
781
  if (!rest[0]) {
719
- (0, _serverOutput.printError)(new Error('Usage: grok s groups list-members <group> [--admin | --no-admin]'));
782
+ (0, _serverOutput.printError)(new Error(`Usage: grok s groups ${verb} <group-or-login> [--admin | --no-admin] [--user]`));
720
783
  return false;
721
784
  }
722
785
  const admin = typeof argv.admin === 'boolean' ? argv.admin : undefined;
723
- const result = await dapi.groups.getMembers(rest[0], admin);
724
- (0, _serverOutput.printOutput)(result, output);
725
- return true;
726
- }
727
- async function handleGroupListMemberships(dapi, rest, argv, output) {
728
- if (!rest[0]) {
729
- (0, _serverOutput.printError)(new Error('Usage: grok s groups list-memberships <group> [--admin | --no-admin]'));
730
- return false;
731
- }
732
- const admin = typeof argv.admin === 'boolean' ? argv.admin : undefined;
733
- 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);
734
788
  (0, _serverOutput.printOutput)(result, output);
735
789
  return true;
736
790
  }
@@ -867,7 +921,7 @@ function parseFuncCall(expr) {
867
921
  params: Object.fromEntries(positional.map((v, i) => [String(i), v]))
868
922
  };
869
923
  }
870
- const HELP_SERVER = `
924
+ const HELP_SERVER = exports.HELP_SERVER = `
871
925
  Usage: grok server <entity> <verb> [args] [options]
872
926
  grok s <entity> <verb> [args] [options]
873
927
 
@@ -875,27 +929,29 @@ Manage a Datagrok server from the command line.
875
929
 
876
930
  Entities:
877
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)
878
933
 
879
934
  Verbs:
880
- list List entities
935
+ list List entities (--filter, --limit, --offset)
936
+ count Count entities (--filter)
881
937
  get Get a single entity by ID or name
882
- 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)
883
939
 
884
940
  Special commands:
885
- 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)
886
942
  grok s functions list [--type <t>] [--language <l>] [--package <p>] [--filter <expr>]
887
943
  Type: script|query|function|package
888
944
  Language applies to scripts (python, r, julia, nodejs, octave, grok)
889
- 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
890
946
  grok s files get <path> Download a file (returns bytes)
891
947
  grok s files delete <path> Delete a file
892
948
  grok s files put <local> <remote> Upload a local file
893
- grok s raw <METHOD> <path> Hit any API endpoint
894
- 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)
895
951
  grok s healthcheck [--module <name>] Check server + per-module health
896
952
  grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
897
953
  Share an entity with one or more groups
898
- 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
899
955
  grok s users save --json user.json Create or update a user from a JSON file
900
956
  grok s groups save --json group.json [--save-relations]
901
957
  Create or update a group from a JSON file
@@ -905,12 +961,15 @@ Special commands:
905
961
  grok s connections test --json conn.json Test connectivity of a connection defined in JSON
906
962
  grok s groups add-members <group> <m>... [--admin] Add one or more users/groups as members
907
963
  grok s groups remove-members <group> <m>... Remove members (no-op if not a member)
908
- grok s groups list-members <group> [--admin] List members (optionally filter by admin)
909
- 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
910
968
  grok s users block <id-or-login> Block a user from the platform
911
969
  grok s users unblock <id-or-login> Unblock a previously blocked user
912
970
  grok s tables upload <name> <file.csv|file.d42> Upload a CSV or d42 binary as a Datagrok table
913
- 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)
914
973
  grok s packages install <name>... [--version <v>] Install latest (or pinned) versions from the registry
915
974
  grok s packages uninstall <name> Uninstall a package (repository entry is kept)
916
975
  grok s packages update <name>... | --all Upgrade to the latest registry version
@@ -919,6 +978,30 @@ Special commands:
919
978
  grok s packages set-version <name> <version> Activate a specific published version
920
979
  grok s packages share <name> <group>[,...] [--access View|Edit]
921
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>]
922
1005
  grok s batch <entity> <verb> arg1 [arg2 ...] Batch operation (one round-trip)
923
1006
  grok s batch <entity> <verb> --json params.json Batch from JSON array
924
1007
  grok s batch manifest.json Run a workflow manifest
@@ -927,14 +1010,47 @@ Special commands:
927
1010
  grok s sync setup get <setup-id> Inspect a setup (selections, direction, last run)
928
1011
  grok s sync run <setup-id> Trigger a push run; prints per-item outcome
929
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
+
930
1041
  Options:
931
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
932
1046
  --output <format> Output format: table (default), json, csv, quiet
933
1047
  --filter <text> Smart filter expression
934
1048
  --limit <n> Page size (default: 50)
935
1049
  --offset <n> Start offset (default: 0)
936
1050
  -r, --recursive Recursive (for files list)
937
- --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
938
1054
  -O, --output-file Write table download to a file instead of stdout
939
1055
  --type <t> Function discriminator: script | query | function | package
940
1056
  --language <lang> Script language: python, r, julia, nodejs, octave, grok
@@ -950,6 +1066,7 @@ Batch manifest options (in manifest.json):
950
1066
  Examples:
951
1067
  grok s users list
952
1068
  grok s users list --output json --limit 10
1069
+ grok s users count --filter 'status = "active"'
953
1070
  grok s users save --json user.json
954
1071
  grok s groups save --json group.json --save-relations
955
1072
  grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
@@ -972,14 +1089,33 @@ Examples:
972
1089
  grok s packages update --all
973
1090
  grok s packages versions Chem
974
1091
  grok s packages share Chem Chemists --access View
975
- grok s raw GET /api/users/current
1092
+ grok s raw GET /users/current
1093
+ grok s raw POST /public/v1/functions/Sin/call --data '{"x": 1}'
976
1094
  grok s describe connections
1095
+ grok s tables download MyTable -O ./my-table.csv
977
1096
  grok s users list --host dev
978
1097
  grok s users list --host "https://mygrok.com/api"
979
1098
  grok s groups add-members Admins alice bob --admin
980
1099
  grok s groups remove-members Admins alice
981
1100
  grok s groups list-members Admins --admin
982
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
983
1119
  grok s batch files delete "System:AppData/old.txt" "System:DemoFiles/tmp.txt"
984
1120
  grok s batch users create --json users.json
985
1121
  grok s batch manifest.json
package/bin/grok.js CHANGED
@@ -36,6 +36,14 @@ const commands = {
36
36
 
37
37
  const onPackageCommandNames = ['api', 'check', 'link', 'publish', 'test'];
38
38
 
39
+ // A machine-readable run prints its error as JSON on stderr (server.ts) and nothing else:
40
+ // a usage dump on stdout would corrupt the output the caller parses.
41
+ const outputFormat = argv.output ?? argv.o;
42
+ function printUsage(command) {
43
+ if (outputFormat !== 'json')
44
+ process.stderr.write(`${help[command]}\n`);
45
+ }
46
+
39
47
  const command = argv['_'][0];
40
48
  if (command !== 'test' && command !== 'stresstest')
41
49
  delete argv.dartium;
@@ -52,23 +60,23 @@ if (command in commands) {
52
60
  if (result && typeof result.then === 'function') {
53
61
  result.then((ok) => {
54
62
  if (!ok) {
55
- console.log(help[command]);
63
+ printUsage(command);
56
64
  exitWithCode(1);
57
65
  }
58
66
  }).catch((err) => {
59
67
  console.error(err);
60
- console.log(help[command]);
68
+ printUsage(command);
61
69
  exitWithCode(255);
62
70
  });
63
71
  }
64
72
  else if (!result) {
65
- console.log(help[command]);
73
+ printUsage(command);
66
74
  exitWithCode(1);
67
75
  }
68
76
  }
69
77
  } catch (err) {
70
78
  console.error(err);
71
- console.log(help[command]);
79
+ printUsage(command);
72
80
  exitWithCode(255);
73
81
  }
74
82
  } else
@@ -76,6 +84,7 @@ if (command in commands) {
76
84
 
77
85
 
78
86
  function exitWithCode(code) {
79
- console.log(`Exiting with code ${code}`);
87
+ if (outputFormat !== 'json')
88
+ console.log(`Exiting with code ${code}`);
80
89
  process.exit(code);
81
90
  }