@andrian.yablonskyy/thub-agent 1.0.34 → 1.1.0

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/README.md CHANGED
@@ -14,13 +14,16 @@ npx -y @andrian.yablonskyy/thub-agent run --type sw --download-file "$IMAGE_URL"
14
14
 
15
15
  ## Configuration
16
16
 
17
- Read from flags, then environment (`THUB_URL`, `THUB_TOKEN`, `THUB_GROUP`, `THUB_USER`), then `~/.config/thub/agent.json`, then a bundled default. `url`/`token` are required by the time a command actually talks to the Coordinator; `group`/`user` are optional everywhere.
17
+ Read from flags (`--url`, `--key`), then environment (`THUB_URL`, `THUB_KEY`, `THUB_GROUP`, `THUB_USER`; the old names `--token`/`THUB_TOKEN` still work), then `~/.config/thub/agent.json`, then a bundled default. `url`/`token` are required by the time a command actually talks to the Coordinator; `group`/`user` are optional everywhere.
18
18
 
19
19
  `npm install -g` creates `~/.config/thub/agent.json` for you (blank `url`/`token`, so nothing works until you set them) if it doesn't already exist — a re-install never overwrites it. Fill it in with `thub config set`:
20
20
 
21
21
  ```bash
22
22
  thub config set url https://thub.example.com
23
- thub config set token agt_...
23
+ thub config set key thk_... # your access key: from an admin (Users), or your dashboard profile
24
+ thub whoami # jane (user) <jane@example.com>
25
+ thub key show # its last characters, created, last used
26
+ thub key rotate # a new key; the old one stops at once (saved here if it came from here)
24
27
  thub config set group 548ae4ae-... # optional default --group
25
28
  thub config set user "Your Name" # optional default --user
26
29
  ```
@@ -249,7 +252,7 @@ test-sw:
249
252
  runs-on: ubuntu-latest
250
253
  env:
251
254
  THUB_URL: https://thub.example.com
252
- THUB_TOKEN: ${{ secrets.THUB_AGENT_TOKEN }}
255
+ THUB_KEY: ${{ secrets.THUB_CI_TOKEN }} # a CI token (dashboard → CI tokens)
253
256
  steps:
254
257
  - run: |
255
258
  npx -y @andrian.yablonskyy/thub-agent run --type sw \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrian.yablonskyy/thub-agent",
3
- "version": "1.0.34",
3
+ "version": "1.1.0",
4
4
  "description": "TestHub Agent CLI — the single entry point CI/CD and developers use to submit and follow test jobs",
5
5
  "bin": {
6
6
  "thub": "./src/cli.js"
@@ -12,7 +12,7 @@
12
12
  "lint:fix": "eslint . --fix"
13
13
  },
14
14
  "dependencies": {
15
- "@andrian.yablonskyy/thub-common": "^1.0.33",
15
+ "@andrian.yablonskyy/thub-common": "^1.1.0",
16
16
  "commander": "^13.1.0"
17
17
  },
