@agent-wechat/cli 0.15.6 → 1.0.0-beta.1

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.
@@ -0,0 +1,181 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { access, chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { spawn, execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { dirname, isAbsolute, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const exec = promisify(execFile);
10
+ export const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ export const releaseRepository = 'thisnick/agent-wechat';
12
+ export const supportedTargets = Object.freeze([
13
+ Object.freeze({ platform: 'linux', arch: 'x64', libc: 'gnu', minimumGlibc: '2.36', rustTarget: 'x86_64-unknown-linux-gnu' }),
14
+ Object.freeze({ platform: 'linux', arch: 'arm64', libc: 'gnu', minimumGlibc: '2.36', rustTarget: 'aarch64-unknown-linux-gnu' }),
15
+ ]);
16
+ export const supportedTarget = supportedTargets[0];
17
+ const maxBinaryBytes = 256 * 1024 * 1024;
18
+ const allowedDownloadHosts = new Set(['github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com']);
19
+
20
+ export function targetFor({ platform = process.platform, arch = process.arch, glibc = process.report?.getReport().header.glibcVersionRuntime } = {}) {
21
+ const [major, minor] = String(glibc ?? '').split('.').map(Number);
22
+ const target = supportedTargets.find(item => item.platform === platform && item.arch === arch);
23
+ if (!target || !glibc || !Number.isFinite(major) || !Number.isFinite(minor) || major < 2 || (major === 2 && minor < 36)) {
24
+ throw new Error('Native wx requires Linux x64 or ARM64 with glibc 2.36 or newer. Install a supported native runtime or connect remotely to its API.');
25
+ }
26
+ return target;
27
+ }
28
+ export function assetName(version, target = targetFor()) { return `wx-v${version}-${target.platform}-${target.arch}-${target.libc}`; }
29
+ export function manifestName(version) { return `wx-v${version}-checksums.json`; }
30
+ export function releaseUrl(version, name) {
31
+ return `https://github.com/${releaseRepository}/releases/download/v${version}/${name}`;
32
+ }
33
+ export function validateManifest(manifest, version, { target = targetFor(), requireAllTargets = false } = {}) {
34
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version) || manifest?.schemaVersion !== 1
35
+ || manifest.version !== version || manifest.repository !== releaseRepository
36
+ || !/^[a-f0-9]{40}$/.test(manifest.sourceCommit ?? '') || typeof manifest.sourceTreeDirty !== 'boolean' || !Array.isArray(manifest.assets) || manifest.assets.length < 1 || manifest.assets.length > supportedTargets.length) {
37
+ throw new Error('Invalid or mismatched native release manifest');
38
+ }
39
+ const seen = new Set();
40
+ for (const asset of manifest.assets) {
41
+ const expected = supportedTargets.find(item => item.arch === asset.arch);
42
+ if (!expected || seen.has(asset.arch) || Object.entries(expected).some(([key, value]) => asset[key] !== value)
43
+ || asset.name !== assetName(version, expected) || !/^[a-f0-9]{64}$/.test(asset.sha256 ?? '')
44
+ || !Number.isSafeInteger(asset.bytes) || asset.bytes < 1 || asset.bytes > maxBinaryBytes) {
45
+ throw new Error('Invalid native release asset, duplicate or unsupported target');
46
+ }
47
+ seen.add(asset.arch);
48
+ }
49
+ if (requireAllTargets && seen.size !== supportedTargets.length) throw new Error('Native publication requires every supported target');
50
+ const asset = manifest.assets.find(item => item.arch === target.arch);
51
+ if (!asset) throw new Error(`Native manifest has no asset for ${target.arch}`);
52
+ return asset;
53
+ }
54
+ export async function readManifest(root = packageRoot) {
55
+ const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
56
+ let data;
57
+ try { data = await readFile(join(root, 'native-release.json')); }
58
+ catch { throw new Error('This CLI package has no verified native release manifest. Build/provision a matching native release; invocation never downloads automatically.'); }
59
+ if (data.length > 65536) throw new Error('Native release manifest is too large');
60
+ const manifest = JSON.parse(data);
61
+ return { version: pkg.version, manifest, asset: validateManifest(manifest, pkg.version) };
62
+ }
63
+ export async function fetchBytes(url, maxBytes, { fetchImpl = fetch, allowHttpLoopback = false } = {}) {
64
+ let current = new URL(url);
65
+ if (current.protocol === 'https:' && process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') throw new Error('Native downloads refuse disabled TLS verification');
66
+ const signal = AbortSignal.timeout(30000);
67
+ for (let redirects = 0; redirects <= 3; redirects++) {
68
+ const testLocal = allowHttpLoopback && current.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(current.hostname);
69
+ if (current.username || current.password || current.hash || (!testLocal && (current.protocol !== 'https:' || !allowedDownloadHosts.has(current.hostname) || current.port))) {
70
+ throw new Error('Native downloads require an approved HTTPS release host');
71
+ }
72
+ const response = await fetchImpl(current, { redirect: 'manual', signal });
73
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
74
+ await response.body?.cancel();
75
+ const location = response.headers.get('location');
76
+ if (!location || redirects === 3) throw new Error('Invalid or excessive native release redirects');
77
+ current = new URL(location, current);
78
+ continue;
79
+ }
80
+ if (!response.ok) { await response.body?.cancel(); throw new Error(`Native release download failed (HTTP ${response.status})`); }
81
+ const declared = response.headers.get('content-length');
82
+ if (declared && (!/^\d+$/.test(declared) || Number(declared) > maxBytes)) {
83
+ await response.body?.cancel(); throw new Error('Native release asset exceeds its size bound');
84
+ }
85
+ const chunks = [];
86
+ let total = 0;
87
+ if (!response.body) throw new Error('Native release asset has no body');
88
+ for await (const chunk of response.body) {
89
+ total += chunk.byteLength;
90
+ if (total > maxBytes) throw new Error('Native release asset exceeds its size bound');
91
+ chunks.push(chunk);
92
+ }
93
+ return Buffer.concat(chunks);
94
+ }
95
+ throw new Error('Native release redirect failed');
96
+ }
97
+ export function verifyBytes(bytes, asset) {
98
+ if (bytes.length !== asset.bytes || createHash('sha256').update(bytes).digest('hex') !== asset.sha256) {
99
+ throw new Error('Native binary size or SHA-256 does not match the manifest bundled in this npm package');
100
+ }
101
+ }
102
+ export async function inspectBinary(path, version) {
103
+ if (!isAbsolute(path)) throw new Error('Native binary path must be absolute');
104
+ const info = await lstat(path);
105
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error('Native binary must be a regular file');
106
+ await access(path, constants.X_OK);
107
+ const { stdout } = await exec(path, ['--json', 'version'], { timeout: 5000, maxBuffer: 65536, windowsHide: true });
108
+ const value = JSON.parse(stdout);
109
+ if (value.product !== 'agent-wechat' || value.backend !== 'native' || value.version !== version
110
+ || value.target !== targetFor().rustTarget || value.apiVersion !== 1 || !value.capabilities?.includes('serve')) {
111
+ throw new Error('Executable is not a matching native wx runtime; no container fallback is performed');
112
+ }
113
+ return value;
114
+ }
115
+ export function installPath(root, version, target = targetFor()) { return join(root, 'native', version, `${target.platform}-${target.arch}-${target.libc}`, 'wx'); }
116
+ export async function installNative({ root = packageRoot, artifactDir, fetchOptions, downloadUrl, inspect = inspectBinary } = {}) {
117
+ targetFor();
118
+ const { version, asset } = await readManifest(root);
119
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') throw new Error('Refusing native installation with TLS verification disabled');
120
+ let bytes;
121
+ if (artifactDir) {
122
+ if (!isAbsolute(artifactDir)) throw new Error('Offline artifact directory must be absolute');
123
+ const localManifest = JSON.parse(await readFile(join(artifactDir, manifestName(version)), 'utf8'));
124
+ const localAsset = validateManifest(localManifest, version);
125
+ if (localAsset.sha256 !== asset.sha256 || localAsset.bytes !== asset.bytes || localManifest.sourceCommit !== (await readManifest(root)).manifest.sourceCommit) {
126
+ throw new Error('Offline manifest differs from the manifest bundled in this npm package');
127
+ }
128
+ const source = join(artifactDir, asset.name);
129
+ const info = await lstat(source);
130
+ if (!info.isFile() || info.isSymbolicLink() || info.size !== asset.bytes) throw new Error('Invalid offline native release file');
131
+ bytes = await readFile(source);
132
+ } else {
133
+ bytes = await fetchBytes(downloadUrl ?? releaseUrl(version, asset.name), asset.bytes, fetchOptions);
134
+ }
135
+ verifyBytes(bytes, asset);
136
+ const destination = installPath(root, version);
137
+ await mkdir(dirname(destination), { recursive: true, mode: 0o755 });
138
+ const temporary = `${destination}.${process.pid}.${createHash('sha256').update(randomUUID()).digest('hex').slice(0, 16)}.tmp`;
139
+ try {
140
+ await writeFile(temporary, bytes, { flag: 'wx', mode: 0o755 });
141
+ await chmod(temporary, 0o755);
142
+ await inspect(temporary, version);
143
+ await rename(temporary, destination);
144
+ } finally { await rm(temporary, { force: true }); }
145
+ return destination;
146
+ }
147
+ export async function resolveNative({ root = packageRoot, binaryOverride = process.env.AGENT_WECHAT_NATIVE_BINARY, inspect = inspectBinary } = {}) {
148
+ targetFor();
149
+ if (binaryOverride) {
150
+ const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
151
+ if (!isAbsolute(binaryOverride)) throw new Error('AGENT_WECHAT_NATIVE_BINARY must be an absolute executable path');
152
+ const canonical = await realpath(binaryOverride);
153
+ await inspect(canonical, version);
154
+ return canonical;
155
+ }
156
+ const { version, asset } = await readManifest(root);
157
+ const path = installPath(root, version);
158
+ let bytes;
159
+ try { bytes = await readFile(path); }
160
+ catch { throw new Error('Native wx is not installed. Run wx-native-install, or provision the exact release and set AGENT_WECHAT_NATIVE_BINARY to its absolute path. No download or Docker fallback occurs during invocation.'); }
161
+ verifyBytes(bytes, asset);
162
+ await inspect(path, version);
163
+ return path;
164
+ }
165
+ export function runNative(path, args, { spawnImpl = spawn, processObject = process } = {}) {
166
+ const child = spawnImpl(path, args, { stdio: 'inherit', shell: false, windowsHide: true });
167
+ const handlers = new Map();
168
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
169
+ const handler = () => { if (!child.killed) child.kill(signal); };
170
+ handlers.set(signal, handler);
171
+ processObject.on(signal, handler);
172
+ }
173
+ const cleanup = () => { for (const [signal, handler] of handlers) processObject.off(signal, handler); };
174
+ child.once('error', () => { cleanup(); processObject.exitCode = 1; console.error('Unable to launch native wx'); });
175
+ child.once('exit', (code, signal) => {
176
+ cleanup();
177
+ if (signal) processObject.kill(processObject.pid, signal);
178
+ else processObject.exitCode = code ?? 1;
179
+ });
180
+ return child;
181
+ }