@oddessentials/agent-guild 0.36.0 → 0.37.0
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 +2 -0
- package/bin/agent-guild.mjs +15 -0
- package/package.json +2 -1
- package/src/manager/doctor.mjs +406 -0
package/README.md
CHANGED
|
@@ -142,6 +142,8 @@ The first Claude Code usage meter may ask for access to the
|
|
|
142
142
|
| `agent-guild restart` | Restart the manager on the installed version and end every session. |
|
|
143
143
|
| `agent-guild url` | Print the page link with the access token. |
|
|
144
144
|
| `agent-guild start` | Run the manager in the foreground. |
|
|
145
|
+
| `agent-guild doctor` | Diagnose the health of the installation and related tooling. |
|
|
146
|
+
| `agent-guild --version` | Print the installed Agent Guild version (`-v` or `version`). |
|
|
145
147
|
|
|
146
148
|
Sessions end when the manager stops or the computer restarts. tmux and herdr
|
|
147
149
|
sessions survive a manager stop.
|
package/bin/agent-guild.mjs
CHANGED
|
@@ -34,6 +34,11 @@ Commands:
|
|
|
34
34
|
restart Stop the session manager and start it again; ends every session
|
|
35
35
|
status Show whether the session manager is running
|
|
36
36
|
url Print the web page URL, including the access token
|
|
37
|
+
doctor Diagnose the health of the installation and related tooling
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
-v, --version Print the installed Agent Guild version
|
|
41
|
+
-h, --help Show this help message
|
|
37
42
|
|
|
38
43
|
Environment:
|
|
39
44
|
AGENT_GUILD_PORT Port for the local API (default 47821)
|
|
@@ -276,6 +281,10 @@ async function main() {
|
|
|
276
281
|
const flags = new Set(args.filter((a) => a.startsWith('-')));
|
|
277
282
|
const command = args.find((a) => !a.startsWith('-')) || 'open';
|
|
278
283
|
if (flags.has('-h') || flags.has('--help') || command === 'help') return usage();
|
|
284
|
+
if (flags.has('-v') || flags.has('--version') || command === 'version') {
|
|
285
|
+
console.log(VERSION);
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
279
288
|
|
|
280
289
|
switch (command) {
|
|
281
290
|
case 'open': return cmdOpen({ browser: !flags.has('--no-browser'), signIn: flags.has('--sign-in') });
|
|
@@ -303,6 +312,12 @@ async function main() {
|
|
|
303
312
|
case 'stop': return cmdStop();
|
|
304
313
|
case 'restart': return cmdRestart();
|
|
305
314
|
case 'status': return cmdStatus();
|
|
315
|
+
case 'doctor': {
|
|
316
|
+
const { runDoctor } = await import('../src/manager/doctor.mjs');
|
|
317
|
+
const ok = await runDoctor();
|
|
318
|
+
process.exitCode = ok ? 0 : 1;
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
306
321
|
case 'url': {
|
|
307
322
|
console.log(pageUrl(baseUrl(), loadOrCreateToken()));
|
|
308
323
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oddessentials/agent-guild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"manager": "node bin/agent-guild.mjs start",
|
|
32
32
|
"stop": "node bin/agent-guild.mjs stop",
|
|
33
33
|
"status": "node bin/agent-guild.mjs status",
|
|
34
|
+
"doctor": "node bin/agent-guild.mjs doctor",
|
|
34
35
|
"demo:build": "node docs/demo/build.mjs",
|
|
35
36
|
"demo:check": "node docs/demo/check.mjs",
|
|
36
37
|
"test": "node --test --test-concurrency=1 \"tests/*.test.mjs\""
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Installation and tooling health diagnostics for Agent Guild.
|
|
2
|
+
//
|
|
3
|
+
// Checks:
|
|
4
|
+
// - Node.js version (minimum 22 required)
|
|
5
|
+
// - Platform and OS details
|
|
6
|
+
// - Terminal PTY subsystem (node-pty native bindings, C library)
|
|
7
|
+
// - Data directory permissions and configuration files
|
|
8
|
+
// - Session manager health, port availability, and version alignment
|
|
9
|
+
// - Availability and versions of AI coding tools and terminal multiplexers
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import net from 'node:net';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_HOST,
|
|
17
|
+
VERSION,
|
|
18
|
+
dataDir,
|
|
19
|
+
ensureDataDir,
|
|
20
|
+
paths,
|
|
21
|
+
readRuntimeFile,
|
|
22
|
+
resolvePort,
|
|
23
|
+
} from './config.mjs';
|
|
24
|
+
import { ptyProblem, loadPty, glibcVersion } from './pty.mjs';
|
|
25
|
+
import { defaultBoot } from './launch.mjs';
|
|
26
|
+
import { startupState, startupSummary, unitPort } from './systemd-service.mjs';
|
|
27
|
+
import { resolveCommand } from './command-resolver.mjs';
|
|
28
|
+
import { probeVersion } from './versions.mjs';
|
|
29
|
+
|
|
30
|
+
export const MIN_NODE_MAJOR = 22;
|
|
31
|
+
|
|
32
|
+
const KNOWN_TOOLS = [
|
|
33
|
+
{
|
|
34
|
+
id: 'google',
|
|
35
|
+
name: 'Antigravity CLI',
|
|
36
|
+
command: 'agy',
|
|
37
|
+
versionArgs: ['--version'],
|
|
38
|
+
install: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
|
|
39
|
+
docs: 'https://antigravity.google/docs/cli/install',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'anthropic',
|
|
43
|
+
name: 'Claude Code',
|
|
44
|
+
command: 'claude',
|
|
45
|
+
versionArgs: ['--version'],
|
|
46
|
+
install: 'curl -fsSL https://claude.ai/install.sh | bash',
|
|
47
|
+
docs: 'https://code.claude.com/docs/en/setup',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'openai',
|
|
51
|
+
name: 'Codex CLI',
|
|
52
|
+
command: 'codex',
|
|
53
|
+
versionArgs: ['--version'],
|
|
54
|
+
install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
|
|
55
|
+
docs: 'https://github.com/openai/codex',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'xai',
|
|
59
|
+
name: 'Grok Build',
|
|
60
|
+
command: 'grok',
|
|
61
|
+
versionArgs: ['--version'],
|
|
62
|
+
install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
|
|
63
|
+
docs: 'https://docs.x.ai/build/overview',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'tmux',
|
|
67
|
+
name: 'tmux',
|
|
68
|
+
command: 'tmux',
|
|
69
|
+
versionArgs: ['-V'],
|
|
70
|
+
docs: 'https://github.com/tmux/tmux/wiki/Installing',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'herdr',
|
|
74
|
+
name: 'herdr',
|
|
75
|
+
command: 'herdr',
|
|
76
|
+
versionArgs: ['--version'],
|
|
77
|
+
docs: 'https://herdr.dev/docs/install/',
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** Check if port is available to listen on host. */
|
|
82
|
+
export function checkPortAvailable(port, host = DEFAULT_HOST) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const srv = net.createServer();
|
|
85
|
+
srv.once('error', (err) => {
|
|
86
|
+
if (err.code === 'EADDRINUSE') resolve(false);
|
|
87
|
+
else resolve(null);
|
|
88
|
+
});
|
|
89
|
+
srv.once('listening', () => {
|
|
90
|
+
srv.close(() => resolve(true));
|
|
91
|
+
});
|
|
92
|
+
srv.listen(port, host);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Check manager health via HTTP. */
|
|
97
|
+
async function checkHealth(url, timeoutMs = 1000) {
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(`${url}/api/v1/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
100
|
+
if (!res.ok) return null;
|
|
101
|
+
const body = await res.json();
|
|
102
|
+
return body?.name === 'agent-guild' ? body : null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Perform all diagnostic checks. All dependencies can be injected for testing.
|
|
110
|
+
*/
|
|
111
|
+
export async function diagnose({
|
|
112
|
+
nodeVersion = process.versions.node,
|
|
113
|
+
platform = process.platform,
|
|
114
|
+
arch = process.arch,
|
|
115
|
+
env = process.env,
|
|
116
|
+
dir = dataDir(),
|
|
117
|
+
port = resolvePort(),
|
|
118
|
+
fetchHealth = checkHealth,
|
|
119
|
+
testPortAvailable = checkPortAvailable,
|
|
120
|
+
checkPtyProblem = ptyProblem,
|
|
121
|
+
verifyLoadPty = loadPty,
|
|
122
|
+
tools = KNOWN_TOOLS,
|
|
123
|
+
} = {}) {
|
|
124
|
+
// 1. Node.js check
|
|
125
|
+
const nodeMajor = Number(nodeVersion.split('.')[0]);
|
|
126
|
+
const nodeOk = nodeMajor >= MIN_NODE_MAJOR;
|
|
127
|
+
const node = {
|
|
128
|
+
version: nodeVersion,
|
|
129
|
+
major: nodeMajor,
|
|
130
|
+
ok: nodeOk,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 2. Platform & OS
|
|
134
|
+
const glibc = platform === 'linux' ? glibcVersion() : null;
|
|
135
|
+
const sys = {
|
|
136
|
+
platform,
|
|
137
|
+
arch,
|
|
138
|
+
type: os.type(),
|
|
139
|
+
release: os.release(),
|
|
140
|
+
glibc,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// 3. PTY subsystem
|
|
144
|
+
let ptyOk = true;
|
|
145
|
+
let ptyError = checkPtyProblem({ platform });
|
|
146
|
+
if (ptyError) {
|
|
147
|
+
ptyOk = false;
|
|
148
|
+
} else {
|
|
149
|
+
try {
|
|
150
|
+
verifyLoadPty();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
ptyOk = false;
|
|
153
|
+
ptyError = err.message;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const pty = {
|
|
157
|
+
ok: ptyOk,
|
|
158
|
+
error: ptyError,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// 4. Data directory & configuration
|
|
162
|
+
let dirExists = false;
|
|
163
|
+
let dirWritable = false;
|
|
164
|
+
let writeError = null;
|
|
165
|
+
try {
|
|
166
|
+
ensureDataDir(dir);
|
|
167
|
+
dirExists = fs.existsSync(dir);
|
|
168
|
+
const probe = path.join(dir, `.doctor-probe-${process.pid}-${Date.now()}`);
|
|
169
|
+
fs.writeFileSync(probe, 'ok');
|
|
170
|
+
fs.rmSync(probe, { force: true });
|
|
171
|
+
dirWritable = true;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
writeError = err.message;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const tokenPath = path.join(dir, 'auth-token');
|
|
177
|
+
const hasToken = fs.existsSync(tokenPath);
|
|
178
|
+
|
|
179
|
+
let providersConfig = null;
|
|
180
|
+
const userProvidersPath = path.join(dir, 'providers.json');
|
|
181
|
+
if (fs.existsSync(userProvidersPath)) {
|
|
182
|
+
try {
|
|
183
|
+
JSON.parse(fs.readFileSync(userProvidersPath, 'utf8'));
|
|
184
|
+
providersConfig = { exists: true, valid: true };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
providersConfig = { exists: true, valid: false, error: err.message };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const storage = {
|
|
191
|
+
dir,
|
|
192
|
+
exists: dirExists,
|
|
193
|
+
writable: dirWritable,
|
|
194
|
+
writeError,
|
|
195
|
+
hasToken,
|
|
196
|
+
providersConfig,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// 5. Session manager & port
|
|
200
|
+
const runtime = readRuntimeFile();
|
|
201
|
+
const url = runtime?.url || `http://${DEFAULT_HOST}:${port}`;
|
|
202
|
+
const health = await fetchHealth(url);
|
|
203
|
+
let managerInfo = null;
|
|
204
|
+
|
|
205
|
+
if (health) {
|
|
206
|
+
let sessionsCount = null;
|
|
207
|
+
if (hasToken) {
|
|
208
|
+
try {
|
|
209
|
+
const token = fs.readFileSync(tokenPath, 'utf8').trim();
|
|
210
|
+
const res = await fetch(`${url}/api/v1/sessions`, {
|
|
211
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
212
|
+
signal: AbortSignal.timeout(1000),
|
|
213
|
+
});
|
|
214
|
+
if (res.ok) {
|
|
215
|
+
const body = await res.json();
|
|
216
|
+
sessionsCount = Array.isArray(body.sessions) ? body.sessions.length : null;
|
|
217
|
+
}
|
|
218
|
+
} catch { /* optional */ }
|
|
219
|
+
}
|
|
220
|
+
managerInfo = {
|
|
221
|
+
running: true,
|
|
222
|
+
url,
|
|
223
|
+
port,
|
|
224
|
+
pid: health.pid,
|
|
225
|
+
version: health.version,
|
|
226
|
+
versionMatch: health.version === VERSION,
|
|
227
|
+
sessionsCount,
|
|
228
|
+
};
|
|
229
|
+
} else {
|
|
230
|
+
const isFree = await testPortAvailable(port, DEFAULT_HOST);
|
|
231
|
+
managerInfo = {
|
|
232
|
+
running: false,
|
|
233
|
+
url,
|
|
234
|
+
port,
|
|
235
|
+
portFree: isFree === true,
|
|
236
|
+
portConflict: isFree === false,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Systemd startup check on Linux
|
|
241
|
+
let startup = null;
|
|
242
|
+
const boot = defaultBoot();
|
|
243
|
+
if (boot) {
|
|
244
|
+
try {
|
|
245
|
+
const read = await boot.read();
|
|
246
|
+
if (read?.enabled) {
|
|
247
|
+
const uPort = unitPort(await boot.text().catch(() => null));
|
|
248
|
+
startup = startupSummary(startupState(read.show, { port: uPort }));
|
|
249
|
+
}
|
|
250
|
+
} catch { /* systemd not accessible */ }
|
|
251
|
+
}
|
|
252
|
+
managerInfo.startup = startup;
|
|
253
|
+
|
|
254
|
+
// 6. AI Assistants & Tools
|
|
255
|
+
const toolResults = [];
|
|
256
|
+
for (const tool of tools) {
|
|
257
|
+
const resolved = resolveCommand(tool.command, env, platform);
|
|
258
|
+
if (resolved) {
|
|
259
|
+
const spec = { command: resolved, args: tool.versionArgs || ['--version'] };
|
|
260
|
+
const probe = await probeVersion(spec, { env, timeoutMs: 3000 });
|
|
261
|
+
toolResults.push({
|
|
262
|
+
...tool,
|
|
263
|
+
found: true,
|
|
264
|
+
path: resolved,
|
|
265
|
+
version: probe.version,
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
toolResults.push({
|
|
269
|
+
...tool,
|
|
270
|
+
found: false,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const fatalIssues = [
|
|
276
|
+
!nodeOk && 'Node.js version is below requirement (>= 22)',
|
|
277
|
+
!ptyOk && (ptyError || 'node-pty native bindings failed to load'),
|
|
278
|
+
!dirWritable && `Data directory is not writable (${writeError})`,
|
|
279
|
+
managerInfo.portConflict && `Port ${port} is in use by another process`,
|
|
280
|
+
providersConfig && !providersConfig.valid && `providers.json contains invalid JSON: ${providersConfig.error}`,
|
|
281
|
+
].filter(Boolean);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
version: VERSION,
|
|
285
|
+
node,
|
|
286
|
+
sys,
|
|
287
|
+
pty,
|
|
288
|
+
storage,
|
|
289
|
+
manager: managerInfo,
|
|
290
|
+
tools: toolResults,
|
|
291
|
+
fatalIssues,
|
|
292
|
+
healthy: fatalIssues.length === 0,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Format the diagnostics object as human-readable markdown / terminal text.
|
|
298
|
+
*/
|
|
299
|
+
export function formatDiagnostics(diag) {
|
|
300
|
+
const lines = [];
|
|
301
|
+
lines.push(`Agent Guild Doctor (v${diag.version})`);
|
|
302
|
+
lines.push('');
|
|
303
|
+
|
|
304
|
+
// 1. Environment
|
|
305
|
+
lines.push('Environment:');
|
|
306
|
+
if (diag.node.ok) {
|
|
307
|
+
lines.push(` ✔ Node.js v${diag.node.version} (meets requirement >= ${MIN_NODE_MAJOR})`);
|
|
308
|
+
} else {
|
|
309
|
+
lines.push(` ✖ Node.js v${diag.node.version} (unsupported; Node.js ${MIN_NODE_MAJOR} or newer is required)`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const glibcNote = diag.sys.glibc ? ` · glibc ${diag.sys.glibc}` : '';
|
|
313
|
+
lines.push(` ✔ Platform: ${diag.sys.platform} (${diag.sys.arch} · ${diag.sys.type} ${diag.sys.release}${glibcNote})`);
|
|
314
|
+
|
|
315
|
+
if (diag.pty.ok) {
|
|
316
|
+
lines.push(' ✔ Terminal subsystem (node-pty): functional');
|
|
317
|
+
} else {
|
|
318
|
+
lines.push(' ✖ Terminal subsystem (node-pty): problem detected');
|
|
319
|
+
for (const msgLine of String(diag.pty.error || '').split('\n')) {
|
|
320
|
+
lines.push(` ${msgLine}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
lines.push('');
|
|
324
|
+
|
|
325
|
+
// 2. Data Directory & Files
|
|
326
|
+
lines.push('Data Directory & Configuration:');
|
|
327
|
+
lines.push(` ✔ Location: ${diag.storage.dir}`);
|
|
328
|
+
if (diag.storage.writable) {
|
|
329
|
+
lines.push(' ✔ Permissions: directory is writable');
|
|
330
|
+
} else {
|
|
331
|
+
lines.push(` ✖ Permissions: cannot write to data directory (${diag.storage.writeError})`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (diag.storage.hasToken) {
|
|
335
|
+
lines.push(' ✔ Access token: present');
|
|
336
|
+
} else {
|
|
337
|
+
lines.push(' ℹ Access token: not yet generated (created automatically on first launch)');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (diag.storage.providersConfig) {
|
|
341
|
+
if (diag.storage.providersConfig.valid) {
|
|
342
|
+
lines.push(' ✔ Custom tools (providers.json): valid');
|
|
343
|
+
} else {
|
|
344
|
+
lines.push(` ✖ Custom tools (providers.json): invalid syntax (${diag.storage.providersConfig.error})`);
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
lines.push(' ✔ Custom tools (providers.json): default configuration');
|
|
348
|
+
}
|
|
349
|
+
lines.push('');
|
|
350
|
+
|
|
351
|
+
// 3. Session Manager
|
|
352
|
+
lines.push('Session Manager:');
|
|
353
|
+
if (diag.manager.running) {
|
|
354
|
+
lines.push(` ✔ Status: running at ${diag.manager.url} (pid ${diag.manager.pid})`);
|
|
355
|
+
if (diag.manager.versionMatch) {
|
|
356
|
+
lines.push(` ✔ Version: v${diag.manager.version} (matches installed package)`);
|
|
357
|
+
} else {
|
|
358
|
+
lines.push(` ! Version: running v${diag.manager.version}, installed v${diag.version} (run "agent-guild stop" then start again to update)`);
|
|
359
|
+
}
|
|
360
|
+
if (typeof diag.manager.sessionsCount === 'number') {
|
|
361
|
+
lines.push(` ℹ Active sessions: ${diag.manager.sessionsCount}`);
|
|
362
|
+
}
|
|
363
|
+
} else if (diag.manager.portConflict) {
|
|
364
|
+
lines.push(` ✖ Status: not running, but port ${diag.manager.port} is in use by another process`);
|
|
365
|
+
} else {
|
|
366
|
+
lines.push(` ℹ Status: not running (port ${diag.manager.port} is available)`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (diag.manager.startup) {
|
|
370
|
+
lines.push(` ✔ Startup service: ${diag.manager.startup}`);
|
|
371
|
+
}
|
|
372
|
+
lines.push('');
|
|
373
|
+
|
|
374
|
+
// 4. Tools
|
|
375
|
+
lines.push('Coding Assistants & Multiplexers:');
|
|
376
|
+
for (const t of diag.tools) {
|
|
377
|
+
if (t.found) {
|
|
378
|
+
const v = t.version ? `v${t.version}` : 'installed';
|
|
379
|
+
lines.push(` ✔ ${t.name} (${t.command}): ${v} (${t.path})`);
|
|
380
|
+
} else {
|
|
381
|
+
const tip = t.install ? ` · install: ${t.install}` : t.docs ? ` · ${t.docs}` : '';
|
|
382
|
+
lines.push(` ℹ ${t.name} (${t.command}): not found on PATH${tip}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
lines.push('');
|
|
386
|
+
|
|
387
|
+
// Summary
|
|
388
|
+
if (diag.healthy) {
|
|
389
|
+
lines.push('Doctor found no fatal problems.');
|
|
390
|
+
} else {
|
|
391
|
+
const count = diag.fatalIssues.length;
|
|
392
|
+
lines.push(`Doctor found ${count} fatal problem${count === 1 ? '' : 's'} (see ✖ above).`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Run doctor command and output to console. Returns true if healthy, false if fatal issues found.
|
|
400
|
+
*/
|
|
401
|
+
export async function runDoctor(opts = {}) {
|
|
402
|
+
const out = opts.log || console.log;
|
|
403
|
+
const diag = await diagnose(opts);
|
|
404
|
+
out(formatDiagnostics(diag));
|
|
405
|
+
return diag.healthy;
|
|
406
|
+
}
|