@openagents-org/agent-launcher 0.2.130 → 0.2.132
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 +1 -1
- package/registry.json +7 -5
- package/src/adapters/base.js +130 -7
- package/src/adapters/claude.js +342 -255
- package/src/adapters/cursor.js +27 -8
- package/src/adapters/workspace-prompt.js +133 -28
- package/src/cli.js +85 -0
- package/src/daemon.js +12 -3
- package/src/github-client.js +165 -0
- package/src/index.js +13 -1
- package/src/installer.js +151 -19
- package/src/mcp-server.js +279 -91
- package/src/paths.js +31 -3
- package/src/registry.js +4 -4
- package/src/skill-catalog.js +125 -0
- package/src/tui.js +1 -16
- package/src/update-check.js +5 -25
- package/src/workspace-client.js +101 -6
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BASE_URL = 'https://api.github.com';
|
|
6
|
+
const USER_AGENT = '@openagents-org/agent-launcher';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Thin GitHub REST API client used by the launcher's GitHub integration.
|
|
10
|
+
*
|
|
11
|
+
* Auth is via a personal access token (classic or fine-grained) or GitHub
|
|
12
|
+
* App installation token. Each call accepts an explicit token so the caller
|
|
13
|
+
* can resolve it from the launcher's credentials store at request time.
|
|
14
|
+
*/
|
|
15
|
+
class GitHubClient {
|
|
16
|
+
constructor(opts = {}) {
|
|
17
|
+
this.baseUrl = (opts.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verify a token and return identity / rate-limit info.
|
|
22
|
+
* Mirrors `GET /user` + `GET /rate_limit`.
|
|
23
|
+
*/
|
|
24
|
+
async probe(token) {
|
|
25
|
+
if (!token) throw new Error('Missing GitHub token');
|
|
26
|
+
const user = await this._get('/user', token);
|
|
27
|
+
const rate = await this._get('/rate_limit', token).catch(() => null);
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
login: user.login,
|
|
31
|
+
name: user.name,
|
|
32
|
+
avatarUrl: user.avatar_url,
|
|
33
|
+
scopes: user.__scopes || [],
|
|
34
|
+
rate: rate ? rate.rate : null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getRepo(owner, name, token) {
|
|
39
|
+
return this._get(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`, token);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} owner
|
|
44
|
+
* @param {string} name
|
|
45
|
+
* @param {{ state?: 'open'|'closed'|'all', perPage?: number, page?: number }} opts
|
|
46
|
+
* @param {string} token
|
|
47
|
+
*/
|
|
48
|
+
async listIssues(owner, name, opts = {}, token) {
|
|
49
|
+
const state = opts.state || 'open';
|
|
50
|
+
const perPage = opts.perPage || 20;
|
|
51
|
+
const page = opts.page || 1;
|
|
52
|
+
// GitHub's /issues endpoint returns PRs too — filter to issue-only.
|
|
53
|
+
const items = await this._get(
|
|
54
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/issues?state=${state}&per_page=${perPage}&page=${page}`,
|
|
55
|
+
token,
|
|
56
|
+
);
|
|
57
|
+
return items.filter((i) => !i.pull_request);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async listPullRequests(owner, name, opts = {}, token) {
|
|
61
|
+
const state = opts.state || 'open';
|
|
62
|
+
const perPage = opts.perPage || 20;
|
|
63
|
+
const page = opts.page || 1;
|
|
64
|
+
return this._get(
|
|
65
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls?state=${state}&per_page=${perPage}&page=${page}`,
|
|
66
|
+
token,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createIssueComment(owner, name, issueNumber, body, token) {
|
|
71
|
+
return this._post(
|
|
72
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/issues/${issueNumber}/comments`,
|
|
73
|
+
{ body },
|
|
74
|
+
token,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse a GitHub-ish input into { owner, name }.
|
|
80
|
+
* Accepts: "owner/name", "https://github.com/owner/name", "git@github.com:owner/name.git".
|
|
81
|
+
*/
|
|
82
|
+
static parseRepo(input) {
|
|
83
|
+
if (!input) return null;
|
|
84
|
+
const v = String(input).trim();
|
|
85
|
+
if (!v) return null;
|
|
86
|
+
// SSH form: git@github.com:owner/name(.git)
|
|
87
|
+
const ssh = v.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
88
|
+
if (ssh) return { owner: ssh[1], name: ssh[2] };
|
|
89
|
+
// URL form
|
|
90
|
+
try {
|
|
91
|
+
const u = new URL(v);
|
|
92
|
+
if (/github\.com$/i.test(u.hostname)) {
|
|
93
|
+
const parts = u.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
|
|
94
|
+
if (parts.length >= 2 && parts[0] && parts[1]) {
|
|
95
|
+
return { owner: parts[0], name: parts[1] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// not a URL — fall through
|
|
100
|
+
}
|
|
101
|
+
// Plain "owner/name"
|
|
102
|
+
const plain = v.match(/^([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
103
|
+
if (plain) return { owner: plain[1], name: plain[2] };
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_headers(token, extra = {}) {
|
|
108
|
+
return {
|
|
109
|
+
Accept: 'application/vnd.github+json',
|
|
110
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
111
|
+
'User-Agent': USER_AGENT,
|
|
112
|
+
Authorization: `Bearer ${token}`,
|
|
113
|
+
...extra,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_get(urlPath, token, timeout = 20000) {
|
|
118
|
+
return this._request('GET', urlPath, null, token, timeout);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_post(urlPath, body, token, timeout = 30000) {
|
|
122
|
+
return this._request('POST', urlPath, body, token, timeout);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_request(method, urlPath, body, token, timeout) {
|
|
126
|
+
const fullUrl = this.baseUrl + urlPath;
|
|
127
|
+
const jsonBody = body == null ? null : JSON.stringify(body);
|
|
128
|
+
const headers = this._headers(token);
|
|
129
|
+
if (jsonBody != null) headers['Content-Type'] = 'application/json';
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const req = https.request(fullUrl, { method, headers, timeout }, (res) => {
|
|
133
|
+
let data = '';
|
|
134
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
135
|
+
res.on('end', () => {
|
|
136
|
+
const status = res.statusCode || 0;
|
|
137
|
+
let parsed = null;
|
|
138
|
+
try { parsed = data ? JSON.parse(data) : null; } catch { parsed = null; }
|
|
139
|
+
if (status >= 400) {
|
|
140
|
+
const msg = (parsed && (parsed.message || parsed.error)) || `HTTP ${status}`;
|
|
141
|
+
const err = new Error(`GitHub ${method} ${urlPath} failed: ${msg}`);
|
|
142
|
+
err.status = status;
|
|
143
|
+
err.body = parsed;
|
|
144
|
+
return reject(err);
|
|
145
|
+
}
|
|
146
|
+
// Attach OAuth scopes for /user probe convenience.
|
|
147
|
+
if (parsed && method === 'GET' && urlPath === '/user') {
|
|
148
|
+
const scopes = res.headers['x-oauth-scopes'];
|
|
149
|
+
if (typeof scopes === 'string') {
|
|
150
|
+
parsed.__scopes = scopes.split(',').map((s) => s.trim()).filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
resolve(parsed);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
req.on('error', reject);
|
|
158
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('GitHub request timed out')); });
|
|
159
|
+
if (jsonBody != null) req.write(jsonBody);
|
|
160
|
+
req.end();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { GitHubClient };
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const { Registry } = require('./registry');
|
|
|
6
6
|
const { Installer } = require('./installer');
|
|
7
7
|
const { Daemon } = require('./daemon');
|
|
8
8
|
const { WorkspaceClient } = require('./workspace-client');
|
|
9
|
+
const { GitHubClient } = require('./github-client');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Main entry point for the agent-connector library.
|
|
@@ -125,6 +126,17 @@ class AgentConnector {
|
|
|
125
126
|
return { success: true };
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Delete the entire type-level env file (~/.openagents/env/<type>.env).
|
|
131
|
+
* Used by the launcher's "wipe saved credentials" path on uninstall so
|
|
132
|
+
* sensitive values (API keys) don't survive a reinstall as surprise
|
|
133
|
+
* pre-filled defaults in the setup wizard.
|
|
134
|
+
*/
|
|
135
|
+
deleteAgentEnv(agentType) {
|
|
136
|
+
this.env.delete(agentType);
|
|
137
|
+
return { success: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
saveAgentInstanceEnv(agentName, env) {
|
|
129
141
|
const agent = this.config.getAgent(agentName);
|
|
130
142
|
if (!agent) throw new Error(`Agent '${agentName}' not found`);
|
|
@@ -274,4 +286,4 @@ class AgentConnector {
|
|
|
274
286
|
const adapters = require('./adapters');
|
|
275
287
|
|
|
276
288
|
const paths = require('./paths');
|
|
277
|
-
module.exports = { AgentConnector, Daemon, WorkspaceClient, adapters, paths };
|
|
289
|
+
module.exports = { AgentConnector, Daemon, WorkspaceClient, GitHubClient, adapters, paths };
|
package/src/installer.js
CHANGED
|
@@ -433,8 +433,15 @@ class Installer {
|
|
|
433
433
|
if (proc.stdout) proc.stdout.setEncoding('utf-8');
|
|
434
434
|
if (proc.stderr) proc.stderr.setEncoding('utf-8');
|
|
435
435
|
|
|
436
|
-
|
|
437
|
-
|
|
436
|
+
let outputTail = '';
|
|
437
|
+
const captureOutput = (d) => {
|
|
438
|
+
const text = String(d);
|
|
439
|
+
outputTail = (outputTail + text).slice(-4000);
|
|
440
|
+
if (onData) onData(text);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
if (proc.stdout) proc.stdout.on('data', captureOutput);
|
|
444
|
+
if (proc.stderr) proc.stderr.on('data', captureOutput);
|
|
438
445
|
|
|
439
446
|
proc.on('error', (err) => reject(err));
|
|
440
447
|
proc.on('close', (code) => {
|
|
@@ -443,7 +450,21 @@ class Installer {
|
|
|
443
450
|
if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
|
|
444
451
|
resolve({ success: true, command: cmd });
|
|
445
452
|
} else {
|
|
446
|
-
|
|
453
|
+
// A partial install can leave a placeholder stub at
|
|
454
|
+
// bin/<binary>.exe and npm-generated cmd-shims under
|
|
455
|
+
// node_modules/.bin/<name>{,.cmd,.ps1} that point to it. On
|
|
456
|
+
// Windows, anything that later tries to run the binary (a health
|
|
457
|
+
// check, a shell open) triggers a "this app can't run on your PC"
|
|
458
|
+
// dialog because the stub isn't a real PE executable. Sweep them
|
|
459
|
+
// up so the next install (or uninstall+reinstall) starts clean.
|
|
460
|
+
try {
|
|
461
|
+
this._cleanStaleShims(agentType);
|
|
462
|
+
this._cleanStubBinary(agentType);
|
|
463
|
+
} catch {}
|
|
464
|
+
const tail = outputTail.trim();
|
|
465
|
+
const msg = tail
|
|
466
|
+
? `Install failed with exit code ${code}\nCommand: ${cmd}\n\n${tail}`
|
|
467
|
+
: `Install failed with exit code ${code}\nCommand: ${cmd}`;
|
|
447
468
|
if (onData) onData(`\n${msg}\n`);
|
|
448
469
|
reject(new Error(msg));
|
|
449
470
|
}
|
|
@@ -451,6 +472,35 @@ class Installer {
|
|
|
451
472
|
});
|
|
452
473
|
}
|
|
453
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Remove the placeholder bin/<binary>.exe (and the npm package directory
|
|
477
|
+
* if it's effectively empty) left over from a failed postinstall.
|
|
478
|
+
*/
|
|
479
|
+
_cleanStubBinary(agentType) {
|
|
480
|
+
const entry = this.registry.getEntry(agentType);
|
|
481
|
+
const install = entry && entry.install;
|
|
482
|
+
if (!install) return;
|
|
483
|
+
const binary = install.binary || agentType;
|
|
484
|
+
const npmPkg = install.npm_package
|
|
485
|
+
|| (() => {
|
|
486
|
+
const cmd = this._getInstallCommand(install);
|
|
487
|
+
if (!cmd || !cmd.includes('npm install')) return null;
|
|
488
|
+
const m = cmd.match(/npm install\s+(?:-g\s+)?(@?[\w-]+(?:\/[\w-]+)?)(?:@\S*)?$/);
|
|
489
|
+
return m ? m[1] : null;
|
|
490
|
+
})();
|
|
491
|
+
if (!npmPkg) return;
|
|
492
|
+
|
|
493
|
+
const prefix = getRuntimePrefix(agentType);
|
|
494
|
+
const pkgDir = path.join(prefix, 'node_modules', npmPkg);
|
|
495
|
+
const stubDir = path.join(pkgDir, 'bin');
|
|
496
|
+
for (const ext of ['', '.exe', '.cmd', '.ps1']) {
|
|
497
|
+
try {
|
|
498
|
+
const f = path.join(stubDir, binary + ext);
|
|
499
|
+
if (fs.existsSync(f) && fs.statSync(f).size < 4096) fs.unlinkSync(f);
|
|
500
|
+
} catch {}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
454
504
|
/**
|
|
455
505
|
* Uninstall an agent runtime.
|
|
456
506
|
* @returns {Promise<{success: boolean, output: string}>}
|
|
@@ -566,7 +616,7 @@ class Installer {
|
|
|
566
616
|
*/
|
|
567
617
|
_getInstallCommand(installCfg) {
|
|
568
618
|
const plat = Installer.platform();
|
|
569
|
-
return installCfg[plat] || installCfg.command || null;
|
|
619
|
+
return installCfg[plat] || installCfg.command || installCfg.npm || null;
|
|
570
620
|
}
|
|
571
621
|
|
|
572
622
|
/**
|
|
@@ -608,7 +658,14 @@ class Installer {
|
|
|
608
658
|
_whichBinary(agentType) {
|
|
609
659
|
const entry = this.registry.getEntry(agentType);
|
|
610
660
|
const binary = entry && entry.install ? entry.install.binary : agentType;
|
|
611
|
-
|
|
661
|
+
const aliases = entry && entry.install && Array.isArray(entry.install.binary_aliases)
|
|
662
|
+
? entry.install.binary_aliases
|
|
663
|
+
: [];
|
|
664
|
+
for (const candidate of [binary, ...aliases]) {
|
|
665
|
+
const found = whichBinary(candidate);
|
|
666
|
+
if (found) return found;
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
612
669
|
}
|
|
613
670
|
|
|
614
671
|
// -- Markers --
|
|
@@ -680,28 +737,58 @@ class Installer {
|
|
|
680
737
|
const extraDirs = [];
|
|
681
738
|
try { extraDirs.push(path.dirname(process.execPath)); } catch {}
|
|
682
739
|
|
|
683
|
-
//
|
|
740
|
+
// Whether a usable `node` is already reachable WITHOUT the bundled
|
|
741
|
+
// standalone node.exe. We must avoid shadowing a working system node with
|
|
742
|
+
// the launcher's bundled exe: that file is an unsigned standalone
|
|
743
|
+
// node.exe and on some Windows machines Defender/SmartScreen blocks
|
|
744
|
+
// CreateProcess on it, which surfaces as `cmd: 拒绝访问` ("Access
|
|
745
|
+
// Denied") when npm runs `node install.cjs` for packages like
|
|
746
|
+
// @anthropic-ai/claude-code, plus a "this app can't run on your PC"
|
|
747
|
+
// dialog.
|
|
748
|
+
const bundledDir = path.join(this.configDir, 'nodejs');
|
|
749
|
+
const hasSystemNode = this._hasSystemNode(bundledDir);
|
|
750
|
+
|
|
684
751
|
if (process.platform === 'win32') {
|
|
685
|
-
try {
|
|
686
|
-
const bundledDir = path.join(this.configDir, 'nodejs');
|
|
687
|
-
if (fs.existsSync(bundledDir)) {
|
|
688
|
-
// Node.js may be directly in nodejs/ (launcher symlink style) or in nodejs/node-v*/
|
|
689
|
-
if (fs.existsSync(path.join(bundledDir, 'node.exe'))) {
|
|
690
|
-
extraDirs.push(bundledDir);
|
|
691
|
-
}
|
|
692
|
-
const entries = fs.readdirSync(bundledDir).filter(e => e.startsWith('node-'));
|
|
693
|
-
if (entries.length > 0) {
|
|
694
|
-
extraDirs.push(path.join(bundledDir, entries[0]));
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
} catch {}
|
|
698
752
|
const appData = env.APPDATA || '';
|
|
699
753
|
if (appData) extraDirs.push(path.join(appData, 'npm'));
|
|
700
754
|
extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'nodejs') : 'C:\\Program Files\\nodejs');
|
|
701
755
|
extraDirs.push(env.SystemRoot ? path.join(env.SystemRoot, 'System32') : 'C:\\Windows\\System32');
|
|
702
756
|
extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'Git', 'cmd') : 'C:\\Program Files\\Git\\cmd');
|
|
757
|
+
|
|
758
|
+
// Bundled node — fallback only. Skip if system node works, otherwise
|
|
759
|
+
// verify the bundled exe can actually launch before exposing it.
|
|
760
|
+
if (!hasSystemNode) {
|
|
761
|
+
try {
|
|
762
|
+
if (fs.existsSync(bundledDir)) {
|
|
763
|
+
const directExe = path.join(bundledDir, 'node.exe');
|
|
764
|
+
if (fs.existsSync(directExe) && this._canExecute(directExe)) {
|
|
765
|
+
extraDirs.unshift(bundledDir);
|
|
766
|
+
}
|
|
767
|
+
for (const entry of fs.readdirSync(bundledDir).filter(e => e.startsWith('node-'))) {
|
|
768
|
+
const nested = path.join(bundledDir, entry);
|
|
769
|
+
if (fs.existsSync(path.join(nested, 'node.exe')) && this._canExecute(path.join(nested, 'node.exe'))) {
|
|
770
|
+
extraDirs.unshift(nested);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
} catch {}
|
|
775
|
+
}
|
|
703
776
|
} else {
|
|
704
777
|
extraDirs.push('/usr/local/bin', '/opt/homebrew/bin');
|
|
778
|
+
if (!hasSystemNode) {
|
|
779
|
+
try {
|
|
780
|
+
const candidates = [
|
|
781
|
+
path.join(bundledDir, 'bin', 'node'),
|
|
782
|
+
path.join(bundledDir, 'node'),
|
|
783
|
+
];
|
|
784
|
+
for (const bin of candidates) {
|
|
785
|
+
if (fs.existsSync(bin) && this._canExecute(bin)) {
|
|
786
|
+
extraDirs.unshift(path.dirname(bin));
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} catch {}
|
|
791
|
+
}
|
|
705
792
|
}
|
|
706
793
|
for (const d of extraDirs) {
|
|
707
794
|
if (d && !(env.PATH || '').includes(d)) {
|
|
@@ -711,6 +798,51 @@ class Installer {
|
|
|
711
798
|
return env;
|
|
712
799
|
}
|
|
713
800
|
|
|
801
|
+
/**
|
|
802
|
+
* True if `node` exists outside the bundled launcher directory. Uses raw
|
|
803
|
+
* process.env.PATH (no enhancement) so we don't accidentally detect the
|
|
804
|
+
* bundled node and conclude that "system node exists".
|
|
805
|
+
*/
|
|
806
|
+
_hasSystemNode(bundledDir) {
|
|
807
|
+
try {
|
|
808
|
+
const { execFileSync } = require('child_process');
|
|
809
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
810
|
+
const args = process.platform === 'win32' ? ['node'] : ['node'];
|
|
811
|
+
const out = execFileSync(cmd, args, {
|
|
812
|
+
encoding: 'utf-8',
|
|
813
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
814
|
+
timeout: 5000,
|
|
815
|
+
windowsHide: true,
|
|
816
|
+
env: { ...process.env },
|
|
817
|
+
});
|
|
818
|
+
const prefix = (bundledDir || '').toLowerCase();
|
|
819
|
+
for (const line of out.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
820
|
+
if (!prefix || !line.toLowerCase().startsWith(prefix)) return true;
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
} catch {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Smoke-test a node binary by running `--version`. Returns false on any
|
|
830
|
+
* failure (missing exe, blocked by AV, arch mismatch, signature policy).
|
|
831
|
+
*/
|
|
832
|
+
_canExecute(binaryPath) {
|
|
833
|
+
try {
|
|
834
|
+
const { spawnSync } = require('child_process');
|
|
835
|
+
const r = spawnSync(binaryPath, ['--version'], {
|
|
836
|
+
timeout: 5000,
|
|
837
|
+
windowsHide: true,
|
|
838
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
839
|
+
});
|
|
840
|
+
return r.status === 0 && !r.error;
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
714
846
|
/**
|
|
715
847
|
* Check if Node.js/npm is available on the system.
|
|
716
848
|
*/
|