@wathba-cli/cli 1.3.5
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 +27 -0
- package/bin/wathba.js +12 -0
- package/install.js +18 -0
- package/lib/install.js +589 -0
- package/lib/launcher.js +41 -0
- package/lib/platform.js +34 -0
- package/lib/release-config.json +5 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# `@wathba-cli/cli`
|
|
2
|
+
|
|
3
|
+
The official npm distribution of the Wathba CLI.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install --global @wathba-cli/cli
|
|
7
|
+
wathba version --json
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The package contains no npm dependencies. During installation it selects the
|
|
11
|
+
matching native Wathba release for macOS, Linux, or Windows on x64 or ARM64,
|
|
12
|
+
authenticates Wathba's signed release metadata, verifies the archive digest,
|
|
13
|
+
and runs the staged CLI's independent release verifier before installing it.
|
|
14
|
+
|
|
15
|
+
The native installer remains available from `https://install.wathba.info`.
|
|
16
|
+
An npm-managed installation must be updated through npm:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install --global @wathba-cli/cli@latest
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Install the embedded operator skills explicitly when an agent host needs them:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
wathba skill agent install --json --no-input
|
|
26
|
+
wathba skill bootstrap install --json --no-input
|
|
27
|
+
```
|
package/bin/wathba.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { ensureInstalled } = require('../lib/install');
|
|
5
|
+
const { exitLikeChild, launch } = require('../lib/launcher');
|
|
6
|
+
|
|
7
|
+
ensureInstalled()
|
|
8
|
+
.then(({ binary }) => exitLikeChild(launch(binary, process.argv.slice(2))))
|
|
9
|
+
.catch((error) => {
|
|
10
|
+
console.error(`wathba: ${error.message}`);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
});
|
package/install.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { ensureInstalled, InstallError } = require('./lib/install');
|
|
5
|
+
|
|
6
|
+
ensureInstalled()
|
|
7
|
+
.then(({ binary, installed }) => {
|
|
8
|
+
if (installed) console.error(`Wathba CLI installed at ${binary}`);
|
|
9
|
+
})
|
|
10
|
+
.catch((error) => {
|
|
11
|
+
if (error instanceof InstallError && error.transient) {
|
|
12
|
+
console.error(`Wathba CLI download was deferred: ${error.message}`);
|
|
13
|
+
console.error('The wathba command will retry on first use.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.error(`Wathba CLI installation failed: ${error.message}`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
});
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const childProcess = require('node:child_process');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const http = require('node:http');
|
|
7
|
+
const https = require('node:https');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { artifactFiles, selectPlatform } = require('./platform');
|
|
11
|
+
|
|
12
|
+
const RELEASE_CONFIG_SCHEMA = 'wathba.npm.release-config.v1';
|
|
13
|
+
const RELEASE_ROOT_SCHEMA = 'wathba.cli.distribution-root.v1';
|
|
14
|
+
const SIGNED_ENVELOPE_SCHEMA = 'wathba.distribution.signed-envelope.v1';
|
|
15
|
+
const LATEST_SCHEMA = 'wathba.distribution.latest.v1';
|
|
16
|
+
const MANIFEST_SCHEMA = 'wathba.distribution.manifest.v1';
|
|
17
|
+
const REVOCATIONS_SCHEMA = 'wathba.distribution.revocations.v1';
|
|
18
|
+
const INSTALL_STATE_SCHEMA = 'wathba.npm.install-state.v1';
|
|
19
|
+
const IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
20
|
+
const NO_CACHE_CONTROL = 'no-cache';
|
|
21
|
+
const MAX_METADATA_BYTES = 4 * 1024 * 1024;
|
|
22
|
+
const MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
|
|
23
|
+
const MAX_SIGNATURE_BYTES = 16 * 1024;
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
25
|
+
const MAX_REDIRECTS = 5;
|
|
26
|
+
const MAX_SIGNED_INTEGER = 9223372036854775807n;
|
|
27
|
+
|
|
28
|
+
class InstallError extends Error {
|
|
29
|
+
constructor(message, options = {}) {
|
|
30
|
+
super(message, options.cause ? { cause: options.cause } : undefined);
|
|
31
|
+
this.name = 'InstallError';
|
|
32
|
+
this.transient = options.transient === true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function exactKeys(value, keys, label) {
|
|
37
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
38
|
+
throw new InstallError(`${label} must be an object`);
|
|
39
|
+
}
|
|
40
|
+
const actual = Object.keys(value).sort();
|
|
41
|
+
const expected = [...keys].sort();
|
|
42
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
43
|
+
throw new InstallError(`${label} has unsupported fields`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseJSON(buffer, label) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(Buffer.isBuffer(buffer) ? buffer.toString('utf8') : buffer);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new InstallError(`${label} is not valid JSON`, { cause: error });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function decodeBase64(value, label, urlSafe = false) {
|
|
56
|
+
if (typeof value !== 'string' || value === '') throw new InstallError(`${label} is empty`);
|
|
57
|
+
const pattern = urlSafe ? /^[A-Za-z0-9_-]+$/ : /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
58
|
+
if (!pattern.test(value)) throw new InstallError(`${label} is not valid base64`);
|
|
59
|
+
const encoding = urlSafe ? 'base64url' : 'base64';
|
|
60
|
+
const decoded = Buffer.from(value, encoding);
|
|
61
|
+
if (decoded.toString(encoding) !== value) throw new InstallError(`${label} is not canonical base64`);
|
|
62
|
+
return decoded;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function decodeRootTrust(encoded) {
|
|
66
|
+
const payload = decodeBase64(encoded, 'distribution root trust', true);
|
|
67
|
+
if (payload.length > 32 * 1024) throw new InstallError('distribution root trust is too large');
|
|
68
|
+
const document = parseJSON(payload, 'distribution root trust');
|
|
69
|
+
exactKeys(document, ['schemaVersion', 'keyId', 'publicKeySpki'], 'distribution root trust');
|
|
70
|
+
if (document.schemaVersion !== RELEASE_ROOT_SCHEMA || !/^[A-Za-z0-9][A-Za-z0-9._-]{2,127}$/.test(document.keyId)) {
|
|
71
|
+
throw new InstallError('distribution root trust schema or key id is invalid');
|
|
72
|
+
}
|
|
73
|
+
const spki = decodeBase64(document.publicKeySpki, 'distribution root public key', true);
|
|
74
|
+
let publicKey;
|
|
75
|
+
try {
|
|
76
|
+
publicKey = crypto.createPublicKey({ key: spki, format: 'der', type: 'spki' });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new InstallError('distribution root public key is invalid', { cause: error });
|
|
79
|
+
}
|
|
80
|
+
const details = publicKey.asymmetricKeyDetails || {};
|
|
81
|
+
if (publicKey.asymmetricKeyType !== 'rsa' || details.modulusLength < 3072 || details.publicExponent !== 65537n) {
|
|
82
|
+
throw new InstallError('distribution root must be RSA-3072 or stronger with exponent 65537');
|
|
83
|
+
}
|
|
84
|
+
return { keyId: document.keyId, publicKey };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function verifyDetached(payload, signature, trust) {
|
|
88
|
+
if (!Buffer.isBuffer(signature) || signature.length === 0 || signature.length > MAX_SIGNATURE_BYTES) {
|
|
89
|
+
throw new InstallError('distribution signature has an invalid size');
|
|
90
|
+
}
|
|
91
|
+
const valid = crypto.verify('sha256', payload, {
|
|
92
|
+
key: trust.publicKey,
|
|
93
|
+
padding: crypto.constants.RSA_PKCS1_PADDING,
|
|
94
|
+
}, signature);
|
|
95
|
+
if (!valid) throw new InstallError('distribution signature verification failed');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function openSignedEnvelope(encoded, trust) {
|
|
99
|
+
const envelope = parseJSON(encoded, 'latest signed envelope');
|
|
100
|
+
exactKeys(envelope, ['schema_version', 'payload_base64', 'signature_base64'], 'latest signed envelope');
|
|
101
|
+
if (envelope.schema_version !== SIGNED_ENVELOPE_SCHEMA) {
|
|
102
|
+
throw new InstallError('latest signed envelope schema is incompatible');
|
|
103
|
+
}
|
|
104
|
+
const payload = decodeBase64(envelope.payload_base64, 'latest envelope payload');
|
|
105
|
+
const signature = decodeBase64(envelope.signature_base64, 'latest envelope signature');
|
|
106
|
+
verifyDetached(payload, signature, trust);
|
|
107
|
+
return payload;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sha256(data) {
|
|
111
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function canonicalVersion(packageVersion) {
|
|
115
|
+
const version = `v${packageVersion}`;
|
|
116
|
+
if (!isCanonicalVersion(version)) throw new InstallError(`npm package version ${packageVersion} is not a canonical Wathba release`);
|
|
117
|
+
return version;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isCanonicalVersion(version) {
|
|
121
|
+
const match = /^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(version);
|
|
122
|
+
if (!match) return false;
|
|
123
|
+
if (match.slice(1, 4).some((part) => BigInt(part) > MAX_SIGNED_INTEGER)) return false;
|
|
124
|
+
if (!match[4]) return true;
|
|
125
|
+
return match[4].split('.').every((part) => !/^0[0-9]+$/.test(part));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function compareVersions(left, right) {
|
|
129
|
+
if (!isCanonicalVersion(left) || !isCanonicalVersion(right)) throw new InstallError('cannot compare invalid release versions');
|
|
130
|
+
const parse = (version) => {
|
|
131
|
+
const raw = version.slice(1);
|
|
132
|
+
const separator = raw.indexOf('-');
|
|
133
|
+
const core = separator === -1 ? raw : raw.slice(0, separator);
|
|
134
|
+
const prerelease = separator === -1 ? '' : raw.slice(separator + 1);
|
|
135
|
+
return { core: core.split('.').map(BigInt), prerelease: prerelease ? prerelease.split('.') : [] };
|
|
136
|
+
};
|
|
137
|
+
const leftVersion = parse(left);
|
|
138
|
+
const rightVersion = parse(right);
|
|
139
|
+
for (let index = 0; index < 3; index += 1) {
|
|
140
|
+
if (leftVersion.core[index] !== rightVersion.core[index]) return leftVersion.core[index] < rightVersion.core[index] ? -1 : 1;
|
|
141
|
+
}
|
|
142
|
+
if (leftVersion.prerelease.length === 0 || rightVersion.prerelease.length === 0) {
|
|
143
|
+
return leftVersion.prerelease.length === rightVersion.prerelease.length ? 0 : leftVersion.prerelease.length === 0 ? 1 : -1;
|
|
144
|
+
}
|
|
145
|
+
const length = Math.min(leftVersion.prerelease.length, rightVersion.prerelease.length);
|
|
146
|
+
for (let index = 0; index < length; index += 1) {
|
|
147
|
+
const leftPart = leftVersion.prerelease[index];
|
|
148
|
+
const rightPart = rightVersion.prerelease[index];
|
|
149
|
+
if (leftPart === rightPart) continue;
|
|
150
|
+
const leftNumeric = /^[0-9]+$/.test(leftPart) && BigInt(leftPart) <= MAX_SIGNED_INTEGER;
|
|
151
|
+
const rightNumeric = /^[0-9]+$/.test(rightPart) && BigInt(rightPart) <= MAX_SIGNED_INTEGER;
|
|
152
|
+
if (leftNumeric && rightNumeric) return BigInt(leftPart) < BigInt(rightPart) ? -1 : 1;
|
|
153
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
154
|
+
return leftPart < rightPart ? -1 : 1;
|
|
155
|
+
}
|
|
156
|
+
return leftVersion.prerelease.length === rightVersion.prerelease.length ? 0 : leftVersion.prerelease.length < rightVersion.prerelease.length ? -1 : 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isRFC3339(value) {
|
|
160
|
+
if (typeof value !== 'string') return false;
|
|
161
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
|
|
162
|
+
if (!match) return false;
|
|
163
|
+
const [, year, month, day, hour, minute, second, , zoneHour = '0', zoneMinute = '0'] = match;
|
|
164
|
+
const numeric = [year, month, day, hour, minute, second, zoneHour, zoneMinute].map(Number);
|
|
165
|
+
const leapYear = numeric[0] % 4 === 0 && (numeric[0] % 100 !== 0 || numeric[0] % 400 === 0);
|
|
166
|
+
const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][numeric[1] - 1];
|
|
167
|
+
return numeric[1] >= 1 && numeric[1] <= 12 && numeric[2] >= 1 && numeric[2] <= daysInMonth
|
|
168
|
+
&& numeric[3] <= 23 && numeric[4] <= 59 && numeric[5] <= 59 && numeric[6] <= 23 && numeric[7] <= 59;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function requireSHA(value, label) {
|
|
172
|
+
if (typeof value !== 'string' || !/^[a-f0-9]{64}$/i.test(value)) throw new InstallError(`${label} has an invalid SHA-256`);
|
|
173
|
+
return value.toLowerCase();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function positiveInteger(value, label) {
|
|
177
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new InstallError(`${label} must be a positive integer`);
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseChannel(value, label) {
|
|
182
|
+
exactKeys(value, ['version', 'sequence', 'released_at', 'manifest_sha256', 'checksums_sha256', 'revocations_sha256'], label);
|
|
183
|
+
if (!isCanonicalVersion(value.version) || !isRFC3339(value.released_at)) throw new InstallError(`${label} is invalid`);
|
|
184
|
+
return {
|
|
185
|
+
version: value.version,
|
|
186
|
+
sequence: positiveInteger(value.sequence, `${label} sequence`),
|
|
187
|
+
manifestSHA256: requireSHA(value.manifest_sha256, `${label} manifest`),
|
|
188
|
+
checksumsSHA256: requireSHA(value.checksums_sha256, `${label} checksums`),
|
|
189
|
+
revocationsSHA256: requireSHA(value.revocations_sha256, `${label} revocations`),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseLatest(payload) {
|
|
194
|
+
const value = parseJSON(payload, 'latest metadata');
|
|
195
|
+
const allowed = ['schema_version', 'sequence', 'revocations_version', 'revocations_sha256', 'stable', 'beta', 'rollback'];
|
|
196
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new InstallError('latest metadata must be an object');
|
|
197
|
+
const keys = Object.keys(value);
|
|
198
|
+
if (keys.some((key) => !allowed.includes(key)) || !['schema_version', 'sequence', 'revocations_version', 'revocations_sha256', 'stable'].every((key) => keys.includes(key))) {
|
|
199
|
+
throw new InstallError('latest metadata has unsupported or missing fields');
|
|
200
|
+
}
|
|
201
|
+
if (value.schema_version !== LATEST_SCHEMA || !isCanonicalVersion(value.revocations_version)) throw new InstallError('latest metadata schema is invalid');
|
|
202
|
+
const latest = {
|
|
203
|
+
sequence: positiveInteger(value.sequence, 'latest sequence'),
|
|
204
|
+
revocationsVersion: value.revocations_version,
|
|
205
|
+
revocationsSHA256: requireSHA(value.revocations_sha256, 'latest revocations'),
|
|
206
|
+
stable: parseChannel(value.stable, 'stable channel'),
|
|
207
|
+
beta: value.beta === undefined || value.beta === null ? null : parseChannel(value.beta, 'beta channel'),
|
|
208
|
+
rollback: null,
|
|
209
|
+
};
|
|
210
|
+
if (latest.stable.sequence > latest.sequence || latest.beta && latest.beta.sequence > latest.sequence) {
|
|
211
|
+
throw new InstallError('release channel sequence exceeds latest sequence');
|
|
212
|
+
}
|
|
213
|
+
if (value.rollback !== undefined && value.rollback !== null) {
|
|
214
|
+
exactKeys(value.rollback, ['from_version', 'to_version', 'sequence'], 'latest rollback authorization');
|
|
215
|
+
const rollback = {
|
|
216
|
+
fromVersion: value.rollback.from_version,
|
|
217
|
+
toVersion: value.rollback.to_version,
|
|
218
|
+
sequence: positiveInteger(value.rollback.sequence, 'latest rollback sequence'),
|
|
219
|
+
};
|
|
220
|
+
if (!isCanonicalVersion(rollback.fromVersion) || !isCanonicalVersion(rollback.toVersion)
|
|
221
|
+
|| compareVersions(rollback.toVersion, rollback.fromVersion) >= 0
|
|
222
|
+
|| rollback.sequence !== latest.sequence || rollback.toVersion !== latest.stable.version) {
|
|
223
|
+
throw new InstallError('latest rollback authorization is invalid');
|
|
224
|
+
}
|
|
225
|
+
latest.rollback = rollback;
|
|
226
|
+
}
|
|
227
|
+
return latest;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseRevocations(payload) {
|
|
231
|
+
const value = parseJSON(payload, 'revocations metadata');
|
|
232
|
+
exactKeys(value, ['schema_version', 'sequence', 'issued_at', 'blocked_versions'], 'revocations metadata');
|
|
233
|
+
if (value.schema_version !== REVOCATIONS_SCHEMA || !isRFC3339(value.issued_at) || !Array.isArray(value.blocked_versions)) {
|
|
234
|
+
throw new InstallError('revocations metadata is invalid');
|
|
235
|
+
}
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
for (const version of value.blocked_versions) {
|
|
238
|
+
if (!isCanonicalVersion(version) || seen.has(version)) throw new InstallError('revocations metadata contains an invalid or duplicate version');
|
|
239
|
+
seen.add(version);
|
|
240
|
+
}
|
|
241
|
+
return { sequence: positiveInteger(value.sequence, 'revocations sequence'), blockedVersions: [...seen] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseManifest(payload) {
|
|
245
|
+
const value = parseJSON(payload, 'release manifest');
|
|
246
|
+
exactKeys(value, ['schema_version', 'version', 'sequence', 'source_commit', 'checksums_sha256', 'artifacts'], 'release manifest');
|
|
247
|
+
if (value.schema_version !== MANIFEST_SCHEMA || !isCanonicalVersion(value.version) || !/^[a-f0-9]{40}$/i.test(value.source_commit)) {
|
|
248
|
+
throw new InstallError('release manifest identity is invalid');
|
|
249
|
+
}
|
|
250
|
+
requireSHA(value.checksums_sha256, 'release manifest checksums');
|
|
251
|
+
exactKeys(value.artifacts, Object.keys(artifactFiles), 'release manifest artifacts');
|
|
252
|
+
const artifacts = {};
|
|
253
|
+
for (const [key, expectedFile] of Object.entries(artifactFiles)) {
|
|
254
|
+
const artifact = value.artifacts[key];
|
|
255
|
+
exactKeys(artifact, ['file', 'sha256'], `release artifact ${key}`);
|
|
256
|
+
if (artifact.file !== expectedFile) throw new InstallError(`release artifact ${key} has an unexpected filename`);
|
|
257
|
+
artifacts[key] = { file: artifact.file, sha256: requireSHA(artifact.sha256, `release artifact ${key}`) };
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
version: value.version,
|
|
261
|
+
sequence: positiveInteger(value.sequence, 'release manifest sequence'),
|
|
262
|
+
sourceCommit: value.source_commit.toLowerCase(),
|
|
263
|
+
artifacts,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function validateCacheControl(headers, expected, label) {
|
|
268
|
+
const actual = headers && typeof headers.get === 'function' ? headers.get('cache-control') : headers['cache-control'];
|
|
269
|
+
if (actual !== expected) throw new InstallError(`${label} has unexpected cache-control ${JSON.stringify(actual)}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function requestBuffer(url, options = {}, redirectCount = 0) {
|
|
273
|
+
const maxBytes = options.maxBytes || MAX_METADATA_BYTES;
|
|
274
|
+
const timeout = options.timeout || DEFAULT_TIMEOUT_MS;
|
|
275
|
+
const parsed = new URL(url);
|
|
276
|
+
if (parsed.protocol !== 'https:' && !(options.allowHTTP === true && parsed.protocol === 'http:')) {
|
|
277
|
+
return Promise.reject(new InstallError(`refusing insecure download URL ${url}`));
|
|
278
|
+
}
|
|
279
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
const request = transport.get(parsed, {
|
|
282
|
+
headers: { 'User-Agent': '@wathba-cli/cli', Accept: 'application/octet-stream, application/json' },
|
|
283
|
+
}, (response) => {
|
|
284
|
+
const status = response.statusCode || 0;
|
|
285
|
+
if ([301, 302, 303, 307, 308].includes(status)) {
|
|
286
|
+
response.resume();
|
|
287
|
+
if (!response.headers.location || redirectCount >= MAX_REDIRECTS) {
|
|
288
|
+
reject(new InstallError('download exceeded the redirect limit'));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const redirected = new URL(response.headers.location, parsed);
|
|
292
|
+
if (parsed.protocol === 'https:' && redirected.protocol !== 'https:') {
|
|
293
|
+
reject(new InstallError('refusing an HTTPS download redirect to an insecure URL'));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
requestBuffer(redirected.href, options, redirectCount + 1).then(resolve, reject);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (status !== 200) {
|
|
300
|
+
response.resume();
|
|
301
|
+
reject(new InstallError(`GET ${parsed.href} failed with HTTP ${status}`, { transient: status === 408 || status === 429 || status >= 500 }));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const chunks = [];
|
|
305
|
+
let size = 0;
|
|
306
|
+
response.on('data', (chunk) => {
|
|
307
|
+
size += chunk.length;
|
|
308
|
+
if (size > maxBytes) {
|
|
309
|
+
response.destroy(new InstallError(`GET ${parsed.href} exceeded ${maxBytes} bytes`));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
chunks.push(chunk);
|
|
313
|
+
});
|
|
314
|
+
response.once('end', () => resolve({ body: Buffer.concat(chunks), headers: response.headers }));
|
|
315
|
+
response.once('error', (error) => reject(error instanceof InstallError ? error : new InstallError(`GET ${parsed.href} failed`, { cause: error, transient: true })));
|
|
316
|
+
});
|
|
317
|
+
request.setTimeout(timeout, () => request.destroy(new InstallError(`GET ${parsed.href} timed out`, { transient: true })));
|
|
318
|
+
request.once('error', (error) => reject(error instanceof InstallError ? error : new InstallError(`GET ${parsed.href} failed`, { cause: error, transient: true })));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function retry(task, attempts = 3) {
|
|
323
|
+
let lastError;
|
|
324
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
325
|
+
try {
|
|
326
|
+
return await task(attempt);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
lastError = error;
|
|
329
|
+
if (!(error instanceof InstallError && error.transient) || attempt === attempts) throw error;
|
|
330
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 200));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
throw lastError;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function fetchSignedPair(payloadURL, signatureURL, trust, options) {
|
|
337
|
+
let verificationError;
|
|
338
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
339
|
+
const suffix = attempt === 1 ? '' : `${payloadURL.includes('?') ? '&' : '?'}wathba_pair_retry=${attempt}-${Date.now()}`;
|
|
340
|
+
const signatureSuffix = attempt === 1 ? '' : `${signatureURL.includes('?') ? '&' : '?'}wathba_pair_retry=${attempt}-${Date.now()}`;
|
|
341
|
+
const payload = await retry(() => requestBuffer(payloadURL + suffix, { ...options, maxBytes: MAX_METADATA_BYTES }));
|
|
342
|
+
const signature = await retry(() => requestBuffer(signatureURL + signatureSuffix, { ...options, maxBytes: MAX_SIGNATURE_BYTES }));
|
|
343
|
+
validateCacheControl(payload.headers, IMMUTABLE_CACHE_CONTROL, 'signed metadata');
|
|
344
|
+
validateCacheControl(signature.headers, IMMUTABLE_CACHE_CONTROL, 'metadata signature');
|
|
345
|
+
try {
|
|
346
|
+
verifyDetached(payload.body, signature.body, trust);
|
|
347
|
+
return payload.body;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
verificationError = error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
throw new InstallError(`signed metadata pair remained inconsistent: ${verificationError.message}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function resolveArtifact(options) {
|
|
356
|
+
const baseURL = options.baseURL.replace(/\/+$/, '');
|
|
357
|
+
const version = options.version;
|
|
358
|
+
const platform = options.platform || selectPlatform();
|
|
359
|
+
const trust = decodeRootTrust(options.rootBase64);
|
|
360
|
+
const requestOptions = { allowHTTP: options.allowHTTP === true, timeout: options.timeout };
|
|
361
|
+
|
|
362
|
+
const envelope = await retry(() => requestBuffer(`${baseURL}/latest.signed.json`, { ...requestOptions, maxBytes: MAX_METADATA_BYTES }));
|
|
363
|
+
validateCacheControl(envelope.headers, NO_CACHE_CONTROL, 'latest signed envelope');
|
|
364
|
+
const latestPayload = openSignedEnvelope(envelope.body, trust);
|
|
365
|
+
const latest = parseLatest(latestPayload);
|
|
366
|
+
|
|
367
|
+
const revocationsPath = `${baseURL}/releases/${latest.revocationsVersion}/revocations.json`;
|
|
368
|
+
const revocationsPayload = await fetchSignedPair(revocationsPath, `${revocationsPath}.sig`, trust, requestOptions);
|
|
369
|
+
if (sha256(revocationsPayload) !== latest.revocationsSHA256) throw new InstallError('signed revocations do not match authenticated latest metadata');
|
|
370
|
+
const revocations = parseRevocations(revocationsPayload);
|
|
371
|
+
if (revocations.sequence !== latest.sequence) throw new InstallError('revocations sequence does not match latest metadata');
|
|
372
|
+
if (revocations.blockedVersions.includes(version)) throw new InstallError(`Wathba release ${version} has been revoked`);
|
|
373
|
+
|
|
374
|
+
const manifestPath = `${baseURL}/releases/${version}/manifest.json`;
|
|
375
|
+
const manifestPayload = await fetchSignedPair(manifestPath, `${manifestPath}.sig`, trust, requestOptions);
|
|
376
|
+
const manifest = parseManifest(manifestPayload);
|
|
377
|
+
if (manifest.version !== version || manifest.sequence > latest.sequence) throw new InstallError('release manifest is outside the authenticated release sequence');
|
|
378
|
+
const channel = latest.stable.version === version ? latest.stable : latest.beta && latest.beta.version === version ? latest.beta : null;
|
|
379
|
+
if (channel && (channel.sequence !== manifest.sequence || channel.manifestSHA256 !== sha256(manifestPayload))) {
|
|
380
|
+
throw new InstallError('release manifest does not match its authenticated channel');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const artifact = manifest.artifacts[platform.key];
|
|
384
|
+
if (!artifact) throw new InstallError(`release manifest does not support ${platform.key}`);
|
|
385
|
+
const archive = await retry(() => requestBuffer(`${baseURL}/releases/${version}/${artifact.file}`, { ...requestOptions, maxBytes: MAX_ARCHIVE_BYTES }));
|
|
386
|
+
validateCacheControl(archive.headers, IMMUTABLE_CACHE_CONTROL, 'release archive');
|
|
387
|
+
if (sha256(archive.body) !== artifact.sha256) throw new InstallError('release archive SHA-256 verification failed');
|
|
388
|
+
return { archive: archive.body, artifact, latest, manifest, platform, trust };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function extractArchive(archivePath, destination, platform, spawnSync = childProcess.spawnSync) {
|
|
392
|
+
let result;
|
|
393
|
+
if (platform.windows) {
|
|
394
|
+
const command = [
|
|
395
|
+
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
396
|
+
'-Command',
|
|
397
|
+
"Expand-Archive -LiteralPath $env:WATHBA_NPM_ARCHIVE -DestinationPath $env:WATHBA_NPM_EXTRACT -Force",
|
|
398
|
+
];
|
|
399
|
+
result = spawnSync('powershell.exe', command, {
|
|
400
|
+
env: { ...process.env, WATHBA_NPM_ARCHIVE: archivePath, WATHBA_NPM_EXTRACT: destination },
|
|
401
|
+
encoding: 'utf8', windowsHide: true,
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
result = spawnSync('tar', ['-xzf', archivePath, '-C', destination], { encoding: 'utf8', windowsHide: true });
|
|
405
|
+
}
|
|
406
|
+
if (result.error || result.status !== 0) {
|
|
407
|
+
const detail = String(result.stderr || result.error && result.error.message || '').trim();
|
|
408
|
+
throw new InstallError(`could not extract the Wathba release archive${detail ? `: ${detail}` : ''}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function verifyCandidate(binary, version, baseURL, spawnSync = childProcess.spawnSync) {
|
|
413
|
+
const result = spawnSync(binary, [
|
|
414
|
+
'self-update', '--verify-only', '--version', version,
|
|
415
|
+
'--install-base-url', baseURL, '--json', '--no-input',
|
|
416
|
+
], {
|
|
417
|
+
env: { ...process.env, WATHBA_INSTALL_METHOD: 'npm' },
|
|
418
|
+
encoding: 'utf8', windowsHide: true, maxBuffer: MAX_METADATA_BYTES,
|
|
419
|
+
});
|
|
420
|
+
if (result.error || result.status !== 0) {
|
|
421
|
+
const detail = String(result.stderr || result.stdout || result.error && result.error.message || '').trim();
|
|
422
|
+
throw new InstallError(`the staged Wathba CLI rejected its release provenance${detail ? `: ${detail}` : ''}`);
|
|
423
|
+
}
|
|
424
|
+
let envelope;
|
|
425
|
+
try {
|
|
426
|
+
envelope = JSON.parse(result.stdout);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
throw new InstallError('the staged Wathba CLI returned invalid verification output', { cause: error });
|
|
429
|
+
}
|
|
430
|
+
if (envelope.schema_version !== 'wathba.output.v1' || envelope.ok !== true || !envelope.data || envelope.data.verify_only !== true) {
|
|
431
|
+
throw new InstallError('the staged Wathba CLI did not confirm release verification');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function readRuntimeConfig(packageRoot) {
|
|
436
|
+
const config = parseJSON(fs.readFileSync(path.join(packageRoot, 'lib', 'release-config.json')), 'npm release configuration');
|
|
437
|
+
exactKeys(config, ['schemaVersion', 'installBaseUrl', 'distributionRootBase64'], 'npm release configuration');
|
|
438
|
+
if (config.schemaVersion !== RELEASE_CONFIG_SCHEMA || !config.installBaseUrl.startsWith('https://') || !config.distributionRootBase64) {
|
|
439
|
+
throw new InstallError('npm release configuration has not been prepared by the Wathba release workflow');
|
|
440
|
+
}
|
|
441
|
+
return config;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function packageVersion(packageRoot) {
|
|
445
|
+
const metadata = parseJSON(fs.readFileSync(path.join(packageRoot, 'package.json')), 'npm package metadata');
|
|
446
|
+
return metadata.version;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function binaryDigest(file) {
|
|
450
|
+
return sha256(fs.readFileSync(file));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function installedBinary(packageRoot, version, platform) {
|
|
454
|
+
const binary = path.join(packageRoot, 'binaries', platform.binary);
|
|
455
|
+
const statePath = path.join(packageRoot, 'binaries', 'install-state.json');
|
|
456
|
+
try {
|
|
457
|
+
const state = parseJSON(fs.readFileSync(statePath), 'npm install state');
|
|
458
|
+
exactKeys(state, ['schemaVersion', 'version', 'artifact', 'archiveSHA256', 'binarySHA256'], 'npm install state');
|
|
459
|
+
const binaryStat = fs.lstatSync(binary);
|
|
460
|
+
if (state.schemaVersion !== INSTALL_STATE_SCHEMA || state.version !== version
|
|
461
|
+
|| state.artifact !== artifactFiles[platform.key] || !binaryStat.isFile() || binaryStat.isSymbolicLink()) return null;
|
|
462
|
+
requireSHA(state.archiveSHA256, 'npm install state archive');
|
|
463
|
+
const expectedBinarySHA256 = requireSHA(state.binarySHA256, 'npm install state binary');
|
|
464
|
+
if (binaryDigest(binary) !== expectedBinarySHA256) return null;
|
|
465
|
+
return binary;
|
|
466
|
+
} catch (_) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function acquireLock(lockPath, timeout = 30_000) {
|
|
472
|
+
const started = Date.now();
|
|
473
|
+
while (true) {
|
|
474
|
+
try {
|
|
475
|
+
return fs.openSync(lockPath, 'wx', 0o600);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error.code !== 'EEXIST') throw error;
|
|
478
|
+
try {
|
|
479
|
+
if (Date.now() - fs.statSync(lockPath).mtimeMs > 5 * 60_000) fs.unlinkSync(lockPath);
|
|
480
|
+
} catch (_) {}
|
|
481
|
+
if (Date.now() - started > timeout) throw new InstallError('timed out waiting for another Wathba npm installation');
|
|
482
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function commitBinary(source, packageRoot, version, artifact) {
|
|
488
|
+
const binaryDir = path.join(packageRoot, 'binaries');
|
|
489
|
+
fs.mkdirSync(binaryDir, { recursive: true, mode: 0o755 });
|
|
490
|
+
const target = path.join(binaryDir, path.basename(source));
|
|
491
|
+
const temporary = path.join(binaryDir, `.${path.basename(source)}-${process.pid}-${Date.now()}.tmp`);
|
|
492
|
+
fs.copyFileSync(source, temporary, fs.constants.COPYFILE_EXCL);
|
|
493
|
+
if (process.platform !== 'win32') fs.chmodSync(temporary, 0o755);
|
|
494
|
+
// Windows requires a writable handle when fsync flushes the file buffer.
|
|
495
|
+
const handle = fs.openSync(temporary, 'r+');
|
|
496
|
+
try { fs.fsyncSync(handle); } finally { fs.closeSync(handle); }
|
|
497
|
+
try { fs.renameSync(temporary, target); } catch (error) {
|
|
498
|
+
if (!['EEXIST', 'EPERM'].includes(error.code)) throw error;
|
|
499
|
+
fs.rmSync(target, { force: true });
|
|
500
|
+
fs.renameSync(temporary, target);
|
|
501
|
+
}
|
|
502
|
+
const state = {
|
|
503
|
+
schemaVersion: INSTALL_STATE_SCHEMA,
|
|
504
|
+
version,
|
|
505
|
+
artifact: artifact.file,
|
|
506
|
+
archiveSHA256: artifact.sha256,
|
|
507
|
+
binarySHA256: binaryDigest(target),
|
|
508
|
+
};
|
|
509
|
+
const stateTemporary = path.join(binaryDir, `.install-state-${process.pid}-${Date.now()}.tmp`);
|
|
510
|
+
fs.writeFileSync(stateTemporary, `${JSON.stringify(state)}\n`, { mode: 0o600 });
|
|
511
|
+
fs.renameSync(stateTemporary, path.join(binaryDir, 'install-state.json'));
|
|
512
|
+
return target;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function installRelease(options) {
|
|
516
|
+
const resolved = await resolveArtifact(options);
|
|
517
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'wathba-npm-'));
|
|
518
|
+
try {
|
|
519
|
+
const archivePath = path.join(temporary, resolved.artifact.file);
|
|
520
|
+
const extractPath = path.join(temporary, 'extract');
|
|
521
|
+
fs.mkdirSync(extractPath, { mode: 0o700 });
|
|
522
|
+
fs.writeFileSync(archivePath, resolved.archive, { mode: 0o600 });
|
|
523
|
+
(options.extractArchive || extractArchive)(archivePath, extractPath, resolved.platform, options.spawnSync);
|
|
524
|
+
const candidate = path.join(extractPath, resolved.platform.binary);
|
|
525
|
+
const stat = fs.lstatSync(candidate);
|
|
526
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0) throw new InstallError('release archive does not contain the expected Wathba binary');
|
|
527
|
+
if (!resolved.platform.windows) fs.chmodSync(candidate, 0o755);
|
|
528
|
+
(options.verifyCandidate || verifyCandidate)(candidate, options.version, options.baseURL, options.spawnSync);
|
|
529
|
+
return commitBinary(candidate, options.packageRoot, options.version, resolved.artifact);
|
|
530
|
+
} finally {
|
|
531
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function ensureInstalled(options = {}) {
|
|
536
|
+
const packageRoot = options.packageRoot || path.resolve(__dirname, '..');
|
|
537
|
+
const npmVersion = options.packageVersion || packageVersion(packageRoot);
|
|
538
|
+
const version = canonicalVersion(npmVersion);
|
|
539
|
+
const platform = options.platform || selectPlatform();
|
|
540
|
+
const existing = installedBinary(packageRoot, version, platform);
|
|
541
|
+
if (existing) return { binary: existing, installed: false, version };
|
|
542
|
+
|
|
543
|
+
const binaryDir = path.join(packageRoot, 'binaries');
|
|
544
|
+
fs.mkdirSync(binaryDir, { recursive: true, mode: 0o755 });
|
|
545
|
+
const lockPath = path.join(binaryDir, '.install.lock');
|
|
546
|
+
const lockHandle = await acquireLock(lockPath, options.lockTimeout);
|
|
547
|
+
try {
|
|
548
|
+
const afterLock = installedBinary(packageRoot, version, platform);
|
|
549
|
+
if (afterLock) return { binary: afterLock, installed: false, version };
|
|
550
|
+
const config = options.config || (options.baseURL && options.rootBase64
|
|
551
|
+
? { installBaseUrl: options.baseURL, distributionRootBase64: options.rootBase64 }
|
|
552
|
+
: readRuntimeConfig(packageRoot));
|
|
553
|
+
const binary = await installRelease({
|
|
554
|
+
...options,
|
|
555
|
+
packageRoot,
|
|
556
|
+
version,
|
|
557
|
+
platform,
|
|
558
|
+
baseURL: options.baseURL || config.installBaseUrl,
|
|
559
|
+
rootBase64: options.rootBase64 || config.distributionRootBase64,
|
|
560
|
+
});
|
|
561
|
+
return { binary, installed: true, version };
|
|
562
|
+
} finally {
|
|
563
|
+
try { fs.closeSync(lockHandle); } catch (_) {}
|
|
564
|
+
fs.rmSync(lockPath, { force: true });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
module.exports = {
|
|
569
|
+
IMMUTABLE_CACHE_CONTROL,
|
|
570
|
+
INSTALL_STATE_SCHEMA,
|
|
571
|
+
InstallError,
|
|
572
|
+
canonicalVersion,
|
|
573
|
+
compareVersions,
|
|
574
|
+
decodeRootTrust,
|
|
575
|
+
ensureInstalled,
|
|
576
|
+
extractArchive,
|
|
577
|
+
fetchSignedPair,
|
|
578
|
+
installedBinary,
|
|
579
|
+
isCanonicalVersion,
|
|
580
|
+
openSignedEnvelope,
|
|
581
|
+
parseLatest,
|
|
582
|
+
parseManifest,
|
|
583
|
+
parseRevocations,
|
|
584
|
+
requestBuffer,
|
|
585
|
+
resolveArtifact,
|
|
586
|
+
sha256,
|
|
587
|
+
verifyCandidate,
|
|
588
|
+
verifyDetached,
|
|
589
|
+
};
|
package/lib/launcher.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
function launch(binary, args, options = {}) {
|
|
6
|
+
const spawnProcess = options.spawn || spawn;
|
|
7
|
+
const env = { ...(options.env || process.env), WATHBA_INSTALL_METHOD: 'npm' };
|
|
8
|
+
return spawnProcess(binary, args, { stdio: 'inherit', env, windowsHide: false });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function exitLikeChild(child) {
|
|
12
|
+
const forwarded = [];
|
|
13
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
14
|
+
if (process.platform === 'win32' && signal === 'SIGHUP') {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const handler = () => {
|
|
18
|
+
if (!child.killed) {
|
|
19
|
+
child.kill(signal);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
process.on(signal, handler);
|
|
23
|
+
forwarded.push([signal, handler]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
child.once('error', (error) => {
|
|
27
|
+
for (const [signal, handler] of forwarded) process.off(signal, handler);
|
|
28
|
+
console.error(`wathba: could not start the installed CLI: ${error.message}`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
});
|
|
31
|
+
child.once('exit', (code, signal) => {
|
|
32
|
+
for (const [name, handler] of forwarded) process.off(name, handler);
|
|
33
|
+
if (signal && process.platform !== 'win32') {
|
|
34
|
+
process.kill(process.pid, signal);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
process.exitCode = Number.isInteger(code) ? code : 1;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { exitLikeChild, launch };
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const artifactFiles = Object.freeze({
|
|
4
|
+
darwin_arm64: 'wathba_Darwin_arm64.tar.gz',
|
|
5
|
+
darwin_x86_64: 'wathba_Darwin_x86_64.tar.gz',
|
|
6
|
+
linux_arm64: 'wathba_Linux_arm64.tar.gz',
|
|
7
|
+
linux_x86_64: 'wathba_Linux_x86_64.tar.gz',
|
|
8
|
+
windows_arm64: 'wathba_Windows_arm64.zip',
|
|
9
|
+
windows_x86_64: 'wathba_Windows_x86_64.zip',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function selectPlatform(platform = process.platform, arch = process.arch) {
|
|
13
|
+
const osName = platform === 'win32' ? 'windows' : platform;
|
|
14
|
+
const archName = arch === 'x64' || arch === 'amd64' || arch === 'x86_64'
|
|
15
|
+
? 'x86_64'
|
|
16
|
+
: arch === 'arm64' || arch === 'aarch64'
|
|
17
|
+
? 'arm64'
|
|
18
|
+
: '';
|
|
19
|
+
if (!['darwin', 'linux', 'windows'].includes(osName)) {
|
|
20
|
+
throw new Error(`Unsupported operating system: ${platform}`);
|
|
21
|
+
}
|
|
22
|
+
if (!archName) {
|
|
23
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
24
|
+
}
|
|
25
|
+
const key = `${osName}_${archName}`;
|
|
26
|
+
return {
|
|
27
|
+
key,
|
|
28
|
+
file: artifactFiles[key],
|
|
29
|
+
binary: osName === 'windows' ? 'wathba.exe' : 'wathba',
|
|
30
|
+
windows: osName === 'windows',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { artifactFiles, selectPlatform };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "wathba.npm.release-config.v1",
|
|
3
|
+
"installBaseUrl": "https://install.wathba.info",
|
|
4
|
+
"distributionRootBase64": "eyJzY2hlbWFWZXJzaW9uIjoid2F0aGJhLmNsaS5kaXN0cmlidXRpb24tcm9vdC52MSIsImtleUlkIjoid2F0aGJhLWRpc3RyaWJ1dGlvbi1yb290LTIwMjYtMDciLCJwdWJsaWNLZXlTcGtpIjoiTUlJQm9qQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FZOEFNSUlCaWdLQ0FZRUFzX0VscnJDOTAtaVNTOHRJc0xZTHRsWk9WX05CdjFIMkNCYnZRT1ByTC01QU5FdTNpdUphdVVCbUk5S3M2VTJUdFRKUVRoWUd0YW5BNnhDdFlZcFdQZlVpVndpaVl3NXNsQTRuNHNCSW11SEJvWkNqbzNrVWt2Q1I2RDVOdlJmaVFBSjVSRnVPOV9nNDlNUlc1MExPcHp1LTQwQ0g1S2pHZ2k0UUJXM3NER0FvakdJeG5rSmtrQ25yanQ0UHJvdmJqVHNqLUNuZFdjRHd4V1dLN0p1cjZYRnVwSnpCcmVVTEk0Ymd5N0tzeGJEbmUzYkRhVXFjS0NPTWw3bzlLN3BJcEd3TGNjRXd3ZTdWM1FhWXJYYVNWRkJ0UkR6WnJTejVmQXl1TGRTQ1UzcDYwYkxCemRTUHhwLThySFIzaEVuOTh4MlJnWlRzaUkxZFpKM21KNE5yMW9kNm9vSjNVQVVkbzF6SWd6T2h5ZXBpa3hWOHMtTXhMVmQ5emtUVHlVX3UwT3R2a2daSUMzNWFRbUIyZXBjZUFiUXZvZ1ZaTU0wWHB4cUJ0cjRuNExNNHN2cG1DcVFhRUwtTVhNbG5DbGRfMXBCMkd2SVNGTU5adkZFNm4xTU1CQ2dSaEdXempuQVFZeDBoeURRSld4WXlnVFNtN0tVbVh4TWxiTHVwOV8zWkFnTUJBQUUifQ"
|
|
5
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wathba-cli/cli",
|
|
3
|
+
"version": "1.3.5",
|
|
4
|
+
"description": "Agent-first CLI for the Wathba developer platform",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"homepage": "https://github.com/wathba-org/wathba-cli#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/wathba-org/wathba-cli/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/wathba-org/wathba-cli.git"
|
|
13
|
+
},
|
|
14
|
+
"type": "commonjs",
|
|
15
|
+
"bin": {
|
|
16
|
+
"wathba": "bin/wathba.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"postinstall": "node install.js",
|
|
20
|
+
"test": "node --test"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"lib/",
|
|
25
|
+
"install.js",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.18.0"
|
|
30
|
+
},
|
|
31
|
+
"os": [
|
|
32
|
+
"darwin",
|
|
33
|
+
"linux",
|
|
34
|
+
"win32"
|
|
35
|
+
],
|
|
36
|
+
"cpu": [
|
|
37
|
+
"x64",
|
|
38
|
+
"arm64"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|