datagrok-tools 6.1.8 → 6.1.10

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.
@@ -0,0 +1,277 @@
1
+ "use strict";
2
+
3
+ var _vitest = require("vitest");
4
+ var os = _interopRequireWildcard(require("os"));
5
+ var fs = _interopRequireWildcard(require("fs"));
6
+ var path = _interopRequireWildcard(require("path"));
7
+ var _server = require("../commands/server");
8
+ 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); }
9
+ (0, _vitest.describe)('parseFuncCall', () => {
10
+ (0, _vitest.it)('parses a single string argument', () => {
11
+ (0, _vitest.expect)((0, _server.parseFuncCall)('Chem:smilesToMw("ccc")')).toEqual({
12
+ name: 'Chem:smilesToMw',
13
+ params: {
14
+ '0': 'ccc'
15
+ }
16
+ });
17
+ });
18
+ (0, _vitest.it)('parses a single-quoted string argument', () => {
19
+ (0, _vitest.expect)((0, _server.parseFuncCall)("Pkg:fn('hello')")).toEqual({
20
+ name: 'Pkg:fn',
21
+ params: {
22
+ '0': 'hello'
23
+ }
24
+ });
25
+ });
26
+ (0, _vitest.it)('parses a numeric argument', () => {
27
+ (0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn(42)')).toEqual({
28
+ name: 'Pkg:fn',
29
+ params: {
30
+ '0': 42
31
+ }
32
+ });
33
+ });
34
+ (0, _vitest.it)('parses multiple positional arguments', () => {
35
+ (0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn(1, "hello", 3.14)')).toEqual({
36
+ name: 'Pkg:fn',
37
+ params: {
38
+ '0': 1,
39
+ '1': 'hello',
40
+ '2': 3.14
41
+ }
42
+ });
43
+ });
44
+ (0, _vitest.it)('parses an object argument with quoted keys', () => {
45
+ (0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn({"a":5,"b":22})')).toEqual({
46
+ name: 'Pkg:fn',
47
+ params: {
48
+ a: 5,
49
+ b: 22
50
+ }
51
+ });
52
+ });
53
+ (0, _vitest.it)('parses an object argument with unquoted keys', () => {
54
+ (0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn({a:5,b:22})')).toEqual({
55
+ name: 'Pkg:fn',
56
+ params: {
57
+ a: 5,
58
+ b: 22
59
+ }
60
+ });
61
+ });
62
+ (0, _vitest.it)('parses an object argument with a boolean value', () => {
63
+ (0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn({unquoted:true})')).toEqual({
64
+ name: 'Pkg:fn',
65
+ params: {
66
+ unquoted: true
67
+ }
68
+ });
69
+ });
70
+ (0, _vitest.it)('returns empty params for a no-argument call', () => {
71
+ (0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn()')).toEqual({
72
+ name: 'Pkg:fn',
73
+ params: {}
74
+ });
75
+ });
76
+ (0, _vitest.it)('returns empty params when there are no parentheses', () => {
77
+ (0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn')).toEqual({
78
+ name: 'Pkg:fn',
79
+ params: {}
80
+ });
81
+ });
82
+ (0, _vitest.it)('handles a package-less function name', () => {
83
+ (0, _vitest.expect)((0, _server.parseFuncCall)('myFunc("arg")')).toEqual({
84
+ name: 'myFunc',
85
+ params: {
86
+ '0': 'arg'
87
+ }
88
+ });
89
+ });
90
+ });
91
+ (0, _vitest.describe)('buildInlineManifest', () => {
92
+ (0, _vitest.it)('sets action to entity.verb', () => {
93
+ const result = (0, _server.buildInlineManifest)('users', 'delete', ['abc']);
94
+ (0, _vitest.expect)(result.operations[0].action).toBe('users.delete');
95
+ });
96
+ (0, _vitest.it)('maps string args to {id} param for non-file entities', () => {
97
+ const result = (0, _server.buildInlineManifest)('users', 'delete', ['id1', 'id2']);
98
+ (0, _vitest.expect)(result.operations).toEqual([{
99
+ id: 'op0',
100
+ action: 'users.delete',
101
+ params: {
102
+ id: 'id1'
103
+ }
104
+ }, {
105
+ id: 'op1',
106
+ action: 'users.delete',
107
+ params: {
108
+ id: 'id2'
109
+ }
110
+ }]);
111
+ });
112
+ (0, _vitest.it)('maps string args to {path} param for the files entity', () => {
113
+ const result = (0, _server.buildInlineManifest)('files', 'delete', ['System:AppData/a.txt', 'System:AppData/b.txt']);
114
+ (0, _vitest.expect)(result.operations).toEqual([{
115
+ id: 'op0',
116
+ action: 'files.delete',
117
+ params: {
118
+ path: 'System:AppData/a.txt'
119
+ }
120
+ }, {
121
+ id: 'op1',
122
+ action: 'files.delete',
123
+ params: {
124
+ path: 'System:AppData/b.txt'
125
+ }
126
+ }]);
127
+ });
128
+ (0, _vitest.it)('passes object array args through as params directly', () => {
129
+ const objs = [{
130
+ name: 'Alice',
131
+ email: 'a@x.com'
132
+ }, {
133
+ name: 'Bob',
134
+ email: 'b@x.com'
135
+ }];
136
+ const result = (0, _server.buildInlineManifest)('users', 'create', objs);
137
+ (0, _vitest.expect)(result.operations).toEqual([{
138
+ id: 'op0',
139
+ action: 'users.create',
140
+ params: {
141
+ name: 'Alice',
142
+ email: 'a@x.com'
143
+ }
144
+ }, {
145
+ id: 'op1',
146
+ action: 'users.create',
147
+ params: {
148
+ name: 'Bob',
149
+ email: 'b@x.com'
150
+ }
151
+ }]);
152
+ });
153
+ (0, _vitest.it)('assigns sequential op ids starting at op0', () => {
154
+ const result = (0, _server.buildInlineManifest)('groups', 'delete', ['x', 'y', 'z']);
155
+ (0, _vitest.expect)(result.operations.map(o => o.id)).toEqual(['op0', 'op1', 'op2']);
156
+ });
157
+ (0, _vitest.it)('returns a single operation for a single arg', () => {
158
+ const result = (0, _server.buildInlineManifest)('connections', 'delete', ['conn-id']);
159
+ (0, _vitest.expect)(result.operations).toHaveLength(1);
160
+ });
161
+ });
162
+ (0, _vitest.describe)('resolveManifestSources', () => {
163
+ let tmpFile;
164
+ (0, _vitest.beforeEach)(() => {
165
+ tmpFile = path.join(os.tmpdir(), `grok-batch-test-${Date.now()}.bin`);
166
+ });
167
+ (0, _vitest.afterEach)(() => {
168
+ if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
169
+ });
170
+ (0, _vitest.it)('passes through a manifest with no files.put operations unchanged', () => {
171
+ const manifest = {
172
+ operations: [{
173
+ id: 'op0',
174
+ action: 'users.delete',
175
+ params: {
176
+ id: 'abc'
177
+ }
178
+ }]
179
+ };
180
+ (0, _vitest.expect)((0, _server.resolveManifestSources)(manifest)).toEqual(manifest);
181
+ });
182
+ (0, _vitest.it)('passes through a files.put operation that has no source field', () => {
183
+ const manifest = {
184
+ operations: [{
185
+ id: 'op0',
186
+ action: 'files.put',
187
+ params: {
188
+ path: 'System:AppData/f.txt',
189
+ content: 'aGk='
190
+ }
191
+ }]
192
+ };
193
+ (0, _vitest.expect)((0, _server.resolveManifestSources)(manifest)).toEqual(manifest);
194
+ });
195
+ (0, _vitest.it)('reads the source file, base64-encodes its contents, and removes the source key', () => {
196
+ const data = 'hello world';
197
+ fs.writeFileSync(tmpFile, data);
198
+ const manifest = {
199
+ operations: [{
200
+ id: 'op0',
201
+ action: 'files.put',
202
+ params: {
203
+ path: 'System:AppData/f.txt',
204
+ source: tmpFile
205
+ }
206
+ }]
207
+ };
208
+ const result = (0, _server.resolveManifestSources)(manifest);
209
+ const params = result.operations[0].params;
210
+ (0, _vitest.expect)(params.content).toBe(Buffer.from(data).toString('base64'));
211
+ (0, _vitest.expect)(params.source).toBeUndefined();
212
+ });
213
+ (0, _vitest.it)('preserves other params alongside the injected content', () => {
214
+ fs.writeFileSync(tmpFile, 'data');
215
+ const manifest = {
216
+ operations: [{
217
+ id: 'op0',
218
+ action: 'files.put',
219
+ params: {
220
+ path: 'System:AppData/f.txt',
221
+ source: tmpFile,
222
+ extra: 'val'
223
+ }
224
+ }]
225
+ };
226
+ const result = (0, _server.resolveManifestSources)(manifest);
227
+ const params = result.operations[0].params;
228
+ (0, _vitest.expect)(params.path).toBe('System:AppData/f.txt');
229
+ (0, _vitest.expect)(params.extra).toBe('val');
230
+ });
231
+ (0, _vitest.it)('leaves non-files.put operations untouched in a mixed manifest', () => {
232
+ fs.writeFileSync(tmpFile, 'x');
233
+ const manifest = {
234
+ operations: [{
235
+ id: 'op0',
236
+ action: 'users.delete',
237
+ params: {
238
+ id: 'u1'
239
+ }
240
+ }, {
241
+ id: 'op1',
242
+ action: 'files.put',
243
+ params: {
244
+ path: 'System:AppData/f.txt',
245
+ source: tmpFile
246
+ }
247
+ }]
248
+ };
249
+ const result = (0, _server.resolveManifestSources)(manifest);
250
+ (0, _vitest.expect)(result.operations[0]).toEqual({
251
+ id: 'op0',
252
+ action: 'users.delete',
253
+ params: {
254
+ id: 'u1'
255
+ }
256
+ });
257
+ (0, _vitest.expect)(result.operations[1].params.source).toBeUndefined();
258
+ (0, _vitest.expect)(result.operations[1].params.content).toBeDefined();
259
+ });
260
+ (0, _vitest.it)('correctly encodes binary file contents', () => {
261
+ const bytes = Buffer.from([0x00, 0xff, 0x10, 0xab]);
262
+ fs.writeFileSync(tmpFile, bytes);
263
+ const manifest = {
264
+ operations: [{
265
+ id: 'op0',
266
+ action: 'files.put',
267
+ params: {
268
+ path: 'System:AppData/f.bin',
269
+ source: tmpFile
270
+ }
271
+ }]
272
+ };
273
+ const result = (0, _server.resolveManifestSources)(manifest);
274
+ const decoded = Buffer.from(result.operations[0].params.content, 'base64');
275
+ (0, _vitest.expect)(decoded).toEqual(bytes);
276
+ });
277
+ });
@@ -0,0 +1,197 @@
1
+ import {describe, it, expect, beforeEach, afterEach} from 'vitest';
2
+ import * as os from 'os';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import {parseFuncCall, buildInlineManifest, resolveManifestSources} from '../commands/server';
6
+
7
+ describe('parseFuncCall', () => {
8
+ it('parses a single string argument', () => {
9
+ expect(parseFuncCall('Chem:smilesToMw("ccc")')).toEqual({
10
+ name: 'Chem:smilesToMw',
11
+ params: {'0': 'ccc'},
12
+ });
13
+ });
14
+
15
+ it('parses a single-quoted string argument', () => {
16
+ expect(parseFuncCall("Pkg:fn('hello')")).toEqual({
17
+ name: 'Pkg:fn',
18
+ params: {'0': 'hello'},
19
+ });
20
+ });
21
+
22
+ it('parses a numeric argument', () => {
23
+ expect(parseFuncCall('Pkg:fn(42)')).toEqual({
24
+ name: 'Pkg:fn',
25
+ params: {'0': 42},
26
+ });
27
+ });
28
+
29
+ it('parses multiple positional arguments', () => {
30
+ expect(parseFuncCall('Pkg:fn(1, "hello", 3.14)')).toEqual({
31
+ name: 'Pkg:fn',
32
+ params: {'0': 1, '1': 'hello', '2': 3.14},
33
+ });
34
+ });
35
+
36
+ it('parses an object argument with quoted keys', () => {
37
+ expect(parseFuncCall('Pkg:fn({"a":5,"b":22})')).toEqual({
38
+ name: 'Pkg:fn',
39
+ params: {a: 5, b: 22},
40
+ });
41
+ });
42
+
43
+ it('parses an object argument with unquoted keys', () => {
44
+ expect(parseFuncCall('Pkg:fn({a:5,b:22})')).toEqual({
45
+ name: 'Pkg:fn',
46
+ params: {a: 5, b: 22},
47
+ });
48
+ });
49
+
50
+ it('parses an object argument with a boolean value', () => {
51
+ expect(parseFuncCall('Pkg:fn({unquoted:true})')).toEqual({
52
+ name: 'Pkg:fn',
53
+ params: {unquoted: true},
54
+ });
55
+ });
56
+
57
+ it('returns empty params for a no-argument call', () => {
58
+ expect(parseFuncCall('Pkg:fn()')).toEqual({
59
+ name: 'Pkg:fn',
60
+ params: {},
61
+ });
62
+ });
63
+
64
+ it('returns empty params when there are no parentheses', () => {
65
+ expect(parseFuncCall('Pkg:fn')).toEqual({
66
+ name: 'Pkg:fn',
67
+ params: {},
68
+ });
69
+ });
70
+
71
+ it('handles a package-less function name', () => {
72
+ expect(parseFuncCall('myFunc("arg")')).toEqual({
73
+ name: 'myFunc',
74
+ params: {'0': 'arg'},
75
+ });
76
+ });
77
+ });
78
+
79
+ describe('buildInlineManifest', () => {
80
+ it('sets action to entity.verb', () => {
81
+ const result = buildInlineManifest('users', 'delete', ['abc']);
82
+ expect(result.operations[0].action).toBe('users.delete');
83
+ });
84
+
85
+ it('maps string args to {id} param for non-file entities', () => {
86
+ const result = buildInlineManifest('users', 'delete', ['id1', 'id2']);
87
+ expect(result.operations).toEqual([
88
+ {id: 'op0', action: 'users.delete', params: {id: 'id1'}},
89
+ {id: 'op1', action: 'users.delete', params: {id: 'id2'}},
90
+ ]);
91
+ });
92
+
93
+ it('maps string args to {path} param for the files entity', () => {
94
+ const result = buildInlineManifest('files', 'delete', ['System:AppData/a.txt', 'System:AppData/b.txt']);
95
+ expect(result.operations).toEqual([
96
+ {id: 'op0', action: 'files.delete', params: {path: 'System:AppData/a.txt'}},
97
+ {id: 'op1', action: 'files.delete', params: {path: 'System:AppData/b.txt'}},
98
+ ]);
99
+ });
100
+
101
+ it('passes object array args through as params directly', () => {
102
+ const objs = [{name: 'Alice', email: 'a@x.com'}, {name: 'Bob', email: 'b@x.com'}];
103
+ const result = buildInlineManifest('users', 'create', objs);
104
+ expect(result.operations).toEqual([
105
+ {id: 'op0', action: 'users.create', params: {name: 'Alice', email: 'a@x.com'}},
106
+ {id: 'op1', action: 'users.create', params: {name: 'Bob', email: 'b@x.com'}},
107
+ ]);
108
+ });
109
+
110
+ it('assigns sequential op ids starting at op0', () => {
111
+ const result = buildInlineManifest('groups', 'delete', ['x', 'y', 'z']);
112
+ expect(result.operations.map((o) => o.id)).toEqual(['op0', 'op1', 'op2']);
113
+ });
114
+
115
+ it('returns a single operation for a single arg', () => {
116
+ const result = buildInlineManifest('connections', 'delete', ['conn-id']);
117
+ expect(result.operations).toHaveLength(1);
118
+ });
119
+ });
120
+
121
+ describe('resolveManifestSources', () => {
122
+ let tmpFile: string;
123
+
124
+ beforeEach(() => {
125
+ tmpFile = path.join(os.tmpdir(), `grok-batch-test-${Date.now()}.bin`);
126
+ });
127
+
128
+ afterEach(() => {
129
+ if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
130
+ });
131
+
132
+ it('passes through a manifest with no files.put operations unchanged', () => {
133
+ const manifest = {
134
+ operations: [{id: 'op0', action: 'users.delete', params: {id: 'abc'}}],
135
+ };
136
+ expect(resolveManifestSources(manifest)).toEqual(manifest);
137
+ });
138
+
139
+ it('passes through a files.put operation that has no source field', () => {
140
+ const manifest = {
141
+ operations: [{id: 'op0', action: 'files.put', params: {path: 'System:AppData/f.txt', content: 'aGk='}}],
142
+ };
143
+ expect(resolveManifestSources(manifest)).toEqual(manifest);
144
+ });
145
+
146
+ it('reads the source file, base64-encodes its contents, and removes the source key', () => {
147
+ const data = 'hello world';
148
+ fs.writeFileSync(tmpFile, data);
149
+
150
+ const manifest = {
151
+ operations: [{id: 'op0', action: 'files.put', params: {path: 'System:AppData/f.txt', source: tmpFile}}],
152
+ };
153
+ const result = resolveManifestSources(manifest);
154
+ const params = result.operations[0].params as any;
155
+ expect(params.content).toBe(Buffer.from(data).toString('base64'));
156
+ expect(params.source).toBeUndefined();
157
+ });
158
+
159
+ it('preserves other params alongside the injected content', () => {
160
+ fs.writeFileSync(tmpFile, 'data');
161
+
162
+ const manifest = {
163
+ operations: [{id: 'op0', action: 'files.put', params: {path: 'System:AppData/f.txt', source: tmpFile, extra: 'val'}}],
164
+ };
165
+ const result = resolveManifestSources(manifest);
166
+ const params = result.operations[0].params as any;
167
+ expect(params.path).toBe('System:AppData/f.txt');
168
+ expect(params.extra).toBe('val');
169
+ });
170
+
171
+ it('leaves non-files.put operations untouched in a mixed manifest', () => {
172
+ fs.writeFileSync(tmpFile, 'x');
173
+
174
+ const manifest = {
175
+ operations: [
176
+ {id: 'op0', action: 'users.delete', params: {id: 'u1'}},
177
+ {id: 'op1', action: 'files.put', params: {path: 'System:AppData/f.txt', source: tmpFile}},
178
+ ],
179
+ };
180
+ const result = resolveManifestSources(manifest);
181
+ expect(result.operations[0]).toEqual({id: 'op0', action: 'users.delete', params: {id: 'u1'}});
182
+ expect((result.operations[1].params as any).source).toBeUndefined();
183
+ expect((result.operations[1].params as any).content).toBeDefined();
184
+ });
185
+
186
+ it('correctly encodes binary file contents', () => {
187
+ const bytes = Buffer.from([0x00, 0xff, 0x10, 0xab]);
188
+ fs.writeFileSync(tmpFile, bytes);
189
+
190
+ const manifest = {
191
+ operations: [{id: 'op0', action: 'files.put', params: {path: 'System:AppData/f.bin', source: tmpFile}}],
192
+ };
193
+ const result = resolveManifestSources(manifest);
194
+ const decoded = Buffer.from((result.operations[0].params as any).content, 'base64');
195
+ expect(decoded).toEqual(bytes);
196
+ });
197
+ });
@@ -54,7 +54,7 @@ async function buildRecursive(baseDir, args, buildCmd) {
54
54
  return false;
55
55
  }
