@andrian.yablonskyy/thub-agent 1.0.7 → 1.0.9
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/package.json +1 -1
- package/src/cli.js +60 -4
- package/src/self-update.js +77 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -16,21 +16,41 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const { Command } = require('commander'),
|
|
19
|
-
{ ApiClient, EXIT_CODES, ACTIVE_JOB_STATES, exitCodeForJobState } =
|
|
19
|
+
{ ApiClient, EXIT_CODES, ACTIVE_JOB_STATES, exitCodeForJobState, PACKAGES, fetchLatestVersion, isNewer, isValidVersion } =
|
|
20
|
+
require('@andrian.yablonskyy/thub-common'),
|
|
20
21
|
{ resolveConnection, resolveGroup, resolveUser, writeConfigFile, readConfigFile } = require('./config'),
|
|
21
22
|
{ parseDurationSec } = require('./duration'),
|
|
22
|
-
{ followJob } = require('./streaming')
|
|
23
|
+
{ followJob } = require('./streaming'),
|
|
24
|
+
{ applyRequestedUpdate, installAgent, version } = require('./self-update');
|
|
23
25
|
|
|
24
26
|
const program = new Command();
|
|
25
27
|
program
|
|
26
28
|
.name('thub')
|
|
27
29
|
.description('TestHub Agent — submit test jobs and follow them, from CI or your laptop')
|
|
28
30
|
.option('--url <url>', 'Coordinator URL (overrides THUB_URL / config file)')
|
|
29
|
-
.option('--token <token>', 'Agent token (overrides THUB_TOKEN / config file)')
|
|
31
|
+
.option('--token <token>', 'Agent token (overrides THUB_TOKEN / config file)')
|
|
32
|
+
.version(version);
|
|
33
|
+
|
|
34
|
+
// An admin-requested self-update (README §10.2) is applied at the start of
|
|
35
|
+
// the next run of any command that talks to the Coordinator.
|
|
36
|
+
const NO_UPDATE_CHECK = new Set(['config', 'set', 'check-update', 'self-update']);
|
|
37
|
+
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
38
|
+
if (NO_UPDATE_CHECK.has(actionCommand.name())){
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let api;
|
|
42
|
+
try {
|
|
43
|
+
api = client();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return; // not configured — the command itself reports that
|
|
47
|
+
}
|
|
48
|
+
await applyRequestedUpdate(api);
|
|
49
|
+
});
|
|
30
50
|
|
|
31
51
|
function client(){
|
|
32
52
|
const { url, token } = resolveConnection(program.opts());
|
|
33
|
-
return new ApiClient({ baseUrl: url, token });
|
|
53
|
+
return new ApiClient({ baseUrl: url, token, userAgent: `thub-agent/${version}` });
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
function fail(err){
|
|
@@ -231,6 +251,42 @@ config
|
|
|
231
251
|
console.log(`Saved ${key} to config`);
|
|
232
252
|
});
|
|
233
253
|
|
|
254
|
+
program
|
|
255
|
+
.command('check-update')
|
|
256
|
+
.description('Compare this Agent with the latest published version')
|
|
257
|
+
.action(async () => {
|
|
258
|
+
try {
|
|
259
|
+
const latest = await fetchLatestVersion(PACKAGES.agent);
|
|
260
|
+
console.log(`Installed: v${version} Latest: v${latest}`);
|
|
261
|
+
console.log(isNewer(latest, version) ? 'Update available: thub self-update' : 'Up to date.');
|
|
262
|
+
}
|
|
263
|
+
catch (err){
|
|
264
|
+
fail(err);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
program
|
|
269
|
+
.command('self-update')
|
|
270
|
+
.description('Update this Agent to the latest (or a given) version with npm i -g')
|
|
271
|
+
.option('--to <x.y.z>', 'Install this version instead of the latest')
|
|
272
|
+
.action(async (opts) => {
|
|
273
|
+
try {
|
|
274
|
+
const target = opts.to || await fetchLatestVersion(PACKAGES.agent);
|
|
275
|
+
if (!isValidVersion(target)){
|
|
276
|
+
throw new Error(`Invalid version "${target}"`);
|
|
277
|
+
}
|
|
278
|
+
if (!opts.to && !isNewer(target, version)){
|
|
279
|
+
console.log(`Already on v${version}.`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
console.log(`Updating v${version} -> v${target}`);
|
|
283
|
+
process.exit(installAgent(target) ? 0 : 1);
|
|
284
|
+
}
|
|
285
|
+
catch (err){
|
|
286
|
+
fail(err);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
234
290
|
function collectRepeatable(value, previous){
|
|
235
291
|
return [...previous, value];
|
|
236
292
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/agent/src/self-update.js
|
|
3
|
+
* @description Agent self-update: an admin-requested update applied at the start of the next run, plus the
|
|
4
|
+
* manual `thub check-update` / `thub self-update` commands (README §10.2)
|
|
5
|
+
*
|
|
6
|
+
* @author Andrian Yablonskyy
|
|
7
|
+
* @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
|
|
8
|
+
*
|
|
9
|
+
* This file is part of TestHub and is proprietary and confidential.
|
|
10
|
+
* Unauthorized copying, modification, distribution, or use of this file,
|
|
11
|
+
* via any medium, is strictly prohibited without prior written permission
|
|
12
|
+
* from AdSystem.PRO.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { spawnSync } = require('node:child_process'),
|
|
18
|
+
{ PACKAGES, isNewer, npmBin, npmInstallGlobal } = require('@andrian.yablonskyy/thub-common'),
|
|
19
|
+
{ version } = require('../package.json');
|
|
20
|
+
|
|
21
|
+
// npm's output goes to stderr so a command's own stdout (e.g. JSON a CI
|
|
22
|
+
// script parses) stays clean.
|
|
23
|
+
const NPM_STDIO = ['inherit', 2, 2],
|
|
24
|
+
CHECK_TIMEOUT_MS = 5000;
|
|
25
|
+
|
|
26
|
+
// Tries a plain `npm i -g` first (nvm, a user-owned prefix, or root in
|
|
27
|
+
// CI); if that fails on an interactive terminal, retries through sudo
|
|
28
|
+
// for a system-wide install. Never throws — returns whether it worked.
|
|
29
|
+
function installAgent(target){
|
|
30
|
+
try {
|
|
31
|
+
if (npmInstallGlobal(PACKAGES.agent, target, { stdio: NPM_STDIO }) === 0){
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (process.getuid?.() !== 0 && process.stdin.isTTY){
|
|
35
|
+
console.error('thub: retrying with sudo');
|
|
36
|
+
return spawnSync('sudo', [npmBin(), 'i', '-g', `${PACKAGES.agent}@${target}`], { stdio: NPM_STDIO }).status === 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (err){
|
|
40
|
+
console.error(`thub: self-update failed: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Runs before every command that talks to the Coordinator. If an admin
|
|
46
|
+
// requested an update (Agents page) and it's newer than this Agent,
|
|
47
|
+
// installs it and re-runs the same command line on the new version. Any
|
|
48
|
+
// failure — Coordinator unreachable, no permission to install — just
|
|
49
|
+
// falls through to running the command on the current version.
|
|
50
|
+
async function applyRequestedUpdate(api){
|
|
51
|
+
if (process.env.THUB_NO_SELF_UPDATE || process.env.THUB_SELF_UPDATED){
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let updateTo;
|
|
55
|
+
try {
|
|
56
|
+
({ updateTo } = await api.get('/agents/me/update', { signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) }));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return; // older Coordinator, or unreachable — the command reports that itself
|
|
60
|
+
}
|
|
61
|
+
if (!isNewer(updateTo, version)){
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.error(`thub: updating v${version} -> v${updateTo} (requested by the Coordinator)`);
|
|
66
|
+
if (!installAgent(updateTo)){
|
|
67
|
+
console.error(`thub: update failed — continuing on v${version}. To update by hand: sudo npm i -g ${PACKAGES.agent}@${updateTo}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const rerun = spawnSync(process.execPath, process.argv.slice(1), {
|
|
71
|
+
stdio: 'inherit',
|
|
72
|
+
env: { ...process.env, THUB_SELF_UPDATED: '1' }
|
|
73
|
+
});
|
|
74
|
+
process.exit(rerun.status ?? 1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { applyRequestedUpdate, installAgent, version };
|