@andrian.yablonskyy/thub-common 1.0.8 → 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/README.md CHANGED
@@ -56,6 +56,10 @@ await client.streamEvents(`/jobs/${job.jobId}/logs/stream`, {
56
56
 
57
57
  `request()`/`get()`/`post()` handle JSON bodies, query strings and non-2xx errors (throwing with `.status`/`.body` set); `streamEvents()` reconnects the caller is expected to drive (it just parses one connection's worth of events and resolves when the stream ends) and honors `Last-Event-ID` for resuming after a drop.
58
58
 
59
+ ### Version checks and self-update helpers
60
+
61
+ `PACKAGES` (the Coordinator/Agent/Client npm names), `fetchLatestVersion(pkg, { registry })` (the `latest` dist-tag from the npm registry), `compareVersions`/`isNewer`/`isValidVersion` (strict `X.Y.Z[-pre]`), `npmBin()` (the npm next to the running node) and `npmInstallGlobal(pkg, version)` (only for the three TestHub packages, only a valid version).
62
+
59
63
  ## Development
60
64
 
61
65
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrian.yablonskyy/thub-common",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Shared job-spec schema, state enums and a thin API client used by the Agent and Client",
5
5
  "main": "src/index.js",
6
6
  "files": [
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file packages/shared/src/index.js
3
- * @description Public entry point re-exporting shared states, schema, and API client
3
+ * @description Public entry point re-exporting shared states, schema, API client and update helpers
4
4
  *
5
5
  * @author Andrian Yablonskyy
6
6
  * @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
@@ -16,5 +16,6 @@
16
16
  module.exports = {
17
17
  ...require('./states'),
18
18
  ...require('./validate-job-spec'),
19
+ ...require('./updates'),
19
20
  ApiClient: require('./api-client').ApiClient
20
21
  };
package/src/updates.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @file packages/shared/src/updates.js
3
+ * @description New-version checks and `npm i -g` self-update helpers shared by the Coordinator, Agent and Client
4
+ *
5
+ * @author Andrian Yablonskyy
6
+ * @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
7
+ *
8
+ * This file is part of TestHub and is proprietary and confidential.
9
+ * Unauthorized copying, modification, distribution, or use of this file,
10
+ * via any medium, is strictly prohibited without prior written permission
11
+ * from AdSystem.PRO.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('node:fs'),
17
+ path = require('node:path'),
18
+ { spawnSync } = require('node:child_process');
19
+
20
+ const PACKAGES = {
21
+ coordinator: '@andrian.yablonskyy/thub-coordinator',
22
+ agent: '@andrian.yablonskyy/thub-agent',
23
+ client: '@andrian.yablonskyy/thub-client'
24
+ },
25
+
26
+ // Strict on purpose: a version string ends up on an `npm i -g` command
27
+ // line (as root, for the Client), so nothing but a plain semver passes.
28
+ VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,32})?$/,
29
+
30
+ DEFAULT_REGISTRY = 'https://registry.npmjs.org';
31
+
32
+ function isValidVersion(v){
33
+ return typeof v === 'string' && VERSION_RE.test(v);
34
+ }
35
+
36
+ // Semver precedence for x.y.z[-pre]: numeric core first, then a release
37
+ // sorts after any of its prereleases. Returns <0, 0 or >0.
38
+ function compareVersions(a, b){
39
+ const [coreA, preA] = String(a).split('-', 2),
40
+ [coreB, preB] = String(b).split('-', 2),
41
+ partsA = coreA.split('.').map(Number),
42
+ partsB = coreB.split('.').map(Number);
43
+ for (let i = 0; i < 3; i++){
44
+ const diff = (partsA[i] || 0) - (partsB[i] || 0);
45
+ if (diff){
46
+ return diff;
47
+ }
48
+ }
49
+ if (preA === preB){
50
+ return 0;
51
+ }
52
+ if (!preA){
53
+ return 1;
54
+ }
55
+ if (!preB){
56
+ return -1;
57
+ }
58
+ return preA < preB ? -1 : 1;
59
+ }
60
+
61
+ function isNewer(candidate, current){
62
+ return isValidVersion(candidate) && isValidVersion(current) && compareVersions(candidate, current) > 0;
63
+ }
64
+
65
+ // The registry npm itself would use (`npm_config_registry` is set when
66
+ // running under npm), else the public one.
67
+ function defaultRegistry(){
68
+ return (process.env.npm_config_registry || DEFAULT_REGISTRY).replace(/\/+$/, '');
69
+ }
70
+
71
+ // Latest published version of `pkg` (its `latest` dist-tag).
72
+ async function fetchLatestVersion(pkg, { registry = defaultRegistry(), timeoutMs = 10_000 } = {}){
73
+ const url = `${registry.replace(/\/+$/, '')}/${pkg.replace('/', '%2f')}/latest`,
74
+ res = await fetch(url, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) });
75
+ if (!res.ok){
76
+ throw new Error(`${pkg}: registry returned HTTP ${res.status}`);
77
+ }
78
+ const { version } = await res.json();
79
+ if (!isValidVersion(version)){
80
+ throw new Error(`${pkg}: registry returned an invalid version`);
81
+ }
82
+ return version;
83
+ }
84
+
85
+ // The npm that belongs to the running node (same prefix as this global
86
+ // install, whether apt, nvm or a custom prefix), falling back to PATH.
87
+ function npmBin(){
88
+ const candidate = path.join(path.dirname(process.execPath), 'npm');
89
+ return fs.existsSync(candidate) ? candidate : 'npm';
90
+ }
91
+
92
+ // `npm i -g <pkg>@<version>` (as the current user — root for the Client's
93
+ // update helper; `sudo` is the caller's job otherwise). Returns npm's exit code.
94
+ function npmInstallGlobal(pkg, version, { env = process.env, stdio = 'inherit' } = {}){
95
+ if (!Object.values(PACKAGES).includes(pkg)){
96
+ throw new Error(`Refusing to install unknown package ${pkg}`);
97
+ }
98
+ if (!isValidVersion(version)){
99
+ throw new Error(`Invalid version "${version}"`);
100
+ }
101
+ const result = spawnSync(npmBin(), ['i', '-g', `${pkg}@${version}`], { env, stdio });
102
+ if (result.error){
103
+ throw result.error;
104
+ }
105
+ return result.status;
106
+ }
107
+
108
+ module.exports = {
109
+ PACKAGES,
110
+ isValidVersion,
111
+ compareVersions,
112
+ isNewer,
113
+ defaultRegistry,
114
+ fetchLatestVersion,
115
+ npmBin,
116
+ npmInstallGlobal
117
+ };