56
56
  console.log(`Found ${filtered.length} package(s): ${filtered.map(p => p.friendlyName).join(', ')}`);
57
- if (!args.silent) {
57
+ if (!args.silent && !args.s) {
58
58
  const confirmed = await confirm(`\nBuild ${filtered.length} package(s)?`);
59
59
  if (!confirmed) {
60
60
  console.log('Aborted.');
@@ -26,7 +26,7 @@ const confTemplate = _jsYaml.default.load(_fs.default.readFileSync(confTemplateD
26
26
  encoding: 'utf-8'
27
27
  }));
28
28
  const dependencies = [];
29
- function createDirectoryContents(name, config, templateDir, packageDir, ide = '', ts = true, eslint = false, test = false) {
29
+ function createDirectoryContents(name, friendlyName, config, templateDir, packageDir, ide = '', ts = true, eslint = false, test = false) {
30
30
  const filesToCreate = _fs.default.readdirSync(templateDir);
31
31
  filesToCreate.forEach(file => {
32
32
  const origFilePath = _path.default.join(templateDir, file);
@@ -38,6 +38,7 @@ function createDirectoryContents(name, config, templateDir, packageDir, ide = ''
38
38
  return false;
39
39
  }
40
40
  let contents = _fs.default.readFileSync(file === 'webpack.config.js' && ts ? _path.default.join(templateDir, 'ts.webpack.config.js') : origFilePath, 'utf8');
41
+ contents = contents.replace(/#{PACKAGE_FRIENDLY_NAME}/g, friendlyName);
41
42
  contents = contents.replace(/#{PACKAGE_NAME}/g, name);
42
43
  contents = contents.replace(/#{PACKAGE_DETECTORS_NAME}/g, utils.kebabToCamelCase(name));
43
44
  contents = contents.replace(/#{PACKAGE_NAME_LOWERCASE}/g, name.toLowerCase());
@@ -114,7 +115,7 @@ function createDirectoryContents(name, config, templateDir, packageDir, ide = ''
114
115
  _fs.default.mkdirSync(copyFilePath);
115
116
  // recursive call
116
117
  if (_path.default.basename(origFilePath) === 'node_modules') return;
117
- createDirectoryContents(name, config, origFilePath, copyFilePath, ide, ts, eslint, test);
118
+ createDirectoryContents(name, friendlyName, config, origFilePath, copyFilePath, ide, ts, eslint, test);
118
119
  }
119
120
  });
120
121
  }
@@ -138,8 +139,10 @@ function create(args) {
138
139
  color.error(confTest.message);
139
140
  return false;
140
141
  }
141
- const name = nArgs === 2 ? args['_'][1] : curFolder;
142
- const validName = /^([A-Za-z\-_\d])+$/.test(name);
142
+ const rawName = nArgs === 2 ? args['_'][1] : curFolder;
143
+ const validName = /^([A-Za-z\-_\d])+$/.test(rawName);
144
+ const friendlyName = rawName;
145
+ const name = rawName.charAt(0).toUpperCase() + rawName.slice(1).toLowerCase();
143
146
  if (validName) {
144
147
  let packageDir = curDir;
145
148
  let repositoryInfo = null;
@@ -183,7 +186,7 @@ function create(args) {
183
186
  }
184
187
  process.exit();
185
188
  });
186
- createDirectoryContents(name, config, templateDir, packageDir, args.ide, ts, !!args.eslint, !!args.test);
189
+ createDirectoryContents(name, friendlyName, config, templateDir, packageDir, args.ide, ts, !!args.eslint, !!args.test);
187
190
  color.success('Successfully created package ' + name);
188
191
  console.log(_entHelpers.help.package(ts));
189
192
  console.log(`\nThe package has the following dependencies:\n${dependencies.join(' ')}\n`);
@@ -13,7 +13,7 @@ Commands:
13
13
  add Add an object template
14
14
  api Create wrapper functions
15
15
  build Build a package or multiple packages
16
- check Check package content (function signatures, etc.)
16
+ check Check package content (function signatures, etgc.)
17
17
  claude Launch Claude Code in a Datagrok dev container
18
18
  config Create and manage config files
19
19
  create Create a package
@@ -26,6 +26,7 @@ Commands:
26
26
  test Run package tests
27
27
  testall Run packages tests
28
28
  migrate Migrate legacy tags to meta.role
29
+ server (s) Manage a Datagrok server (list/get/delete entities, run functions)
29
30
 
30
31
  To get help on a particular command, use:
31
32
  grok <command> --help
@@ -359,6 +360,63 @@ Examples:
359
360
 
360
361
  The instance name must match a server alias in ~/.grok/config.yaml.
361
362
  `;
363
+ const HELP_SERVER = `
364
+ Usage: grok server <entity> <verb> [args] [options]
365
+ grok s <entity> <verb> [args] [options]
366
+
367
+ Manage a Datagrok server from the command line.
368
+
369
+ Entities:
370
+ users, groups, functions, connections, queries, scripts, packages, reports, files
371
+
372
+ Verbs:
373
+ list List entities
374
+ get Get a single entity by ID or name
375
+ delete Delete an entity by ID
376
+
377
+ Special commands:
378
+ grok s functions run <Name:func(args)> Call a function
379
+ grok s files list [path] [-r] List files (recursive with -r)
380
+ grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
381
+ Share an entity with groups
382
+ grok s shares list <entity-id> List who an entity (UUID) is shared with
383
+ grok s users save --json user.json Create or update a user from JSON
384
+ grok s groups save --json group.json [--save-relations]
385
+ Create or update a group from JSON
386
+ grok s connections save --json conn.json [--save-credentials]
387
+ Create or update a connection from JSON
388
+ grok s connections test <id-or-name> Test connectivity of an existing connection
389
+ grok s connections test --json conn.json Test connectivity of a connection defined in JSON
390
+ grok s raw <METHOD> <path> Hit any API endpoint
391
+ grok s describe <entity-type> Show entity JSON schema
392
+
393
+ Options:
394
+ --host <alias|url> Server alias from config or full URL
395
+ --output <format> Output format: table (default), json, csv, quiet
396
+ --filter <text> Smart filter expression
397
+ --limit <n> Page size (default: 50)
398
+ --offset <n> Start offset (default: 0)
399
+ -r, --recursive Recursive (for files list)
400
+ --json <file> Read function parameters from JSON file
401
+
402
+ Examples:
403
+ grok s users list
404
+ grok s connections list --filter "PostgreSQL" --output json
405
+ grok s connections get <id>
406
+ grok s connections delete <id>
407
+ grok s connections save --json conn.json --save-credentials
408
+ grok s connections test "JohnDoe:MyConnection"
409
+ grok s connections test --json conn.json
410
+ grok s users save --json user.json
411
+ grok s groups save --json group.json --save-relations
412
+ grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
413
+ grok s shares list <entity-uuid>
414
+ grok s functions run 'Chem:smilesToMw("ccc")'
415
+ grok s files list "System:AppData" -r
416
+ grok s raw GET /api/users/current
417
+ grok s describe connections
418
+ grok s users list --host dev
419
+ `;
362
420
  const help = exports.help = {
363
421
  add: HELP_ADD,
364
422
  api: HELP_API,
@@ -376,5 +434,7 @@ const help = exports.help = {
376
434
  test: HELP_TEST,
377
435
  testall: HELP_TESTALL,
378
436
  migrate: HELP_MIGRATE,
437
+ server: HELP_SERVER,
438
+ s: HELP_SERVER,
379
439
  help: HELP
380
440
  };
@@ -307,6 +307,10 @@ async function processDockerImages(packageName, version, registry, devKey, host,
307
307
  if (registry) {
308
308
  const remoteTag = `${registry}/datagrok/${remoteFullName}`;
309
309
  if (foundLocalName !== remoteTag) dockerTag(foundLocalName, remoteTag);
310
+ } else {
311
+ const canonicalTag = `datagrok/${remoteFullName}`;
312
+ dockerTag(foundLocalName, canonicalTag);
313
+ color.log(` Tagged as ${canonicalTag}`);
310
314
  }
311
315
  result = pushImage(img.imageName, registryTag, registry);
312
316
  } else {
@@ -331,6 +335,13 @@ async function processDockerImages(packageName, version, registry, devKey, host,
331
335
  requestedVersion: registryTag
332
336
  };
333
337
  if (!result || result.fallback) color.warn(`Build failed. Falling back to ${fallback.image} (hash mismatch)`);
338
+ } else if (skipDockerRebuild) {
339
+ color.warn(`No fallback available. Skipping docker build (--skip-docker-rebuild).`);
340
+ result = {
341
+ image: null,
342
+ fallback: true,
343
+ requestedVersion: registryTag
344
+ };
334
345
  } else {
335
346
  // No fallback and no local image — must build
336
347
  color.warn(`No fallback available. Building ${img.fullLocalName}...`);
@@ -369,7 +380,7 @@ function pushImage(imageName, tag, registry) {
369
380
  fallback: false
370
381
  };
371
382
  }
372
- color.warn('No registry configured. Image tagged locally only.');
383
+ color.warn(`No registry configured. Image tagged locally only. Run \`grok config --registry\` to configure.`);
373
384
  return {
374
385
  image: canonicalImage,
375
386
  fallback: false