@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrian.yablonskyy/thub-agent",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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"
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 } = require('@andrian.yablonskyy/thub-common'),
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 };