datagrok-tools 6.6.0 → 6.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/entrypoint.sh +0 -0
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +11 -1
- package/CLAUDE.md +5 -6
- package/GROK_S.md +10 -2
- package/LICENSE.md +16 -0
- package/README.md +5 -3
- package/bin/commands/build.js +123 -139
- package/bin/commands/check.js +3 -4
- package/bin/commands/config.js +4 -2
- package/bin/commands/create.js +37 -11
- package/bin/commands/help.js +85 -17
- package/bin/commands/link.js +10 -0
- package/bin/commands/login.js +213 -0
- package/bin/commands/publish.js +26 -23
- package/bin/commands/server.js +7 -1
- package/bin/commands/setup.js +139 -0
- package/bin/commands/stress-tests.js +3 -1
- package/bin/commands/tsc.js +26 -0
- package/bin/grok.js +34 -20
- package/bin/utils/color-utils.js +30 -1
- package/bin/utils/dev-key.js +45 -5
- package/bin/utils/keypair.js +292 -0
- package/bin/utils/node-dapi.js +20 -6
- package/bin/utils/playwright-runner.js +9 -3
- package/bin/utils/server-client.js +6 -7
- package/bin/utils/test-utils.js +19 -1
- package/bin/utils/toolchain.js +121 -0
- package/bin/utils/utils.js +9 -0
- package/package-template/.vscode/tasks.json +1 -1
- package/package-template/package.json +9 -14
- package/package-template/tsconfig.json +2 -69
- package/package.json +17 -17
- package/plugins/func-gen-plugin.js +9 -4
- package/turbo.json +9 -0
- package/package-template/.eslintrc.json +0 -38
- package/package-template/ts.webpack.config.js +0 -46
- package/package-template/webpack.config.js +0 -35
package/bin/commands/help.js
CHANGED
|
@@ -13,14 +13,17 @@ Datagrok's package management tool
|
|
|
13
13
|
Commands:
|
|
14
14
|
add Add an object template
|
|
15
15
|
api Create wrapper functions
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
setup Get a public/ checkout ready to build (pnpm, install, npm-era clean-up)
|
|
17
|
+
build Build a package and its dependencies (Turborepo), or just this package
|
|
18
|
+
tsc Run the workspace TypeScript compiler
|
|
19
|
+
check Check package content (function signatures, etc.)
|
|
18
20
|
claude Launch Claude Code in a Datagrok dev container
|
|
19
21
|
config Create and manage config files
|
|
20
22
|
create Create a package
|
|
21
23
|
docker-gen Generate Celery Docker artifacts from Python functions
|
|
22
24
|
init Modify a package template
|
|
23
25
|
link Link \`datagrok-api\` and libraries for local development
|
|
26
|
+
login Log in to a server with a keypair (replaces the developer key)
|
|
24
27
|
publish Upload a package
|
|
25
28
|
report Manage user error reports (fetch, resolve, create ticket)
|
|
26
29
|
run Build, publish, and open in browser
|
|
@@ -138,6 +141,29 @@ Options:
|
|
|
138
141
|
file exists, plain \`grok api\` keeps it up to date; delete it to
|
|
139
142
|
opt out again
|
|
140
143
|
`;
|
|
144
|
+
const HELP_LOGIN = `
|
|
145
|
+
Usage: grok login <server>
|
|
146
|
+
|
|
147
|
+
Log in to a Datagrok server with a keypair. Generates an EC P-256 key, registers
|
|
148
|
+
its public half on your account, and stores the private half in
|
|
149
|
+
~/.grok/keys/<alias>.json. Nothing reusable is ever copied out of the UI, and the
|
|
150
|
+
key can be given an expiry and revoked on its own.
|
|
151
|
+
|
|
152
|
+
grok login https://dev.datagrok.ai Approve the key in the browser
|
|
153
|
+
grok login dev --code AB12CD34 Use a code from your profile page
|
|
154
|
+
(Profile > Public keys...), no browser
|
|
155
|
+
|
|
156
|
+
Options:
|
|
157
|
+
[--code] [--name] [--expires] [--alias]
|
|
158
|
+
|
|
159
|
+
--code One-shot enrollment code from your profile page. Skips the browser
|
|
160
|
+
--name Key name shown in your profile (default: user@host)
|
|
161
|
+
--expires Days from now, or an ISO date (2027-01-31). Default: never
|
|
162
|
+
--alias Config alias to write (default: the server's first host label)
|
|
163
|
+
|
|
164
|
+
For CI, set GROK_PRIVATE_KEY to the private key JWK (raw or base64) instead of a
|
|
165
|
+
config file. Read more: https://datagrok.ai/help/govern/access-control/keypair-authentication
|
|
166
|
+
`;
|
|
141
167
|
const HELP_CONFIG = `
|
|
142
168
|
Usage: grok config
|
|
143
169
|
|
|
@@ -150,6 +176,7 @@ Options:
|
|
|
150
176
|
--server Use to add a server to the config (\`grok config add --alias alias --server url --key key\`)
|
|
151
177
|
--alias Use in conjunction with the \`server\` option to set the server name
|
|
152
178
|
--key Use in conjunction with the \`server\` option to set the developer key
|
|
179
|
+
(deprecated - prefer \`grok login\`, which needs no key here)
|
|
153
180
|
--default Use in conjunction with the \`server\` option to set the added server as default
|
|
154
181
|
--registry Docker registry URL (default: registry.{server hostname})
|
|
155
182
|
`;
|
|
@@ -184,7 +211,8 @@ Options:
|
|
|
184
211
|
--all Publish all available packages (run in packages directory)
|
|
185
212
|
--refresh Publish all available already loaded packages (run in packages directory)
|
|
186
213
|
--link Link the package to local packages
|
|
187
|
-
--build Builds the package
|
|
214
|
+
--build Builds the package (the default; kept for compatibility)
|
|
215
|
+
--skip-build Upload the existing dist/ without rebuilding
|
|
188
216
|
--release Publish package as release version
|
|
189
217
|
--rebuild-docker Force rebuild Docker images locally before pushing to registry
|
|
190
218
|
--skip-docker-rebuild Skip auto-rebuild when Dockerfile folder has changed
|
|
@@ -283,6 +311,9 @@ https://datagrok.ai/help/develop/how-to/test-packages#local-testing
|
|
|
283
311
|
const HELP_LINK = `
|
|
284
312
|
Usage: grok link
|
|
285
313
|
|
|
314
|
+
In a pnpm workspace checkout (public/) this is a no-op: \`pnpm install\` at the repository root
|
|
315
|
+
links every in-repo dependency (workspace:^). The options below apply to standalone checkouts only.
|
|
316
|
+
|
|
286
317
|
Links \`datagrok-api\`, all necessary libraries and packages for local development.
|
|
287
318
|
Uses \`npm link\` unless the --path option specified.
|
|
288
319
|
By default, it links packages from the parent directory of the repository's root.
|
|
@@ -316,27 +347,61 @@ Example:
|
|
|
316
347
|
⟶
|
|
317
348
|
meta: { role: 'viewer,ml' }
|
|
318
349
|
`;
|
|
350
|
+
const HELP_SETUP = `
|
|
351
|
+
Usage: grok setup [--check] [--global]
|
|
352
|
+
|
|
353
|
+
Gets a public/ checkout (or a fresh worktree) ready to build, and keeps it that way after a pull:
|
|
354
|
+
1. Node 20+ (22 recommended); pnpm through corepack, at the version the workspace pins
|
|
355
|
+
2. removes per-package node_modules left by the npm era and stray package-lock.json files
|
|
356
|
+
3. pnpm install at the workspace root
|
|
357
|
+
4. reports a global grok older than the workspace one
|
|
358
|
+
|
|
359
|
+
--check Report only, change nothing
|
|
360
|
+
--global Also update the global datagrok-tools to the workspace version
|
|
361
|
+
|
|
362
|
+
Examples:
|
|
363
|
+
grok setup After cloning, after a pull that changed dependencies, in a new worktree
|
|
364
|
+
grok setup --check What would change
|
|
365
|
+
`;
|
|
366
|
+
const HELP_TSC = `
|
|
367
|
+
Usage: grok tsc [tsc arguments]
|
|
368
|
+
|
|
369
|
+
Runs the workspace TypeScript compiler (the version @datagrok/build-config pins) with the given arguments,
|
|
370
|
+
so a package never needs its own tsc. After an emitting run in a library, the css/wasm/json assets its
|
|
371
|
+
sources import are mirrored into dist/.
|
|
372
|
+
|
|
373
|
+
Examples:
|
|
374
|
+
grok tsc --noEmit -p tsconfig.json Type-check (what the \`typecheck\` script runs)
|
|
375
|
+
grok tsc -p tsconfig.build.json A library with a custom build config
|
|
376
|
+
`;
|
|
319
377
|
const HELP_BUILD = `
|
|
320
378
|
Usage: grok build
|
|
321
379
|
|
|
322
|
-
|
|
380
|
+
Inside the public/ workspace: build the package in the current directory and everything it depends
|
|
381
|
+
on, in dependency order, through Turborepo (cached: unchanged packages take milliseconds). Never prompts.
|
|
382
|
+
Inside a Turborepo task, in a standalone package, or with --local: build just this package — an rspack
|
|
383
|
+
bundle (src/package.ts -> dist/package.js, function metadata generated, \`grok check --soft\`) for a
|
|
384
|
+
plugin, a TypeScript emit into dist/ for a library.
|
|
323
385
|
|
|
324
386
|
Options:
|
|
325
|
-
[
|
|
326
|
-
|
|
327
|
-
--
|
|
328
|
-
--
|
|
329
|
-
--
|
|
330
|
-
--
|
|
331
|
-
--parallel N Max parallel builds (default:
|
|
332
|
-
--
|
|
387
|
+
[--all] [--affected] [--typecheck] [--filter <turbo filter>] [--parallel N] [--force] [--local] [--skip-check] [-v | --verbose]
|
|
388
|
+
|
|
389
|
+
--all Build the whole workspace
|
|
390
|
+
--affected Build everything the diff against origin/master affects, dependents included
|
|
391
|
+
--typecheck Also run the type-check task
|
|
392
|
+
--filter Extra Turborepo filter (e.g. --filter "@datagrok/chem...")
|
|
393
|
+
--parallel N Max parallel builds (default: 3)
|
|
394
|
+
--force Ignore the build cache
|
|
395
|
+
--local Build only this package, without Turborepo
|
|
396
|
+
--skip-check Local build: skip \`grok check\`
|
|
397
|
+
--verbose Show full build output instead of errors only
|
|
333
398
|
|
|
334
399
|
Examples:
|
|
335
|
-
grok build
|
|
336
|
-
grok build
|
|
337
|
-
grok build
|
|
338
|
-
grok build
|
|
339
|
-
grok build
|
|
400
|
+
grok build This package and its dependencies
|
|
401
|
+
grok build --all Everything
|
|
402
|
+
grok build --affected What my change touches (CI does the same)
|
|
403
|
+
grok build --typecheck Bundle and type-check
|
|
404
|
+
grok build --local Just this package (what the package's build script runs)
|
|
340
405
|
`;
|
|
341
406
|
|
|
342
407
|
// const HELP_MIGRATE = `
|
|
@@ -410,13 +475,16 @@ const help = exports.help = {
|
|
|
410
475
|
'docker-gen': HELP_DOCKER_GEN,
|
|
411
476
|
init: HELP_INIT,
|
|
412
477
|
link: HELP_LINK,
|
|
478
|
+
login: HELP_LOGIN,
|
|
413
479
|
publish: HELP_PUBLISH,
|
|
414
480
|
report: HELP_REPORT,
|
|
415
481
|
run: HELP_RUN,
|
|
416
482
|
test: HELP_TEST,
|
|
483
|
+
tsc: HELP_TSC,
|
|
417
484
|
testall: HELP_TESTALL,
|
|
418
485
|
migrate: HELP_MIGRATE,
|
|
419
486
|
server: _server.HELP_SERVER,
|
|
420
487
|
s: _server.HELP_SERVER,
|
|
488
|
+
setup: HELP_SETUP,
|
|
421
489
|
help: HELP
|
|
422
490
|
};
|
package/bin/commands/link.js
CHANGED
|
@@ -30,11 +30,21 @@ while (_path.default.dirname(dirStep) !== dirStep) {
|
|
|
30
30
|
}
|
|
31
31
|
dirStep = _path.default.dirname(dirStep);
|
|
32
32
|
}
|
|
33
|
+
function isPnpmWorkspace(dir) {
|
|
34
|
+
for (let d = _path.default.resolve(dir);; d = _path.default.dirname(d)) {
|
|
35
|
+
if (_fs.default.existsSync(_path.default.join(d, 'pnpm-workspace.yaml'))) return true;
|
|
36
|
+
if (_path.default.dirname(d) === d) return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
33
39
|
let verbose = false;
|
|
34
40
|
let pathMode = false;
|
|
35
41
|
let devMode = false;
|
|
36
42
|
let unlink = false;
|
|
37
43
|
async function link(args) {
|
|
44
|
+
if (isPnpmWorkspace(curDir)) {
|
|
45
|
+
console.log('This checkout is a pnpm workspace: in-repo dependencies are linked by `pnpm install` ' + '(workspace:^), and `grok link` is not needed. Run `pnpm install` at the repository root.');
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
38
48
|
verbose = args.verbose ?? false;
|
|
39
49
|
devMode = args.dev ?? false;
|
|
40
50
|
pathMode = args.path ?? false;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.login = login;
|
|
8
|
+
var _crypto = _interopRequireDefault(require("crypto"));
|
|
9
|
+
var _http = _interopRequireDefault(require("http"));
|
|
10
|
+
var _os = _interopRequireDefault(require("os"));
|
|
11
|
+
var _child_process = require("child_process");
|
|
12
|
+
var color = _interopRequireWildcard(require("../utils/color-utils"));
|
|
13
|
+
var kp = _interopRequireWildcard(require("../utils/keypair"));
|
|
14
|
+
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); }
|
|
15
|
+
/**
|
|
16
|
+
* `grok login <server>` - generates a keypair, registers its public half on the
|
|
17
|
+
* server, and stores the private half locally. Replaces the developer key: no
|
|
18
|
+
* reusable secret is copied out of the UI, and the key can be given an expiry
|
|
19
|
+
* and revoked on its own.
|
|
20
|
+
*/
|
|
21
|
+
async function login(args) {
|
|
22
|
+
if (args['_'].length > 2) return false;
|
|
23
|
+
const target = (args['_'][1] ?? '').toString();
|
|
24
|
+
const name = args.name ?? `${_os.default.userInfo().username}-${_os.default.hostname()}`;
|
|
25
|
+
const expires = parseExpiry(args.expires);
|
|
26
|
+
if (args.expires && !expires) {
|
|
27
|
+
color.error('--expires takes a number of days or an ISO date (2027-01-31)');
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
let url;
|
|
31
|
+
let alias;
|
|
32
|
+
let keyFile;
|
|
33
|
+
let registered;
|
|
34
|
+
const config = kp.readConfig();
|
|
35
|
+
const {
|
|
36
|
+
publicKey,
|
|
37
|
+
privateKey
|
|
38
|
+
} = kp.generateKeyPair();
|
|
39
|
+
try {
|
|
40
|
+
({
|
|
41
|
+
url,
|
|
42
|
+
alias
|
|
43
|
+
} = resolveTarget(target, config, args.alias));
|
|
44
|
+
url = await color.step(`Finding the Datagrok API at ${url}`, () => resolveApiRoot(url));
|
|
45
|
+
registered = args.code ? await color.step(`Registering "${name}" with the enrollment code`, () => kp.enrollWithCode(url, args.code, publicKey, name, expires)) : await enrollInBrowser(url, publicKey, name, expires);
|
|
46
|
+
if (registered.login == null) throw new Error(registered.comment ?? registered.message ?? 'The server did not accept the key');
|
|
47
|
+
keyFile = await color.step('Storing the private key', async () => {
|
|
48
|
+
const file = kp.savePrivateKey(alias, privateKey);
|
|
49
|
+
config.servers ??= {};
|
|
50
|
+
config.servers[alias] = {
|
|
51
|
+
...(config.servers[alias] ?? {}),
|
|
52
|
+
url,
|
|
53
|
+
key: config.servers[alias]?.key ?? '',
|
|
54
|
+
keyFile: file,
|
|
55
|
+
login: registered.login
|
|
56
|
+
};
|
|
57
|
+
config.default ??= alias;
|
|
58
|
+
kp.writeConfig(config);
|
|
59
|
+
return file;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Proves the round trip before the user walks away, rather than at the next
|
|
63
|
+
// `grok publish`: a key that registered but cannot sign is worse than none.
|
|
64
|
+
await color.step('Signing in with the new key', () => kp.keyLogin(url, privateKey));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
color.error(error.message ?? String(error));
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
color.success(`Logged in to ${url} as ${registered.login}`);
|
|
70
|
+
console.log(` key ${name} (${kp.fingerprint(publicKey)})`);
|
|
71
|
+
console.log(` private key ${keyFile}`);
|
|
72
|
+
console.log(` alias ${alias} - use it as \`grok publish ${alias}\`, \`grok test --host ${alias}\``);
|
|
73
|
+
if (expires) console.log(` expires ${expires}`);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
function resolveTarget(target, config, aliasArg) {
|
|
77
|
+
if (target === '') {
|
|
78
|
+
const alias = aliasArg ?? config.default;
|
|
79
|
+
if (!alias || !config.servers?.[alias]) throw new Error('Which server? Pass a URL or a configured alias: grok login https://dev.datagrok.ai/api');
|
|
80
|
+
return {
|
|
81
|
+
url: config.servers[alias].url,
|
|
82
|
+
alias
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const configured = config.servers?.[target];
|
|
86
|
+
if (configured) return {
|
|
87
|
+
url: configured.url,
|
|
88
|
+
alias: aliasArg ?? target
|
|
89
|
+
};
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = new URL(target);
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error(`"${target}" is neither a URL nor a server in your config`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
url: parsed.href.replace(/\/$/, ''),
|
|
98
|
+
alias: aliasArg ?? defaultAlias(parsed)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The API base for [url]. A stand behind nginx serves it at `<origin>/api`, a bare Datlas at
|
|
104
|
+
* the origin itself, and there is no telling which from the URL alone - so ask, rather than
|
|
105
|
+
* guess and fail at the first call.
|
|
106
|
+
*
|
|
107
|
+
* A 200 is not the answer: nginx serves the single-page app for anything it does not route,
|
|
108
|
+
* so the origin of a real stand answers `/info/server` with the app's HTML. Only a JSON body
|
|
109
|
+
* that names the server counts.
|
|
110
|
+
*/
|
|
111
|
+
async function resolveApiRoot(url) {
|
|
112
|
+
const candidates = /\/api$/.test(url) ? [url] : [`${url}/api`, url];
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(`${candidate}/info/server`);
|
|
116
|
+
if (!response.ok) continue;
|
|
117
|
+
const info = JSON.parse(await response.text());
|
|
118
|
+
if (info?.webRoot != null || info?.Version != null) return candidate;
|
|
119
|
+
} catch {/* not JSON, or unreachable: try the next shape */}
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`${url} does not answer as a Datagrok API (tried ${candidates.join(' and ')})`);
|
|
122
|
+
}
|
|
123
|
+
function defaultAlias(url) {
|
|
124
|
+
const host = url.hostname;
|
|
125
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return 'local';
|
|
126
|
+
return host.split('.')[0];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Days from now, or an ISO date, as the ISO instant the server stores. */
|
|
130
|
+
function parseExpiry(value) {
|
|
131
|
+
if (value == null || value === '') return undefined;
|
|
132
|
+
const days = Number(value);
|
|
133
|
+
if (!isNaN(days) && days > 0) return new Date(Date.now() + days * 86400000).toISOString();
|
|
134
|
+
const date = new Date(String(value));
|
|
135
|
+
return isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Opens the server's enrollment page in a browser and waits for it to call back
|
|
140
|
+
* on a loopback listener. The user authenticates however that stand does -
|
|
141
|
+
* password, SSO, SAML - because the approval happens in the platform UI.
|
|
142
|
+
*/
|
|
143
|
+
async function enrollInBrowser(url, publicKey, name, expires) {
|
|
144
|
+
const origin = new URL(url).origin;
|
|
145
|
+
// The page asks for this before it registers anything. It exists only in this terminal, so a
|
|
146
|
+
// link someone else sent has nothing for the user to type — which is the difference between
|
|
147
|
+
// approving one's own `grok login` and handing an attacker a key to one's account.
|
|
148
|
+
const verify = _crypto.default.randomBytes(4).toString('hex').toUpperCase().slice(0, 6);
|
|
149
|
+
let started;
|
|
150
|
+
const listening = new Promise(resolve => started = resolve);
|
|
151
|
+
const approved = new Promise((resolve, reject) => {
|
|
152
|
+
const timeout = setTimeout(() => {
|
|
153
|
+
server.close();
|
|
154
|
+
reject(new Error('Timed out waiting for the browser. Use `grok login <server> --code <code>` ' + 'with a code from your profile page instead.'));
|
|
155
|
+
}, 5 * 60 * 1000);
|
|
156
|
+
const server = _http.default.createServer((req, res) => {
|
|
157
|
+
const params = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams;
|
|
158
|
+
const status = params.get('status');
|
|
159
|
+
res.writeHead(200, {
|
|
160
|
+
'content-type': 'text/html; charset=utf-8'
|
|
161
|
+
});
|
|
162
|
+
res.end(status === 'ok' ? '<h3>Key registered. You can close this tab and return to the terminal.</h3>' : `<h3>Key registration was cancelled.</h3><p>${escapeHtml(params.get('message') ?? '')}</p>`);
|
|
163
|
+
clearTimeout(timeout);
|
|
164
|
+
server.close();
|
|
165
|
+
if (status === 'ok') resolve({
|
|
166
|
+
login: params.get('login'),
|
|
167
|
+
fingerprint: params.get('fingerprint')
|
|
168
|
+
});else reject(new Error(params.get('message') ?? 'Key registration was cancelled in the browser'));
|
|
169
|
+
});
|
|
170
|
+
server.listen(0, '127.0.0.1', () => {
|
|
171
|
+
const port = server.address().port;
|
|
172
|
+
const enrollUrl = `${origin}/enroll-key?` + new URLSearchParams({
|
|
173
|
+
pk: JSON.stringify(publicKey),
|
|
174
|
+
name,
|
|
175
|
+
...(expires ? {
|
|
176
|
+
expires
|
|
177
|
+
} : {}),
|
|
178
|
+
verify,
|
|
179
|
+
cb: `http://127.0.0.1:${port}`
|
|
180
|
+
}).toString();
|
|
181
|
+
console.log(`\n Verification code: ${verify}`);
|
|
182
|
+
console.log(` Type it on the page that opens. If the browser does not open, visit:`);
|
|
183
|
+
console.log(` ${enrollUrl}\n`);
|
|
184
|
+
openBrowser(enrollUrl);
|
|
185
|
+
started();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// The prints above land before the spinner starts, so the code stays readable on screen.
|
|
190
|
+
await listening;
|
|
191
|
+
return await color.step(`Waiting for approval in the browser at ${origin}`, () => approved);
|
|
192
|
+
}
|
|
193
|
+
function escapeHtml(s) {
|
|
194
|
+
return s.replace(/[&<>"]/g, c => ({
|
|
195
|
+
'&': '&',
|
|
196
|
+
'<': '<',
|
|
197
|
+
'>': '>',
|
|
198
|
+
'"': '"'
|
|
199
|
+
})[c]);
|
|
200
|
+
}
|
|
201
|
+
function openBrowser(url) {
|
|
202
|
+
try {
|
|
203
|
+
// cmd splits an unquoted argument at `&`, and the enrollment URL is all query parameters;
|
|
204
|
+
// rundll32 takes the whole thing and hands it to the default browser.
|
|
205
|
+
const [command, args] = process.platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]] : process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
|
|
206
|
+
(0, _child_process.spawn)(command, args, {
|
|
207
|
+
detached: true,
|
|
208
|
+
stdio: 'ignore'
|
|
209
|
+
}).unref();
|
|
210
|
+
} catch {
|
|
211
|
+
// The URL is printed above; a headless box just uses that.
|
|
212
|
+
}
|
|
213
|
+
}
|
package/bin/commands/publish.js
CHANGED
|
@@ -21,6 +21,7 @@ var _check = require("./check");
|
|
|
21
21
|
var _pythonCeleryGen = require("../utils/python-celery-gen");
|
|
22
22
|
var _queueWorkerGen = require("../utils/queue-worker-gen");
|
|
23
23
|
var _devKey = require("../utils/dev-key");
|
|
24
|
+
var keypair = _interopRequireWildcard(require("../utils/keypair"));
|
|
24
25
|
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); }
|
|
25
26
|
// @ts-ignore
|
|
26
27
|
|
|
@@ -242,18 +243,21 @@ function listRecursive(basePath, rel) {
|
|
|
242
243
|
return results;
|
|
243
244
|
}
|
|
244
245
|
async function getUserLogin(host, devKey) {
|
|
245
|
-
let
|
|
246
|
+
let token;
|
|
246
247
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
token = await (0, _devKey.keypairToken)(host, devKey);
|
|
249
|
+
if (token == null) {
|
|
250
|
+
const loginResp = await (0, _devKey.devKeyFetch)(`${host}/users/login/dev`, `${host}/users/login/dev/${devKey}`, devKey, {
|
|
251
|
+
method: 'POST'
|
|
252
|
+
});
|
|
253
|
+
if (loginResp.status !== 200) return null;
|
|
254
|
+
token = (await loginResp.json()).token;
|
|
255
|
+
}
|
|
250
256
|
} catch (e) {
|
|
251
257
|
color.warn(`Cannot reach server ${host}: ${e.message || e}`);
|
|
252
258
|
return null;
|
|
253
259
|
}
|
|
254
|
-
if (
|
|
255
|
-
const loginData = await loginResp.json();
|
|
256
|
-
const token = loginData.token;
|
|
260
|
+
if (token == null) return null;
|
|
257
261
|
try {
|
|
258
262
|
const userResp = await (0, _nodeFetch.default)(`${host}/users/current`, {
|
|
259
263
|
headers: {
|
|
@@ -489,7 +493,7 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
|
|
|
489
493
|
const url = `${host}/packages/dev/${packageName}`;
|
|
490
494
|
const legacyUrl = `${host}/packages/dev/${devKey}/${packageName}`;
|
|
491
495
|
try {
|
|
492
|
-
const checkResp = await (0, _devKey.devKeyFetch)(`${url}/timestamps`, `${legacyUrl}/timestamps`, devKey);
|
|
496
|
+
const checkResp = await (0, _devKey.devKeyFetch)(`${url}/timestamps`, `${legacyUrl}/timestamps`, devKey, {}, host);
|
|
493
497
|
const checkData = await checkResp.json();
|
|
494
498
|
if (checkData['#type'] === 'ApiError') {
|
|
495
499
|
color.error(checkData.message);
|
|
@@ -522,7 +526,10 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
|
|
|
522
526
|
includeEmpty: false,
|
|
523
527
|
follow: true
|
|
524
528
|
});
|
|
525
|
-
|
|
529
|
+
|
|
530
|
+
// A bundled package ships dist/package.js: built by webpack (legacy) or by `grok build`
|
|
531
|
+
// (rspack via @datagrok/build-config, which needs no config file at all).
|
|
532
|
+
const isWebpack = _fs.default.existsSync('webpack.config.js') || _fs.default.existsSync('rspack.config.js') || _fs.default.existsSync('dist/package.js') || _fs.default.existsSync(_path.default.join(curDir, 'src', 'package.ts'));
|
|
526
533
|
if (!rebuild && isWebpack) {
|
|
527
534
|
if (_fs.default.existsSync('dist/package.js')) {
|
|
528
535
|
const distFiles = await (0, _ignoreWalk.default)({
|
|
@@ -546,12 +553,6 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
|
|
|
546
553
|
const json = JSON.parse(_fs.default.readFileSync(packageFilePath, {
|
|
547
554
|
encoding: 'utf-8'
|
|
548
555
|
}));
|
|
549
|
-
if (isWebpack) {
|
|
550
|
-
const webpackConfigPath = _path.default.join(curDir, 'webpack.config.js');
|
|
551
|
-
const content = _fs.default.readFileSync(webpackConfigPath, {
|
|
552
|
-
encoding: 'utf-8'
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
556
|
const funcFiles = jsTsFiles.filter(f => packageFiles.includes(f));
|
|
556
557
|
color.log(`Checks finished in ${Date.now() - checkStart} ms`);
|
|
557
558
|
const reg = new RegExp(/\${(\w*)}/g);
|
|
@@ -624,7 +625,7 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
|
|
|
624
625
|
const body = await (0, _devKey.devKeyFetch)(url + query, legacyUrl + query, devKey, {
|
|
625
626
|
method: 'POST',
|
|
626
627
|
body: zipBuffer
|
|
627
|
-
});
|
|
628
|
+
}, host);
|
|
628
629
|
const log = JSON.parse(await body.text());
|
|
629
630
|
if (log != undefined) {
|
|
630
631
|
if (log['#type'] === 'ApiError') {
|
|
@@ -688,12 +689,14 @@ async function publish(args) {
|
|
|
688
689
|
}
|
|
689
690
|
async function publishPackage(args) {
|
|
690
691
|
const nArgs = args['_'].length;
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
692
|
+
|
|
693
|
+
// A bundled package is built locally before upload unless --skip-build. Inside the pnpm
|
|
694
|
+
// workspace the install already happened at the root; standalone packages install first.
|
|
695
|
+
if (!args.link && !args['skip-build'] && !args.rebuild && _fs.default.existsSync(_path.default.join(curDir, 'src'))) {
|
|
696
|
+
color.log('Building');
|
|
697
|
+
const workspace = utils.isPnpmWorkspace(curDir);
|
|
698
|
+
if (!workspace && !_fs.default.existsSync(_path.default.join(curDir, 'node_modules'))) await utils.runScript('npm install', curDir, false);
|
|
699
|
+
await utils.runScript(workspace ? 'pnpm run build' : 'npm run build', curDir, false);
|
|
697
700
|
}
|
|
698
701
|
if (args.debug && args.release) {
|
|
699
702
|
color.error('Incompatible options: --debug and --release');
|
|
@@ -728,7 +731,7 @@ async function publishPackage(args) {
|
|
|
728
731
|
|
|
729
732
|
// Update the developer key
|
|
730
733
|
if (args.key) key = args.key;
|
|
731
|
-
if (key
|
|
734
|
+
if (!key && !keypair.keypairFor(url)) return color.warn(`No credentials for ${url}. Run \`grok login ${host}\`, ` + 'or pass a developer key with `--key` (deprecated).');
|
|
732
735
|
|
|
733
736
|
// Get the package name
|
|
734
737
|
if (!_fs.default.existsSync(packDir)) return color.error('`package.json` doesn\'t exist');
|
package/bin/commands/server.js
CHANGED
|
@@ -27,7 +27,7 @@ const ENTITY_TYPES = {
|
|
|
27
27
|
reports: 'UserReport'
|
|
28
28
|
};
|
|
29
29
|
const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files', 'tables'];
|
|
30
|
-
const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle'];
|
|
30
|
+
const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token'];
|
|
31
31
|
const VERBS = ['list', 'count', 'get', 'delete'];
|
|
32
32
|
async function server(argv) {
|
|
33
33
|
const args = argv['_'].slice(1);
|
|
@@ -61,6 +61,12 @@ async function server(argv) {
|
|
|
61
61
|
if (['pull', 'push', 'migrate', 'diff', 'bundle'].includes(entity)) return await (0, _serverMigrate.handleMigrate)(dapi, entity, [verb, ...rest].filter(Boolean), argv, output);
|
|
62
62
|
if (entity === 'domains') return await (0, _serverDomains.handleDomains)(dapi, verb, rest, argv, output);
|
|
63
63
|
if (entity === 'batch') return await handleBatch(dapi, argv, verb, rest, output);
|
|
64
|
+
// Shell scripts that used to curl /users/login/dev get a token the same way
|
|
65
|
+
// every other command does, whatever credential the config holds.
|
|
66
|
+
if (entity === 'token') {
|
|
67
|
+
console.log(client.token);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
64
70
|
if (entity === 'raw') return await handleRaw(dapi, verb, rest, argv, output);
|
|
65
71
|
if (entity === 'describe') return await handleDescribe(dapi, verb ?? rest[0], output);
|
|
66
72
|
if (entity === 'healthcheck') return await handleHealthcheck(dapi, argv, output);
|