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.
@@ -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
- build Build a package or multiple packages
17
- check Check package content (function signatures, etgc.)
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
- Build a package in the current directory, or recursively build multiple packages.
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
- [-r | --recursive] [-s | --silent] [--filter] [--no-incremental] [--parallel N] [-v | --verbose]
326
-
327
- --recursive Build all packages in the current directory
328
- --silent Skip confirmation prompt (for recursive builds)
329
- --filter Filter packages by package.json fields (e.g. --filter "category:Cheminformatics")
330
- --no-incremental Run a full build instead of the default incremental build
331
- --parallel N Max parallel builds (default: 4)
332
- --verbose Print detailed output
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 Build the current package
336
- grok build -r Build all packages in the current directory
337
- grok build -r -s Build all packages without confirmation
338
- grok build -r --filter "category:Cheminformatics" Build only matching packages
339
- grok build -r --parallel 8 Build with 8 parallel jobs
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
  };
@@ -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
+ '&': '&amp;',
196
+ '<': '&lt;',
197
+ '>': '&gt;',
198
+ '"': '&quot;'
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
+ }
@@ -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 loginResp;
246
+ let token;
246
247
  try {
247
- loginResp = await (0, _devKey.devKeyFetch)(`${host}/users/login/dev`, `${host}/users/login/dev/${devKey}`, devKey, {
248
- method: 'POST'
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 (loginResp.status !== 200) return null;
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
- const isWebpack = _fs.default.existsSync('webpack.config.js');
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
- if (!args.link) {
692
- if (args.build || args.rebuild) {
693
- color.log('Building');
694
- await utils.runScript('npm install', curDir, false);
695
- await utils.runScript('npm run build', curDir, false);
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 === '') return color.warn('Please provide the key with `--key` option or add it by running `grok config`');
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');
@@ -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);