@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 +6 -3
- package/package.json +2 -2
- package/src/cli.js +74 -10
- package/src/config.js +11 -5
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`, `
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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('--
|
|
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('<
|
|
378
|
+
.argument('<name>', 'url | key | group | user')
|
|
378
379
|
.argument('<value>')
|
|
379
|
-
.action((
|
|
380
|
-
|
|
381
|
-
|
|
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, [
|
|
386
|
-
console.log(`Saved ${
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
54
|
-
'or THUB_URL /
|
|
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
|