@yeaft/webchat-agent 0.1.209 → 0.1.210

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/cli.js CHANGED
@@ -19,7 +19,9 @@ const subArgs = args.slice(1);
19
19
  // Service management subcommands
20
20
  const SERVICE_COMMANDS = ['install', 'uninstall', 'start', 'stop', 'restart', 'status', 'logs'];
21
21
 
22
- if (command === 'upgrade') {
22
+ if (command === 'doctor') {
23
+ handleDoctorCommand();
24
+ } else if (command === 'upgrade') {
23
25
  upgrade();
24
26
  } else if (command === '--version' || command === '-v') {
25
27
  console.log(pkg.version);
@@ -45,6 +47,7 @@ function printHelp() {
45
47
  yeaft-agent restart Restart installed service
46
48
  yeaft-agent status Show service status
47
49
  yeaft-agent logs View service logs (follow mode)
50
+ yeaft-agent doctor Diagnose service configuration
48
51
  yeaft-agent upgrade Upgrade to latest version
49
52
  yeaft-agent --version Show version
50
53
 
@@ -82,6 +85,11 @@ async function handleServiceCommand(command, args) {
82
85
  }
83
86
  }
84
87
 
88
+ async function handleDoctorCommand() {
89
+ const { doctor } = await import('./service.js');
90
+ doctor();
91
+ }
92
+
85
93
  function parseAndStart(args) {
86
94
  // Parse CLI flags → set environment variables (env vars take precedence over flags)
87
95
  for (let i = 0; i < args.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.209",
3
+ "version": "0.1.210",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Service — doctor command
3
+ * Diagnoses service configuration issues: checks paths, service status, version consistency.
4
+ */
5
+ import { execSync } from 'child_process';
6
+ import { existsSync, readFileSync, statSync } from 'fs';
7
+ import { join, dirname } from 'path';
8
+ import { platform, homedir } from 'os';
9
+ import { fileURLToPath } from 'url';
10
+ import { SERVICE_NAME } from './config.js';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+
14
+ // ── Platform-specific service config paths ─────────────────────────────────
15
+
16
+ function getMacPlistPath() {
17
+ return join(homedir(), 'Library', 'LaunchAgents', 'com.yeaft.agent.plist');
18
+ }
19
+
20
+ function getLinuxServicePath() {
21
+ return join(homedir(), '.config', 'systemd', 'user', `${SERVICE_NAME}.service`);
22
+ }
23
+
24
+ function getWindowsEcosystemPath() {
25
+ const appData = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
26
+ return join(appData, SERVICE_NAME, 'ecosystem.config.cjs');
27
+ }
28
+
29
+ // ── Parse paths from service config files ──────────────────────────────────
30
+
31
+ /**
32
+ * Parse node path and cli path from macOS launchd plist.
33
+ * ProgramArguments is an array: [nodePath, cliPath]
34
+ */
35
+ function parseMacPaths(plistPath) {
36
+ const content = readFileSync(plistPath, 'utf-8');
37
+ const args = [];
38
+ // Match <string> values inside <array> after ProgramArguments
39
+ const arrayMatch = content.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
40
+ if (arrayMatch) {
41
+ const stringRegex = /<string>(.*?)<\/string>/g;
42
+ let m;
43
+ while ((m = stringRegex.exec(arrayMatch[1])) !== null) {
44
+ args.push(m[1]);
45
+ }
46
+ }
47
+ return { nodePath: args[0] || null, cliPath: args[1] || null };
48
+ }
49
+
50
+ /**
51
+ * Parse node path and cli path from Linux systemd unit.
52
+ * ExecStart=<nodePath> <cliPath>
53
+ */
54
+ function parseLinuxPaths(servicePath) {
55
+ const content = readFileSync(servicePath, 'utf-8');
56
+ const match = content.match(/^ExecStart=(.+)$/m);
57
+ if (match) {
58
+ const parts = match[1].trim().split(/\s+/);
59
+ return { nodePath: parts[0] || null, cliPath: parts[1] || null };
60
+ }
61
+ return { nodePath: null, cliPath: null };
62
+ }
63
+
64
+ /**
65
+ * Parse node path and cli path from Windows pm2 ecosystem config.
66
+ * interpreter: '<nodePath>', script: '<cliPath>'
67
+ */
68
+ function parseWindowsPaths(ecoPath) {
69
+ const content = readFileSync(ecoPath, 'utf-8');
70
+ const interpreterMatch = content.match(/interpreter:\s*'([^']+)'/);
71
+ const scriptMatch = content.match(/script:\s*'([^']+)'/);
72
+ return {
73
+ nodePath: interpreterMatch ? interpreterMatch[1].replace(/\\\\/g, '\\') : null,
74
+ cliPath: scriptMatch ? scriptMatch[1].replace(/\\\\/g, '\\') : null,
75
+ };
76
+ }
77
+
78
+ // ── Check helpers ──────────────────────────────────────────────────────────
79
+
80
+ function isExecutable(filePath) {
81
+ try {
82
+ const stat = statSync(filePath);
83
+ if (platform() === 'win32') {
84
+ return stat.isFile();
85
+ }
86
+ // On Unix, check if any execute bit is set
87
+ return stat.isFile() && (stat.mode & 0o111) !== 0;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ function getNodeVersion(nodePath) {
94
+ try {
95
+ return execSync(`"${nodePath}" --version`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ function getCliVersion(nodePath, cliPath) {
102
+ try {
103
+ return execSync(`"${nodePath}" "${cliPath}" --version`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function getCurrentNodePath() {
110
+ return process.execPath;
111
+ }
112
+
113
+ function getCurrentNodeVersion() {
114
+ return process.version;
115
+ }
116
+
117
+ // ── Service status checks ──────────────────────────────────────────────────
118
+
119
+ function getMacServiceStatus() {
120
+ try {
121
+ const output = execSync('launchctl list | grep com.yeaft.agent', {
122
+ encoding: 'utf-8',
123
+ stdio: ['pipe', 'pipe', 'pipe'],
124
+ });
125
+ if (output.trim()) {
126
+ const parts = output.trim().split(/\s+/);
127
+ const pid = parts[0];
128
+ if (pid !== '-') {
129
+ return { running: true, pid };
130
+ }
131
+ return { running: false, pid: null };
132
+ }
133
+ return { running: false, pid: null };
134
+ } catch {
135
+ return { running: false, pid: null };
136
+ }
137
+ }
138
+
139
+ function getLinuxServiceStatus() {
140
+ try {
141
+ const output = execSync(`systemctl --user is-active ${SERVICE_NAME}`, {
142
+ encoding: 'utf-8',
143
+ stdio: ['pipe', 'pipe', 'pipe'],
144
+ }).trim();
145
+ if (output === 'active') {
146
+ // Try to get PID
147
+ let pid = null;
148
+ try {
149
+ pid = execSync(`systemctl --user show ${SERVICE_NAME} --property=MainPID --value`, {
150
+ encoding: 'utf-8',
151
+ stdio: ['pipe', 'pipe', 'pipe'],
152
+ }).trim();
153
+ if (pid === '0') pid = null;
154
+ } catch {}
155
+ return { running: true, pid };
156
+ }
157
+ return { running: false, pid: null };
158
+ } catch {
159
+ return { running: false, pid: null };
160
+ }
161
+ }
162
+
163
+ function getWindowsServiceStatus() {
164
+ try {
165
+ const output = execSync('pm2 jlist', {
166
+ encoding: 'utf-8',
167
+ stdio: ['pipe', 'pipe', 'pipe'],
168
+ });
169
+ const apps = JSON.parse(output);
170
+ const app = Array.isArray(apps) && apps.find(a => a.name === 'yeaft-agent');
171
+ if (app) {
172
+ const running = app.pm2_env && app.pm2_env.status === 'online';
173
+ const pid = running ? app.pid : null;
174
+ return { running, pid: pid ? String(pid) : null };
175
+ }
176
+ return { running: false, pid: null };
177
+ } catch {
178
+ return { running: false, pid: null };
179
+ }
180
+ }
181
+
182
+ // ── Tilde shorthand for display ────────────────────────────────────────────
183
+
184
+ function tildeify(filePath) {
185
+ const home = homedir();
186
+ if (filePath && filePath.startsWith(home)) {
187
+ return '~' + filePath.slice(home.length);
188
+ }
189
+ return filePath;
190
+ }
191
+
192
+ // ── Main doctor logic ──────────────────────────────────────────────────────
193
+
194
+ export function doctor() {
195
+ const os = platform();
196
+ let configPath = null;
197
+ let parsePaths = null;
198
+ let getServiceStatus = null;
199
+
200
+ console.log('');
201
+
202
+ // 1. Determine platform and config path
203
+ if (os === 'darwin') {
204
+ configPath = getMacPlistPath();
205
+ parsePaths = parseMacPaths;
206
+ getServiceStatus = getMacServiceStatus;
207
+ } else if (os === 'linux') {
208
+ configPath = getLinuxServicePath();
209
+ parsePaths = parseLinuxPaths;
210
+ getServiceStatus = getLinuxServiceStatus;
211
+ } else if (os === 'win32') {
212
+ configPath = getWindowsEcosystemPath();
213
+ parsePaths = parseWindowsPaths;
214
+ getServiceStatus = getWindowsServiceStatus;
215
+ } else {
216
+ console.log(`\u26a0\ufe0f Unsupported platform: ${os}`);
217
+ console.log(` The doctor command supports macOS, Linux, and Windows.`);
218
+ console.log('');
219
+ process.exit(1);
220
+ }
221
+
222
+ // 2. Check if service config exists
223
+ if (!existsSync(configPath)) {
224
+ console.log(`\u26a0\ufe0f No service configuration found.`);
225
+ console.log(` Run 'yeaft-agent install --server <url> --name <name> --secret <secret>' to set up.`);
226
+ console.log('');
227
+ return;
228
+ }
229
+
230
+ console.log(`\u2705 Service installed: ${tildeify(configPath)}`);
231
+
232
+ let hasErrors = false;
233
+
234
+ // 3. Parse paths from config
235
+ const { nodePath, cliPath } = parsePaths(configPath);
236
+
237
+ // 4. Check node path
238
+ if (!nodePath) {
239
+ console.log(`\u274c Node path: not found in service config`);
240
+ hasErrors = true;
241
+ } else if (!existsSync(nodePath)) {
242
+ console.log(`\u274c Node path invalid: ${nodePath} (file not found)`);
243
+ console.log(` Current node: ${getCurrentNodePath()} (${getCurrentNodeVersion()})`);
244
+ hasErrors = true;
245
+ } else if (!isExecutable(nodePath)) {
246
+ console.log(`\u274c Node path not executable: ${nodePath}`);
247
+ console.log(` Current node: ${getCurrentNodePath()} (${getCurrentNodeVersion()})`);
248
+ hasErrors = true;
249
+ } else {
250
+ const nodeVersion = getNodeVersion(nodePath);
251
+ console.log(`\u2705 Node path valid: ${nodePath} (${nodeVersion || 'unknown version'})`);
252
+
253
+ // 4a. Version consistency check
254
+ const currentVersion = getCurrentNodeVersion();
255
+ if (nodeVersion && nodeVersion !== currentVersion) {
256
+ console.log(` \u26a0\ufe0f Version mismatch: service uses ${nodeVersion}, current terminal uses ${currentVersion}`);
257
+ }
258
+ }
259
+
260
+ // 5. Check cli path
261
+ if (!cliPath) {
262
+ console.log(`\u274c CLI path: not found in service config`);
263
+ hasErrors = true;
264
+ } else if (!existsSync(cliPath)) {
265
+ console.log(`\u274c CLI path invalid: ${cliPath} (file not found)`);
266
+ hasErrors = true;
267
+ } else {
268
+ // Try to get CLI version
269
+ let versionStr = '';
270
+ if (nodePath && existsSync(nodePath)) {
271
+ const cliVersion = getCliVersion(nodePath, cliPath);
272
+ if (cliVersion) versionStr = ` (v${cliVersion})`;
273
+ }
274
+ console.log(`\u2705 CLI path valid: ${tildeify(cliPath)}${versionStr}`);
275
+ }
276
+
277
+ // 6. Check service status
278
+ const status = getServiceStatus();
279
+ if (status.running) {
280
+ console.log(`\u2705 Service running: PID ${status.pid}`);
281
+ } else {
282
+ console.log(`\u274c Service not running`);
283
+ hasErrors = true;
284
+ }
285
+
286
+ // 7. Summary
287
+ console.log('');
288
+ if (hasErrors) {
289
+ console.log('Fix: Run the following commands:');
290
+ console.log(' npm install -g @yeaft/webchat-agent');
291
+ console.log(' yeaft-agent install --server <your-server-url> --name <your-agent-name> --secret <your-secret>');
292
+ } else {
293
+ console.log('All checks passed.');
294
+ }
295
+ console.log('');
296
+ }
package/service/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { getSystemdServicePath, linuxInstall, linuxUninstall, linuxStart, linuxStop, linuxRestart, linuxStatus, linuxLogs } from './linux.js';
13
13
  import { getLaunchdPlistPath, macInstall, macUninstall, macStart, macStop, macRestart, macStatus, macLogs } from './macos.js';
14
14
  import { winInstall, winUninstall, winStart, winStop, winRestart, winStatus, winLogs } from './windows.js';
15
+ import { doctor } from './doctor.js';
15
16
 
16
17
  export {
17
18
  getConfigDir, getLogDir, getConfigPath,
@@ -97,3 +98,5 @@ export function logs() {
97
98
  else if (os === 'darwin') macLogs();
98
99
  else if (os === 'win32') winLogs();
99
100
  }
101
+
102
+ export { doctor };
package/service.js CHANGED
@@ -22,5 +22,6 @@ export {
22
22
  stop,
23
23
  restart,
24
24
  status,
25
- logs
25
+ logs,
26
+ doctor
26
27
  } from './service/index.js';