@andrian.yablonskyy/thub-common 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/README.md +6 -1
- package/package.json +1 -1
- package/src/api-client.js +7 -1
- package/src/index.js +2 -1
- package/src/updates.js +117 -0
package/README.md
CHANGED
|
@@ -46,7 +46,8 @@ A minimal `fetch`-based HTTP client for the Coordinator's `/api/v1` surface, wit
|
|
|
46
46
|
```js
|
|
47
47
|
const { ApiClient } = require('@andrian.yablonskyy/thub-common');
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// userAgent is optional; the Coordinator records "thub-agent/<v>" / "thub-client/<v>" versions for its dashboard
|
|
50
|
+
const client = new ApiClient({ baseUrl: 'https://thub.example.com', token: 'agt_...', userAgent: 'thub-agent/1.0.8' });
|
|
50
51
|
const job = await client.post('/jobs', spec);
|
|
51
52
|
await client.streamEvents(`/jobs/${job.jobId}/logs/stream`, {
|
|
52
53
|
onEvent: ({ event, id, data }) => console.log(event, data)
|
|
@@ -55,6 +56,10 @@ await client.streamEvents(`/jobs/${job.jobId}/logs/stream`, {
|
|
|
55
56
|
|
|
56
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.
|
|
57
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
|
+
|
|
58
63
|
## Development
|
|
59
64
|
|
|
60
65
|
```bash
|
package/package.json
CHANGED
package/src/api-client.js
CHANGED
|
@@ -20,12 +20,15 @@
|
|
|
20
20
|
* and manual usage identical").
|
|
21
21
|
*/
|
|
22
22
|
class ApiClient{
|
|
23
|
-
|
|
23
|
+
// `userAgent` (e.g. "thub-agent/1.0.8") is how the Coordinator learns
|
|
24
|
+
// which Agent/Client version is talking to it, for the dashboard.
|
|
25
|
+
constructor({ baseUrl, token, userAgent }){
|
|
24
26
|
if (!baseUrl){
|
|
25
27
|
throw new Error('ApiClient requires baseUrl');
|
|
26
28
|
}
|
|
27
29
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
28
30
|
this.token = token;
|
|
31
|
+
this.userAgent = userAgent;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
_url(path){
|
|
@@ -34,6 +37,9 @@ class ApiClient{
|
|
|
34
37
|
|
|
35
38
|
_headers(extra = {}){
|
|
36
39
|
const headers = { ...extra };
|
|
40
|
+
if (this.userAgent){
|
|
41
|
+
headers['User-Agent'] = this.userAgent;
|
|
42
|
+
}
|
|
37
43
|
if (this.token){
|
|
38
44
|
headers.Authorization = `Bearer ${this.token}`;
|
|
39
45
|
}
|
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
|
|
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
|
+
};
|