18
18
  "devDependencies": {
package/src/cli.js CHANGED
@@ -15,13 +15,13 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { Command } = require('commander'),
18
+ const { Command, Option } = require('commander'),
19
19
  {
20
20
  ApiClient, EXIT_CODES, ACTIVE_JOB_STATES, exitCodeForJobState, PACKAGES, fetchLatestVersion, isNewer, isValidVersion, formatDateTime,
21
21
  splitArgs,
22
22
  parseEnvList
23
23
  } = require('@andrian.yablonskyy/thub-common'),
24
- { resolveConnection, resolveGroup, resolveUser, writeConfigFile, readConfigFile } = require('./config'),
24
+ { resolveConnection, resolveGroup, resolveUser, writeConfigFile, readConfigFile, CONFIG_PATH } = require('./config'),
25
25
  { parseDurationSec } = require('./duration'),
26
26
  { followJob } = require('./streaming'),
27
27
  { applyRequestedUpdate, installAgent, version } = require('./self-update');
@@ -31,7 +31,8 @@ program
31
31
  .name('thub')
32
32
  .description('TestHub Agent — submit test jobs and follow them, from CI or your laptop')
33
33
  .option('--url <url>', 'Coordinator URL (overrides THUB_URL / config file)')
34
- .option('--token <token>', 'Agent token (overrides THUB_TOKEN / config file)')
34
+ .option('--key <key>', 'Your access key (overrides THUB_KEY / config file)')
35
+ .addOption(new Option('--token <token>', 'Old name of --key').hideHelp())
35
36
  .version(version);
36
37
 
37
38
  // An admin-requested self-update (README §10.2) is applied at the start of
@@ -374,16 +375,79 @@ function formatBytes(bytes){
374
375
  const config = program.command('config').description('Manage local Agent configuration');
375
376
  config
376
377
  .command('set')
377
- .argument('<key>', 'url | token | group | user')
378
+ .argument('<name>', 'url | key | group | user')
378
379
  .argument('<value>')
379
- .action((key, value) => {
380
- if (!['url', 'token', 'group', 'user'].includes(key)){
381
- console.error('Error: key must be "url", "token", "group", or "user"');
380
+ .action((name, value) => {
381
+ // `token` is the old name of `key`.
382
+ const setting = name === 'token' ? 'key' : name;
383
+ if (!['url', 'key', 'group', 'user'].includes(setting)){
384
+ console.error('Error: the setting must be "url", "key", "group", or "user"');
382
385
  process.exit(EXIT_CODES.USAGE);
383
386
  }
384
- const current = readConfigFile();
385
- writeConfigFile({ ...current, [key]: value });
386
- console.log(`Saved ${key} to config`);
387
+ const { token: _old, ...current } = readConfigFile();
388
+ writeConfigFile({ ...(setting === 'key' ? current : { ...current, ...(_old ? { token: _old } : {}) }), [setting]: value });
389
+ console.log(`Saved ${setting} to config`);
390
+ });
391
+
392
+ // Who this access key belongs to (§10.3).
393
+ function describeMe(me){
394
+ const role = me.user?.role ? me.user.role.charAt(0).toUpperCase() + me.user.role.slice(1) : '';
395
+ return me.user ? `${me.user.username} (${role})${me.user.email ? ` <${me.user.email}>` : ''}` : `CI token "${me.name}"`;
396
+ }
397
+
398
+ program
399
+ .command('whoami')
400
+ .description('Show whose access key this Agent uses')
401
+ .action(async () => {
402
+ try {
403
+ console.log(describeMe(await client().get('/me')));
404
+ }
405
+ catch (err){
406
+ fail(err);
407
+ }
408
+ });
409
+
410
+ const keyCommand = program.command('key').description('Your access key');
411
+ keyCommand
412
+ .command('show')
413
+ .description('Show your access key\'s details (never the key itself — only its last characters)')
414
+ .action(async () => {
415
+ try {
416
+ const me = await client().get('/me'),
417
+ { keySource } = resolveConnection(program.opts());
418
+ console.log(`User: ${describeMe(me)}`);
419
+ if (me.key){
420
+ console.log(`Key: …${me.key.hint || '????'} (from ${keySource === 'file' ? CONFIG_PATH : keySource === 'flag' ? '--key' : keySource})`);
421
+ console.log(`Created: ${formatDateTime(me.key.createdAt)}`);
422
+ console.log(`Last used: ${me.key.lastUsedAt ? formatDateTime(me.key.lastUsedAt) : 'never'}`);
423
+ }
424
+ }
425
+ catch (err){
426
+ fail(err);
427
+ }
428
+ });
429
+ keyCommand
430
+ .command('rotate')
431
+ .description('Replace your access key: the current one stops working at once')
432
+ .action(async () => {
433
+ try {
434
+ const { keySource } = resolveConnection(program.opts()),
435
+ { key, username } = await client().post('/me/key/rotate');
436
+ if (keySource === 'file'){
437
+ const { token: _old, ...current } = readConfigFile();
438
+ writeConfigFile({ ...current, key });
439
+ console.log(`New access key for ${username} saved to ${CONFIG_PATH} — the old one has stopped working.`);
440
+ }
441
+ else {
442
+ console.log(`New access key for ${username} — the old one has stopped working. Shown only now:\n\n ${key}\n`);
443
+ console.log(keySource === 'flag'
444
+ ? 'Use it with --key from now on (or save it: thub config set key <key>).'
445
+ : `Update ${keySource} (e.g. your CI secret) with it, or save it: thub config set key <key>.`);
446
+ }
447
+ }
448
+ catch (err){
449
+ fail(err);
450
+ }
387
451
  });
388
452
 
389
453
  program
package/src/config.js CHANGED
@@ -18,7 +18,8 @@ const fs = require('node:fs'),
18
18
  path = require('node:path');
19
19
 
20
20
  // §7: "Configuration is read from flags, then environment (THUB_URL,
21
- // THUB_TOKEN), then ~/.config/thub/agent.json."
21
+ // THUB_KEY), then ~/.config/thub/agent.json." The access key (§10.3) used to
22
+ // be called a token: --token, THUB_TOKEN and a saved `token` still work.
22
23
  const CONFIG_PATH = path.join(os.homedir(), '.config', 'thub', 'agent.json'),
23
24
 
24
25
  // Bundled with the package as a last-resort default, below the user's own
@@ -44,19 +45,24 @@ function writeConfigFile(config){
44
45
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
45
46
  }
46
47
 
48
+ // `keySource` says where the key came from — `thub key rotate` saves the
49
+ // new one only where it can (the config file), and says what to update
50
+ // otherwise.
47
51
  function resolveConnection(flags = {}){
48
52
  const file = { ...readJsonFile(PACKAGE_DEFAULT_CONFIG_PATH), ...readConfigFile() },
49
53
  url = flags.url || process.env.THUB_URL || file.url,
50
- token = flags.token || process.env.THUB_TOKEN || file.token;
54
+ [token, keySource] = flags.key || flags.token ? [flags.key || flags.token, 'flag']
55
+ : process.env.THUB_KEY || process.env.THUB_TOKEN ? [process.env.THUB_KEY || process.env.THUB_TOKEN, process.env.THUB_KEY ? 'THUB_KEY' : 'THUB_TOKEN']
56
+ : file.key || file.token ? [file.key || file.token, 'file'] : [null, null];
51
57
  if (!url || !token){
52
58
  const err = new Error(
53
- 'Missing coordinator URL or token. Set with `thub config set url <url>` / `thub config set token <token>`, ' +
54
- 'or THUB_URL / THUB_TOKEN, or --url / --token.'
59
+ 'Missing Coordinator URL or access key. Set them with `thub config set url <url>` and `thub config set key <key>`, ' +
60
+ 'or THUB_URL / THUB_KEY, or --url / --key. An admin gives you your key (Users), or create it on your dashboard profile.'
55
61
  );
56
62
  err.status = 4;
57
63
  throw err;
58
64
  }
59
- return { url, token };
65
+ return { url, token, keySource };
60
66
  }
61
67
 
62
68
  // Optional default for `--group` (§4.3/§7.1) — same flags > env > file