@yeaft/webchat-agent 1.0.24 → 1.0.27
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 +25 -14
- package/connection/index.js +6 -3
- package/connection/message-router.js +3 -1
- package/index.js +5 -3
- package/package.json +1 -1
- package/service/config.js +90 -15
- package/service/index.js +59 -39
- package/service/linux.js +48 -37
- package/service/macos.js +35 -30
- package/service/windows.js +46 -39
- package/service.js +10 -0
package/cli.js
CHANGED
|
@@ -64,35 +64,40 @@ function printHelp() {
|
|
|
64
64
|
Usage:
|
|
65
65
|
yeaft-agent [options] Run agent in foreground
|
|
66
66
|
yeaft-agent install [options] Install as system service
|
|
67
|
-
yeaft-agent uninstall
|
|
68
|
-
yeaft-agent start
|
|
69
|
-
yeaft-agent stop
|
|
70
|
-
yeaft-agent restart
|
|
71
|
-
yeaft-agent status
|
|
72
|
-
yeaft-agent logs
|
|
67
|
+
yeaft-agent uninstall [options] Remove system service
|
|
68
|
+
yeaft-agent start [options] Start installed service
|
|
69
|
+
yeaft-agent stop [options] Stop installed service
|
|
70
|
+
yeaft-agent restart [options] Restart installed service
|
|
71
|
+
yeaft-agent status [options] Show service status
|
|
72
|
+
yeaft-agent logs [options] View service logs (follow mode)
|
|
73
73
|
yeaft-agent doctor Diagnose service configuration
|
|
74
74
|
yeaft-agent llm <command> Configure local Yeaft LLM providers/models
|
|
75
75
|
yeaft-agent upgrade Upgrade to latest version
|
|
76
76
|
yeaft-agent --version Show version
|
|
77
77
|
|
|
78
78
|
Options:
|
|
79
|
+
--instance <id> Local service instance id (default: default)
|
|
79
80
|
--server <url> WebSocket server URL (default: ws://localhost:3456)
|
|
80
81
|
--name <name> Agent display name (default: Worker-{platform}-{pid})
|
|
81
82
|
--secret <secret> Agent secret for authentication
|
|
82
83
|
--work-dir <dir> Default working directory (default: cwd)
|
|
84
|
+
--yeaft-dir <dir> Yeaft data directory for this instance
|
|
83
85
|
--auto-upgrade Check for updates on startup
|
|
84
86
|
|
|
85
87
|
Environment variables (alternative to flags):
|
|
88
|
+
YEAFT_AGENT_INSTANCE Local service instance id
|
|
86
89
|
SERVER_URL WebSocket server URL
|
|
87
90
|
AGENT_NAME Agent display name
|
|
88
91
|
AGENT_SECRET Agent secret
|
|
89
92
|
WORK_DIR Working directory
|
|
93
|
+
YEAFT_DIR Yeaft data directory
|
|
90
94
|
|
|
91
95
|
Examples:
|
|
92
96
|
yeaft-agent --server wss://your-server.com --name my-worker --secret xxx
|
|
93
97
|
yeaft-agent install --server wss://your-server.com --name my-worker --secret xxx
|
|
94
|
-
yeaft-agent
|
|
95
|
-
yeaft-agent
|
|
98
|
+
yeaft-agent install --instance second --server wss://your-server.com --name my-worker-2 --secret xxx
|
|
99
|
+
yeaft-agent status --instance second
|
|
100
|
+
yeaft-agent logs --instance second
|
|
96
101
|
`);
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -381,12 +386,12 @@ async function handleServiceCommand(command, args) {
|
|
|
381
386
|
const service = await import('./service.js');
|
|
382
387
|
switch (command) {
|
|
383
388
|
case 'install': service.install(args); break;
|
|
384
|
-
case 'uninstall': service.uninstall(); break;
|
|
385
|
-
case 'start': service.start(); break;
|
|
386
|
-
case 'stop': service.stop(); break;
|
|
387
|
-
case 'restart': service.restart(); break;
|
|
388
|
-
case 'status': service.status(); break;
|
|
389
|
-
case 'logs': service.logs(); break;
|
|
389
|
+
case 'uninstall': service.uninstall(args); break;
|
|
390
|
+
case 'start': service.start(args); break;
|
|
391
|
+
case 'stop': service.stop(args); break;
|
|
392
|
+
case 'restart': service.restart(args); break;
|
|
393
|
+
case 'status': service.status(args); break;
|
|
394
|
+
case 'logs': service.logs(args); break;
|
|
390
395
|
}
|
|
391
396
|
}
|
|
392
397
|
|
|
@@ -402,6 +407,9 @@ function parseAndStart(args) {
|
|
|
402
407
|
const next = args[i + 1];
|
|
403
408
|
|
|
404
409
|
switch (arg) {
|
|
410
|
+
case '--instance':
|
|
411
|
+
if (next) { process.env.YEAFT_AGENT_INSTANCE = process.env.YEAFT_AGENT_INSTANCE || next; i++; }
|
|
412
|
+
break;
|
|
405
413
|
case '--server':
|
|
406
414
|
if (next) { process.env.SERVER_URL = process.env.SERVER_URL || next; i++; }
|
|
407
415
|
break;
|
|
@@ -414,6 +422,9 @@ function parseAndStart(args) {
|
|
|
414
422
|
case '--work-dir':
|
|
415
423
|
if (next) { process.env.WORK_DIR = process.env.WORK_DIR || next; i++; }
|
|
416
424
|
break;
|
|
425
|
+
case '--yeaft-dir':
|
|
426
|
+
if (next) { process.env.YEAFT_DIR = process.env.YEAFT_DIR || next; i++; }
|
|
427
|
+
break;
|
|
417
428
|
case '--auto-upgrade':
|
|
418
429
|
checkForUpdates();
|
|
419
430
|
break;
|
package/connection/index.js
CHANGED
|
@@ -5,12 +5,15 @@ import { startAgentHeartbeat, stopAgentHeartbeat, scheduleReconnect } from './he
|
|
|
5
5
|
import { handleMessage } from './message-router.js';
|
|
6
6
|
|
|
7
7
|
export function connect() {
|
|
8
|
-
// Don't include secret in URL - it will be sent via WebSocket message after connection
|
|
9
|
-
//
|
|
8
|
+
// Don't include secret in URL - it will be sent via WebSocket message after connection.
|
|
9
|
+
// instanceId is the stable local service identity; agentName is display-only.
|
|
10
|
+
// Old configs without instanceId still use agentName for backward-compatible identity.
|
|
11
|
+
const instanceId = ctx.CONFIG.instanceId || ctx.CONFIG.agentName;
|
|
10
12
|
const params = new URLSearchParams({
|
|
11
13
|
type: 'agent',
|
|
12
|
-
id:
|
|
14
|
+
id: instanceId,
|
|
13
15
|
name: ctx.CONFIG.agentName,
|
|
16
|
+
instanceId,
|
|
14
17
|
workDir: ctx.CONFIG.workDir,
|
|
15
18
|
capabilities: ctx.agentCapabilities.join(',')
|
|
16
19
|
});
|
|
@@ -58,11 +58,13 @@ export async function handleMessage(msg) {
|
|
|
58
58
|
console.log('[WS] Server accepts plaintext, disabling outbound encryption');
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// 只保存基本配置。instanceId 是本地服务实例身份;agentName 只用于展示。
|
|
62
62
|
ctx.saveConfig({
|
|
63
|
+
instanceId: ctx.CONFIG.instanceId,
|
|
63
64
|
serverUrl: ctx.CONFIG.serverUrl,
|
|
64
65
|
agentName: ctx.CONFIG.agentName,
|
|
65
66
|
workDir: ctx.CONFIG.workDir,
|
|
67
|
+
yeaftDir: ctx.CONFIG.yeaftDir,
|
|
66
68
|
reconnectInterval: ctx.CONFIG.reconnectInterval
|
|
67
69
|
// 不保存 agentSecret 到配置文件(安全考虑)
|
|
68
70
|
});
|
package/index.js
CHANGED
|
@@ -2,14 +2,14 @@ import { assertNodeVersion } from './check-node-version.js';
|
|
|
2
2
|
assertNodeVersion({ component: '@yeaft/webchat-agent' });
|
|
3
3
|
|
|
4
4
|
import 'dotenv/config';
|
|
5
|
-
import { platform
|
|
5
|
+
import { platform } from 'os';
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, chmodSync, readdirSync } from 'fs';
|
|
7
7
|
import { join, dirname } from 'path';
|
|
8
8
|
import { exec } from 'child_process';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import ctx from './context.js';
|
|
12
|
-
import { getConfigPath, loadServiceConfig } from './service.js';
|
|
12
|
+
import { DEFAULT_INSTANCE_ID, getDefaultYeaftDir, validateInstanceId, getConfigPath, loadServiceConfig } from './service.js';
|
|
13
13
|
import { loadNodePty } from './terminal.js';
|
|
14
14
|
import { connect } from './connection.js';
|
|
15
15
|
import { loadMcpServers } from './mcp.js';
|
|
@@ -60,6 +60,7 @@ function saveConfig(config) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const fileConfig = loadConfig();
|
|
63
|
+
const INSTANCE_ID = validateInstanceId(process.env.YEAFT_AGENT_INSTANCE || fileConfig.instanceId || DEFAULT_INSTANCE_ID);
|
|
63
64
|
|
|
64
65
|
// task-fix (5-bugs): the Yeaft web-bridge reads `ctx.CONFIG.yeaftDir`
|
|
65
66
|
// for every group / VP / memory operation. If unset, `path.join(undefined, …)`
|
|
@@ -67,7 +68,7 @@ const fileConfig = loadConfig();
|
|
|
67
68
|
// and the UI surfaces "群组操作失败: …" with a raw node error. Resolve the
|
|
68
69
|
// default (`~/.yeaft`) here and make sure the directory exists before the
|
|
69
70
|
// WebSocket connection goes live, so downstream code can assume a real path.
|
|
70
|
-
const YEAFT_DIR = process.env.YEAFT_DIR || fileConfig.yeaftDir ||
|
|
71
|
+
const YEAFT_DIR = process.env.YEAFT_DIR || fileConfig.yeaftDir || getDefaultYeaftDir(INSTANCE_ID);
|
|
71
72
|
try {
|
|
72
73
|
if (!existsSync(YEAFT_DIR)) {
|
|
73
74
|
mkdirSync(YEAFT_DIR, { recursive: true });
|
|
@@ -78,6 +79,7 @@ try {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
const CONFIG = {
|
|
82
|
+
instanceId: INSTANCE_ID,
|
|
81
83
|
serverUrl: process.env.SERVER_URL || fileConfig.serverUrl,
|
|
82
84
|
agentName: process.env.AGENT_NAME || fileConfig.agentName,
|
|
83
85
|
workDir: process.env.WORK_DIR || fileConfig.workDir || process.cwd(),
|
package/package.json
CHANGED
package/service/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
2
|
* Service — shared configuration and utility functions
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
export const SERVICE_NAME = 'yeaft-agent';
|
|
11
|
+
export const DEFAULT_INSTANCE_ID = 'default';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Load .env file from agent directory (or cwd) into process.env
|
|
@@ -40,36 +41,98 @@ function loadDotenv() {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
export function normalizeInstanceId(instanceId) {
|
|
45
|
+
const raw = String(instanceId || '').trim();
|
|
46
|
+
return raw || DEFAULT_INSTANCE_ID;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isDefaultInstance(instanceId) {
|
|
50
|
+
return normalizeInstanceId(instanceId) === DEFAULT_INSTANCE_ID;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateInstanceId(instanceId) {
|
|
54
|
+
const normalized = normalizeInstanceId(instanceId);
|
|
55
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
56
|
+
throw new Error('Instance id may only contain letters, numbers, dot, underscore, or dash');
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getInstanceIdFromArgs(args = [], env = process.env) {
|
|
62
|
+
let instanceId = env.YEAFT_AGENT_INSTANCE || '';
|
|
63
|
+
for (let i = 0; i < args.length; i++) {
|
|
64
|
+
const arg = args[i];
|
|
65
|
+
const next = args[i + 1];
|
|
66
|
+
if (arg === '--instance' && next) {
|
|
67
|
+
instanceId = next;
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return validateInstanceId(instanceId || DEFAULT_INSTANCE_ID);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getServiceName(instanceId = DEFAULT_INSTANCE_ID) {
|
|
75
|
+
const normalized = validateInstanceId(instanceId);
|
|
76
|
+
return isDefaultInstance(normalized) ? SERVICE_NAME : `${SERVICE_NAME}@${normalized}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getPm2AppName(instanceId = DEFAULT_INSTANCE_ID) {
|
|
80
|
+
const normalized = validateInstanceId(instanceId);
|
|
81
|
+
return isDefaultInstance(normalized) ? SERVICE_NAME : `${SERVICE_NAME}-${normalized}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getLaunchdLabel(instanceId = DEFAULT_INSTANCE_ID) {
|
|
85
|
+
const normalized = validateInstanceId(instanceId);
|
|
86
|
+
return isDefaultInstance(normalized) ? 'com.yeaft.agent' : `com.yeaft.agent.${normalized}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getDefaultYeaftDir(instanceId = DEFAULT_INSTANCE_ID) {
|
|
90
|
+
const normalized = validateInstanceId(instanceId);
|
|
91
|
+
return isDefaultInstance(normalized)
|
|
92
|
+
? join(homedir(), '.yeaft')
|
|
93
|
+
: join(homedir(), '.yeaft', 'instances', normalized);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getBaseConfigDir() {
|
|
45
97
|
if (platform() === 'win32') {
|
|
46
98
|
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), SERVICE_NAME);
|
|
47
99
|
}
|
|
48
100
|
return join(homedir(), '.config', SERVICE_NAME);
|
|
49
101
|
}
|
|
50
102
|
|
|
51
|
-
|
|
52
|
-
|
|
103
|
+
// Standard config/log directory per platform. The default instance keeps the
|
|
104
|
+
// historical paths for compatibility; named instances live under instances/<id>.
|
|
105
|
+
export function getConfigDir(instanceId = process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID) {
|
|
106
|
+
const normalized = validateInstanceId(instanceId);
|
|
107
|
+
const base = getBaseConfigDir();
|
|
108
|
+
return isDefaultInstance(normalized) ? base : join(base, 'instances', normalized);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getLogDir(instanceId = process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID) {
|
|
112
|
+
return join(getConfigDir(instanceId), 'logs');
|
|
53
113
|
}
|
|
54
114
|
|
|
55
|
-
export function getConfigPath() {
|
|
56
|
-
return join(getConfigDir(), 'config.json');
|
|
115
|
+
export function getConfigPath(instanceId = process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID) {
|
|
116
|
+
return join(getConfigDir(instanceId), 'config.json');
|
|
57
117
|
}
|
|
58
118
|
|
|
59
119
|
/** Save agent configuration to standard location */
|
|
60
120
|
export function saveServiceConfig(config) {
|
|
61
|
-
const
|
|
121
|
+
const instanceId = validateInstanceId(config.instanceId || DEFAULT_INSTANCE_ID);
|
|
122
|
+
const dir = getConfigDir(instanceId);
|
|
62
123
|
mkdirSync(dir, { recursive: true });
|
|
63
|
-
mkdirSync(getLogDir(), { recursive: true });
|
|
64
|
-
writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
|
|
124
|
+
mkdirSync(getLogDir(instanceId), { recursive: true });
|
|
125
|
+
writeFileSync(getConfigPath(instanceId), JSON.stringify({ ...config, instanceId }, null, 2));
|
|
65
126
|
}
|
|
66
127
|
|
|
67
128
|
/** Load agent configuration from standard location */
|
|
68
|
-
export function loadServiceConfig() {
|
|
69
|
-
const
|
|
129
|
+
export function loadServiceConfig(instanceId = process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID) {
|
|
130
|
+
const normalized = validateInstanceId(instanceId);
|
|
131
|
+
const configPath = getConfigPath(normalized);
|
|
70
132
|
if (!existsSync(configPath)) return null;
|
|
71
133
|
try {
|
|
72
|
-
|
|
134
|
+
const loaded = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
135
|
+
return { ...loaded, instanceId: loaded.instanceId || normalized };
|
|
73
136
|
} catch {
|
|
74
137
|
return null;
|
|
75
138
|
}
|
|
@@ -86,18 +149,21 @@ export function getCliPath() {
|
|
|
86
149
|
}
|
|
87
150
|
|
|
88
151
|
/**
|
|
89
|
-
* Parse
|
|
152
|
+
* Parse service options from args, merging with the selected instance config.
|
|
90
153
|
*/
|
|
91
154
|
export function parseServiceArgs(args) {
|
|
92
155
|
// Load .env if available (for dev / source-based usage)
|
|
93
156
|
loadDotenv();
|
|
94
157
|
|
|
95
|
-
const
|
|
158
|
+
const instanceId = getInstanceIdFromArgs(args);
|
|
159
|
+
const existing = loadServiceConfig(instanceId) || {};
|
|
96
160
|
const config = {
|
|
161
|
+
instanceId,
|
|
97
162
|
serverUrl: existing.serverUrl || '',
|
|
98
163
|
agentName: existing.agentName || '',
|
|
99
164
|
agentSecret: existing.agentSecret || '',
|
|
100
165
|
workDir: existing.workDir || '',
|
|
166
|
+
yeaftDir: existing.yeaftDir || '',
|
|
101
167
|
};
|
|
102
168
|
|
|
103
169
|
// Environment variables override saved config
|
|
@@ -105,16 +171,19 @@ export function parseServiceArgs(args) {
|
|
|
105
171
|
if (process.env.AGENT_NAME) config.agentName = process.env.AGENT_NAME;
|
|
106
172
|
if (process.env.AGENT_SECRET) config.agentSecret = process.env.AGENT_SECRET;
|
|
107
173
|
if (process.env.WORK_DIR) config.workDir = process.env.WORK_DIR;
|
|
174
|
+
if (process.env.YEAFT_DIR) config.yeaftDir = process.env.YEAFT_DIR;
|
|
108
175
|
|
|
109
176
|
// CLI args override everything
|
|
110
177
|
for (let i = 0; i < args.length; i++) {
|
|
111
178
|
const arg = args[i];
|
|
112
179
|
const next = args[i + 1];
|
|
113
180
|
switch (arg) {
|
|
181
|
+
case '--instance': if (next) { i++; } break;
|
|
114
182
|
case '--server': if (next) { config.serverUrl = next; i++; } break;
|
|
115
183
|
case '--name': if (next) { config.agentName = next; i++; } break;
|
|
116
184
|
case '--secret': if (next) { config.agentSecret = next; i++; } break;
|
|
117
185
|
case '--work-dir': if (next) { config.workDir = next; i++; } break;
|
|
186
|
+
case '--yeaft-dir': if (next) { config.yeaftDir = next; i++; } break;
|
|
118
187
|
}
|
|
119
188
|
}
|
|
120
189
|
|
|
@@ -122,6 +191,12 @@ export function parseServiceArgs(args) {
|
|
|
122
191
|
}
|
|
123
192
|
|
|
124
193
|
export function validateConfig(config) {
|
|
194
|
+
try {
|
|
195
|
+
validateInstanceId(config.instanceId || DEFAULT_INSTANCE_ID);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(`Error: ${err.message}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
125
200
|
if (!config.serverUrl) {
|
|
126
201
|
console.error('Error: --server <url> is required');
|
|
127
202
|
process.exit(1);
|
package/service/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
2
|
* Service — platform dispatcher
|
|
3
3
|
* Routes install/uninstall/start/stop/restart/status/logs to the correct platform module.
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync } from 'fs';
|
|
6
6
|
import { platform } from 'os';
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
getConfigDir, getLogDir, getConfigPath,
|
|
9
9
|
saveServiceConfig, loadServiceConfig,
|
|
10
|
-
parseServiceArgs, validateConfig
|
|
10
|
+
parseServiceArgs, validateConfig, getInstanceIdFromArgs, getDefaultYeaftDir
|
|
11
11
|
} from './config.js';
|
|
12
12
|
import { initYeaftDir } from '../yeaft/init.js';
|
|
13
13
|
import { getSystemdServicePath, linuxInstall, linuxUninstall, linuxStart, linuxStop, linuxRestart, linuxStatus, linuxLogs } from './linux.js';
|
|
@@ -20,17 +20,29 @@ export {
|
|
|
20
20
|
saveServiceConfig, loadServiceConfig,
|
|
21
21
|
parseServiceArgs
|
|
22
22
|
};
|
|
23
|
+
export {
|
|
24
|
+
SERVICE_NAME,
|
|
25
|
+
DEFAULT_INSTANCE_ID,
|
|
26
|
+
normalizeInstanceId,
|
|
27
|
+
isDefaultInstance,
|
|
28
|
+
validateInstanceId,
|
|
29
|
+
getInstanceIdFromArgs,
|
|
30
|
+
getServiceName,
|
|
31
|
+
getPm2AppName,
|
|
32
|
+
getLaunchdLabel,
|
|
33
|
+
getDefaultYeaftDir,
|
|
34
|
+
} from './config.js';
|
|
23
35
|
|
|
24
36
|
const os = platform();
|
|
25
37
|
|
|
26
|
-
function ensureInstalled() {
|
|
38
|
+
function ensureInstalled(instanceId) {
|
|
27
39
|
if (os === 'linux') {
|
|
28
|
-
if (!existsSync(getSystemdServicePath())) {
|
|
40
|
+
if (!existsSync(getSystemdServicePath(instanceId))) {
|
|
29
41
|
console.error('Service not installed. Run "yeaft-agent install" first.');
|
|
30
42
|
process.exit(1);
|
|
31
43
|
}
|
|
32
44
|
} else if (os === 'darwin') {
|
|
33
|
-
if (!existsSync(getLaunchdPlistPath())) {
|
|
45
|
+
if (!existsSync(getLaunchdPlistPath(instanceId))) {
|
|
34
46
|
console.error('Service not installed. Run "yeaft-agent install" first.');
|
|
35
47
|
process.exit(1);
|
|
36
48
|
}
|
|
@@ -45,17 +57,19 @@ export function install(args) {
|
|
|
45
57
|
|
|
46
58
|
// Initialize ~/.yeaft/ directory + default config.json
|
|
47
59
|
// so `yeaft` CLI is ready to use immediately after install
|
|
48
|
-
const
|
|
60
|
+
const effectiveYeaftDir = config.yeaftDir || getDefaultYeaftDir(config.instanceId);
|
|
61
|
+
const { dir, created } = initYeaftDir(effectiveYeaftDir);
|
|
49
62
|
if (created.length > 0) {
|
|
50
63
|
console.log(`Initialized ${dir}`);
|
|
51
64
|
console.log(` Edit ${dir}/config.json to configure LLM providers.`);
|
|
52
65
|
console.log('');
|
|
53
66
|
}
|
|
54
67
|
|
|
55
|
-
console.log(`Installing
|
|
56
|
-
console.log(`
|
|
57
|
-
console.log(`
|
|
58
|
-
console.log(`
|
|
68
|
+
console.log(`Installing yeaft-agent service...`);
|
|
69
|
+
console.log(` Instance: ${config.instanceId}`);
|
|
70
|
+
console.log(` Server: ${config.serverUrl}`);
|
|
71
|
+
console.log(` Name: ${config.agentName || '(auto)'}`);
|
|
72
|
+
console.log(` WorkDir: ${config.workDir || '(home)'}`);
|
|
59
73
|
console.log('');
|
|
60
74
|
|
|
61
75
|
if (os === 'linux') linuxInstall(config);
|
|
@@ -68,45 +82,51 @@ export function install(args) {
|
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
export function uninstall() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
else if (os === '
|
|
85
|
+
export function uninstall(args = []) {
|
|
86
|
+
const instanceId = getInstanceIdFromArgs(args);
|
|
87
|
+
console.log(`Uninstalling yeaft-agent service (${instanceId})...`);
|
|
88
|
+
if (os === 'linux') linuxUninstall(instanceId);
|
|
89
|
+
else if (os === 'darwin') macUninstall(instanceId);
|
|
90
|
+
else if (os === 'win32') winUninstall(instanceId);
|
|
76
91
|
else { console.error(`Unsupported platform: ${os}`); process.exit(1); }
|
|
77
92
|
}
|
|
78
93
|
|
|
79
|
-
export function start() {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
else if (os === '
|
|
94
|
+
export function start(args = []) {
|
|
95
|
+
const instanceId = getInstanceIdFromArgs(args);
|
|
96
|
+
ensureInstalled(instanceId);
|
|
97
|
+
if (os === 'linux') linuxStart(instanceId);
|
|
98
|
+
else if (os === 'darwin') macStart(instanceId);
|
|
99
|
+
else if (os === 'win32') winStart(instanceId);
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
export function stop() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else if (os === '
|
|
102
|
+
export function stop(args = []) {
|
|
103
|
+
const instanceId = getInstanceIdFromArgs(args);
|
|
104
|
+
ensureInstalled(instanceId);
|
|
105
|
+
if (os === 'linux') linuxStop(instanceId);
|
|
106
|
+
else if (os === 'darwin') macStop(instanceId);
|
|
107
|
+
else if (os === 'win32') winStop(instanceId);
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
export function restart() {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
else if (os === '
|
|
110
|
+
export function restart(args = []) {
|
|
111
|
+
const instanceId = getInstanceIdFromArgs(args);
|
|
112
|
+
ensureInstalled(instanceId);
|
|
113
|
+
if (os === 'linux') linuxRestart(instanceId);
|
|
114
|
+
else if (os === 'darwin') macRestart(instanceId);
|
|
115
|
+
else if (os === 'win32') winRestart(instanceId);
|
|
98
116
|
}
|
|
99
117
|
|
|
100
|
-
export function status() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
else if (os === '
|
|
118
|
+
export function status(args = []) {
|
|
119
|
+
const instanceId = getInstanceIdFromArgs(args);
|
|
120
|
+
if (os === 'linux') linuxStatus(instanceId);
|
|
121
|
+
else if (os === 'darwin') macStatus(instanceId);
|
|
122
|
+
else if (os === 'win32') winStatus(instanceId);
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
export function logs() {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
else if (os === '
|
|
125
|
+
export function logs(args = []) {
|
|
126
|
+
const instanceId = getInstanceIdFromArgs(args);
|
|
127
|
+
if (os === 'linux') linuxLogs(instanceId);
|
|
128
|
+
else if (os === 'darwin') macLogs(instanceId);
|
|
129
|
+
else if (os === 'win32') winLogs(instanceId);
|
|
110
130
|
}
|
|
111
131
|
|
|
112
132
|
export { doctor };
|
package/service/linux.js
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
2
|
* Service — Linux (systemd) platform implementation
|
|
3
3
|
*/
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
|
6
6
|
import { join, dirname } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
|
-
import {
|
|
8
|
+
import { getServiceName, getLogDir, getNodePath, getCliPath, DEFAULT_INSTANCE_ID } from './config.js';
|
|
9
9
|
|
|
10
10
|
/** Pure path getter — no side effects (no directory creation). */
|
|
11
|
-
export function getSystemdServicePath() {
|
|
12
|
-
return join(homedir(), '.config', 'systemd', 'user', `${
|
|
11
|
+
export function getSystemdServicePath(instanceId = DEFAULT_INSTANCE_ID) {
|
|
12
|
+
return join(homedir(), '.config', 'systemd', 'user', `${getServiceName(instanceId)}.service`);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function generateSystemdUnit(config) {
|
|
16
16
|
const nodePath = getNodePath();
|
|
17
17
|
const cliPath = getCliPath();
|
|
18
|
+
const logDir = getLogDir(config.instanceId);
|
|
18
19
|
const envLines = [];
|
|
20
|
+
if (config.instanceId) envLines.push(`Environment=YEAFT_AGENT_INSTANCE=${config.instanceId}`);
|
|
19
21
|
if (config.serverUrl) envLines.push(`Environment=SERVER_URL=${config.serverUrl}`);
|
|
20
22
|
if (config.agentName) envLines.push(`Environment=AGENT_NAME=${config.agentName}`);
|
|
21
23
|
if (config.agentSecret) envLines.push(`Environment=AGENT_SECRET=${config.agentSecret}`);
|
|
22
24
|
if (config.workDir) envLines.push(`Environment=WORK_DIR=${config.workDir}`);
|
|
25
|
+
if (config.yeaftDir) envLines.push(`Environment=YEAFT_DIR=${config.yeaftDir}`);
|
|
23
26
|
|
|
24
27
|
// Include node's bin dir in PATH for claude CLI access
|
|
25
28
|
const nodeBinDir = dirname(nodePath);
|
|
26
29
|
|
|
27
30
|
return `[Unit]
|
|
28
|
-
Description=Yeaft WebChat Agent
|
|
31
|
+
Description=Yeaft WebChat Agent (${config.instanceId || DEFAULT_INSTANCE_ID})
|
|
29
32
|
After=network-online.target
|
|
30
33
|
Wants=network-online.target
|
|
31
34
|
|
|
@@ -39,8 +42,8 @@ KillMode=process
|
|
|
39
42
|
${envLines.join('\n')}
|
|
40
43
|
Environment=PATH=${nodeBinDir}:${homedir()}/.local/bin:${homedir()}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
|
|
41
44
|
|
|
42
|
-
StandardOutput=append:${
|
|
43
|
-
StandardError=append:${
|
|
45
|
+
StandardOutput=append:${logDir}/out.log
|
|
46
|
+
StandardError=append:${logDir}/error.log
|
|
44
47
|
|
|
45
48
|
[Install]
|
|
46
49
|
WantedBy=default.target
|
|
@@ -48,61 +51,68 @@ WantedBy=default.target
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export function linuxInstall(config) {
|
|
51
|
-
const
|
|
54
|
+
const serviceName = getServiceName(config.instanceId);
|
|
55
|
+
const servicePath = getSystemdServicePath(config.instanceId);
|
|
52
56
|
// Ensure systemd user directory exists before writing
|
|
53
57
|
mkdirSync(join(homedir(), '.config', 'systemd', 'user'), { recursive: true });
|
|
58
|
+
mkdirSync(getLogDir(config.instanceId), { recursive: true });
|
|
54
59
|
writeFileSync(servicePath, generateSystemdUnit(config));
|
|
55
60
|
execSync('systemctl --user daemon-reload');
|
|
56
|
-
execSync(`systemctl --user enable ${
|
|
57
|
-
execSync(`systemctl --user start ${
|
|
58
|
-
console.log(`Service installed and started
|
|
61
|
+
execSync(`systemctl --user enable ${serviceName}`);
|
|
62
|
+
execSync(`systemctl --user start ${serviceName}`);
|
|
63
|
+
console.log(`Service installed and started: ${serviceName}`);
|
|
59
64
|
console.log(`\nManage with:`);
|
|
60
|
-
console.log(` yeaft-agent status`);
|
|
61
|
-
console.log(` yeaft-agent logs`);
|
|
62
|
-
console.log(` yeaft-agent restart`);
|
|
63
|
-
console.log(` yeaft-agent uninstall`);
|
|
65
|
+
console.log(` yeaft-agent status --instance ${config.instanceId}`);
|
|
66
|
+
console.log(` yeaft-agent logs --instance ${config.instanceId}`);
|
|
67
|
+
console.log(` yeaft-agent restart --instance ${config.instanceId}`);
|
|
68
|
+
console.log(` yeaft-agent uninstall --instance ${config.instanceId}`);
|
|
64
69
|
console.log(`\nTo run when not logged in:`);
|
|
65
70
|
console.log(` sudo loginctl enable-linger $(whoami)`);
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
export function linuxUninstall() {
|
|
69
|
-
|
|
70
|
-
try { execSync(`systemctl --user
|
|
71
|
-
|
|
73
|
+
export function linuxUninstall(instanceId = DEFAULT_INSTANCE_ID) {
|
|
74
|
+
const serviceName = getServiceName(instanceId);
|
|
75
|
+
try { execSync(`systemctl --user stop ${serviceName} 2>/dev/null`); } catch {}
|
|
76
|
+
try { execSync(`systemctl --user disable ${serviceName} 2>/dev/null`); } catch {}
|
|
77
|
+
const servicePath = getSystemdServicePath(instanceId);
|
|
72
78
|
if (existsSync(servicePath)) unlinkSync(servicePath);
|
|
73
79
|
try { execSync('systemctl --user daemon-reload'); } catch {}
|
|
74
|
-
console.log(
|
|
80
|
+
console.log(`Service uninstalled: ${serviceName}`);
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
export function linuxStart() {
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
export function linuxStart(instanceId = DEFAULT_INSTANCE_ID) {
|
|
84
|
+
const serviceName = getServiceName(instanceId);
|
|
85
|
+
execSync(`systemctl --user start ${serviceName}`, { stdio: 'inherit' });
|
|
86
|
+
console.log(`Service started: ${serviceName}`);
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
export function linuxStop() {
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
export function linuxStop(instanceId = DEFAULT_INSTANCE_ID) {
|
|
90
|
+
const serviceName = getServiceName(instanceId);
|
|
91
|
+
execSync(`systemctl --user stop ${serviceName}`, { stdio: 'inherit' });
|
|
92
|
+
console.log(`Service stopped: ${serviceName}`);
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
export function linuxRestart() {
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
export function linuxRestart(instanceId = DEFAULT_INSTANCE_ID) {
|
|
96
|
+
const serviceName = getServiceName(instanceId);
|
|
97
|
+
execSync(`systemctl --user restart ${serviceName}`, { stdio: 'inherit' });
|
|
98
|
+
console.log(`Service restarted: ${serviceName}`);
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
/**
|
|
93
102
|
* Query systemd for the current service status.
|
|
94
103
|
* Returns { running: boolean, pid: string|null }.
|
|
95
104
|
*/
|
|
96
|
-
export function getLinuxServiceStatus() {
|
|
105
|
+
export function getLinuxServiceStatus(instanceId = DEFAULT_INSTANCE_ID) {
|
|
106
|
+
const serviceName = getServiceName(instanceId);
|
|
97
107
|
try {
|
|
98
|
-
const output = execSync(`systemctl --user is-active ${
|
|
108
|
+
const output = execSync(`systemctl --user is-active ${serviceName}`, {
|
|
99
109
|
encoding: 'utf-8',
|
|
100
110
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
101
111
|
}).trim();
|
|
102
112
|
if (output === 'active') {
|
|
103
113
|
let pid = null;
|
|
104
114
|
try {
|
|
105
|
-
pid = execSync(`systemctl --user show ${
|
|
115
|
+
pid = execSync(`systemctl --user show ${serviceName} --property=MainPID --value`, {
|
|
106
116
|
encoding: 'utf-8',
|
|
107
117
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
118
|
}).trim();
|
|
@@ -116,20 +126,21 @@ export function getLinuxServiceStatus() {
|
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
128
|
|
|
119
|
-
export function linuxStatus() {
|
|
129
|
+
export function linuxStatus(instanceId = DEFAULT_INSTANCE_ID) {
|
|
120
130
|
try {
|
|
121
|
-
execSync(`systemctl --user status ${
|
|
131
|
+
execSync(`systemctl --user status ${getServiceName(instanceId)}`, { stdio: 'inherit' });
|
|
122
132
|
} catch {
|
|
123
133
|
// systemctl status returns non-zero when service is stopped
|
|
124
134
|
}
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
export function linuxLogs() {
|
|
137
|
+
export function linuxLogs(instanceId = DEFAULT_INSTANCE_ID) {
|
|
138
|
+
const serviceName = getServiceName(instanceId);
|
|
128
139
|
try {
|
|
129
|
-
execSync(`journalctl --user -u ${
|
|
140
|
+
execSync(`journalctl --user -u ${serviceName} -f --no-pager -n 100`, { stdio: 'inherit' });
|
|
130
141
|
} catch {
|
|
131
142
|
// Fallback to log files
|
|
132
|
-
const logFile = join(getLogDir(), 'out.log');
|
|
143
|
+
const logFile = join(getLogDir(instanceId), 'out.log');
|
|
133
144
|
if (existsSync(logFile)) {
|
|
134
145
|
execSync(`tail -f -n 100 ${logFile}`, { stdio: 'inherit' });
|
|
135
146
|
} else {
|
package/service/macos.js
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
2
|
* Service — macOS (launchd) platform implementation
|
|
3
3
|
*/
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
|
-
import { getLogDir, getNodePath, getCliPath } from './config.js';
|
|
8
|
+
import { getLaunchdLabel, getLogDir, getNodePath, getCliPath, DEFAULT_INSTANCE_ID } from './config.js';
|
|
9
9
|
|
|
10
10
|
/** Pure path getter — no side effects (no directory creation). */
|
|
11
|
-
export function getLaunchdPlistPath() {
|
|
12
|
-
return join(homedir(), 'Library', 'LaunchAgents',
|
|
11
|
+
export function getLaunchdPlistPath(instanceId = DEFAULT_INSTANCE_ID) {
|
|
12
|
+
return join(homedir(), 'Library', 'LaunchAgents', `${getLaunchdLabel(instanceId)}.plist`);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function generateLaunchdPlist(config) {
|
|
16
16
|
const nodePath = getNodePath();
|
|
17
17
|
const cliPath = getCliPath();
|
|
18
|
-
const logDir = getLogDir();
|
|
18
|
+
const logDir = getLogDir(config.instanceId);
|
|
19
|
+
const label = getLaunchdLabel(config.instanceId);
|
|
19
20
|
|
|
20
21
|
const envDict = [];
|
|
22
|
+
if (config.instanceId) envDict.push(` <key>YEAFT_AGENT_INSTANCE</key>\n <string>${config.instanceId}</string>`);
|
|
21
23
|
if (config.serverUrl) envDict.push(` <key>SERVER_URL</key>\n <string>${config.serverUrl}</string>`);
|
|
22
24
|
if (config.agentName) envDict.push(` <key>AGENT_NAME</key>\n <string>${config.agentName}</string>`);
|
|
23
25
|
if (config.agentSecret) envDict.push(` <key>AGENT_SECRET</key>\n <string>${config.agentSecret}</string>`);
|
|
24
26
|
if (config.workDir) envDict.push(` <key>WORK_DIR</key>\n <string>${config.workDir}</string>`);
|
|
27
|
+
if (config.yeaftDir) envDict.push(` <key>YEAFT_DIR</key>\n <string>${config.yeaftDir}</string>`);
|
|
25
28
|
|
|
26
29
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
27
30
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
28
31
|
<plist version="1.0">
|
|
29
32
|
<dict>
|
|
30
33
|
<key>Label</key>
|
|
31
|
-
<string
|
|
34
|
+
<string>${label}</string>
|
|
32
35
|
<key>ProgramArguments</key>
|
|
33
36
|
<array>
|
|
34
37
|
<string>${nodePath}</string>
|
|
@@ -59,62 +62,64 @@ ${envDict.join('\n')}
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
export function macInstall(config) {
|
|
62
|
-
const plistPath = getLaunchdPlistPath();
|
|
65
|
+
const plistPath = getLaunchdPlistPath(config.instanceId);
|
|
63
66
|
// Ensure LaunchAgents directory exists before writing
|
|
64
67
|
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
68
|
+
mkdirSync(getLogDir(config.instanceId), { recursive: true });
|
|
65
69
|
// Unload first if exists
|
|
66
70
|
if (existsSync(plistPath)) {
|
|
67
71
|
try { execSync(`launchctl unload ${plistPath} 2>/dev/null`); } catch {}
|
|
68
72
|
}
|
|
69
73
|
writeFileSync(plistPath, generateLaunchdPlist(config));
|
|
70
74
|
execSync(`launchctl load ${plistPath}`);
|
|
71
|
-
console.log(
|
|
75
|
+
console.log(`Service installed and started: ${getLaunchdLabel(config.instanceId)}`);
|
|
72
76
|
console.log(`\nManage with:`);
|
|
73
|
-
console.log(` yeaft-agent status`);
|
|
74
|
-
console.log(` yeaft-agent logs`);
|
|
75
|
-
console.log(` yeaft-agent restart`);
|
|
76
|
-
console.log(` yeaft-agent uninstall`);
|
|
77
|
+
console.log(` yeaft-agent status --instance ${config.instanceId}`);
|
|
78
|
+
console.log(` yeaft-agent logs --instance ${config.instanceId}`);
|
|
79
|
+
console.log(` yeaft-agent restart --instance ${config.instanceId}`);
|
|
80
|
+
console.log(` yeaft-agent uninstall --instance ${config.instanceId}`);
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
export function macUninstall() {
|
|
80
|
-
const plistPath = getLaunchdPlistPath();
|
|
83
|
+
export function macUninstall(instanceId = DEFAULT_INSTANCE_ID) {
|
|
84
|
+
const plistPath = getLaunchdPlistPath(instanceId);
|
|
81
85
|
if (existsSync(plistPath)) {
|
|
82
86
|
try { execSync(`launchctl unload ${plistPath}`); } catch {}
|
|
83
87
|
unlinkSync(plistPath);
|
|
84
88
|
}
|
|
85
|
-
console.log(
|
|
89
|
+
console.log(`Service uninstalled: ${getLaunchdLabel(instanceId)}`);
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
export function macStart() {
|
|
89
|
-
const plistPath = getLaunchdPlistPath();
|
|
92
|
+
export function macStart(instanceId = DEFAULT_INSTANCE_ID) {
|
|
93
|
+
const plistPath = getLaunchdPlistPath(instanceId);
|
|
90
94
|
if (!existsSync(plistPath)) {
|
|
91
95
|
console.error('Service not installed. Run "yeaft-agent install" first.');
|
|
92
96
|
process.exit(1);
|
|
93
97
|
}
|
|
94
98
|
execSync(`launchctl load ${plistPath}`);
|
|
95
|
-
console.log(
|
|
99
|
+
console.log(`Service started: ${getLaunchdLabel(instanceId)}`);
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
export function macStop() {
|
|
99
|
-
const plistPath = getLaunchdPlistPath();
|
|
102
|
+
export function macStop(instanceId = DEFAULT_INSTANCE_ID) {
|
|
103
|
+
const plistPath = getLaunchdPlistPath(instanceId);
|
|
100
104
|
if (existsSync(plistPath)) {
|
|
101
105
|
execSync(`launchctl unload ${plistPath}`);
|
|
102
106
|
}
|
|
103
|
-
console.log(
|
|
107
|
+
console.log(`Service stopped: ${getLaunchdLabel(instanceId)}`);
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
export function macRestart() {
|
|
107
|
-
macStop();
|
|
108
|
-
macStart();
|
|
110
|
+
export function macRestart(instanceId = DEFAULT_INSTANCE_ID) {
|
|
111
|
+
macStop(instanceId);
|
|
112
|
+
macStart(instanceId);
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
/**
|
|
112
116
|
* Query launchd for the current service status.
|
|
113
117
|
* Returns { running: boolean, pid: string|null, exitCode: string|null }.
|
|
114
118
|
*/
|
|
115
|
-
export function getMacServiceStatus() {
|
|
119
|
+
export function getMacServiceStatus(instanceId = DEFAULT_INSTANCE_ID) {
|
|
120
|
+
const label = getLaunchdLabel(instanceId);
|
|
116
121
|
try {
|
|
117
|
-
const output = execSync(
|
|
122
|
+
const output = execSync(`launchctl list | grep ${label}`, {
|
|
118
123
|
encoding: 'utf-8',
|
|
119
124
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
120
125
|
});
|
|
@@ -133,8 +138,8 @@ export function getMacServiceStatus() {
|
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
export function macStatus() {
|
|
137
|
-
const status = getMacServiceStatus();
|
|
141
|
+
export function macStatus(instanceId = DEFAULT_INSTANCE_ID) {
|
|
142
|
+
const status = getMacServiceStatus(instanceId);
|
|
138
143
|
if (status.running) {
|
|
139
144
|
console.log(`Service is running (PID: ${status.pid})`);
|
|
140
145
|
} else if (status.exitCode !== null) {
|
|
@@ -144,8 +149,8 @@ export function macStatus() {
|
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
export function macLogs() {
|
|
148
|
-
const logFile = join(getLogDir(), 'out.log');
|
|
152
|
+
export function macLogs(instanceId = DEFAULT_INSTANCE_ID) {
|
|
153
|
+
const logFile = join(getLogDir(instanceId), 'out.log');
|
|
149
154
|
if (existsSync(logFile)) {
|
|
150
155
|
execSync(`tail -f -n 100 ${logFile}`, { stdio: 'inherit' });
|
|
151
156
|
} else {
|
package/service/windows.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
2
|
* Service — Windows (pm2) platform implementation
|
|
3
3
|
*/
|
|
4
4
|
import { execSync, spawn } from 'child_process';
|
|
5
5
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
|
|
6
6
|
import { join, dirname } from 'path';
|
|
7
|
-
import {
|
|
7
|
+
import { getConfigDir, getLogDir, getNodePath, getCliPath, getPm2AppName, DEFAULT_INSTANCE_ID } from './config.js';
|
|
8
8
|
|
|
9
9
|
const WIN_TASK_NAME = 'YeaftAgent';
|
|
10
|
-
const PM2_APP_NAME = 'yeaft-agent';
|
|
11
10
|
|
|
12
11
|
// Legacy paths for cleanup
|
|
13
|
-
function getWinWrapperPath() { return join(getConfigDir(), 'run.vbs'); }
|
|
14
|
-
function getWinBatPath() { return join(getConfigDir(), 'run.bat'); }
|
|
12
|
+
function getWinWrapperPath(instanceId = DEFAULT_INSTANCE_ID) { return join(getConfigDir(instanceId), 'run.vbs'); }
|
|
13
|
+
function getWinBatPath(instanceId = DEFAULT_INSTANCE_ID) { return join(getConfigDir(instanceId), 'run.bat'); }
|
|
15
14
|
|
|
16
15
|
function ensurePm2() {
|
|
17
16
|
try {
|
|
@@ -22,25 +21,28 @@ function ensurePm2() {
|
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export function getEcosystemPath() {
|
|
26
|
-
return join(getConfigDir(), 'ecosystem.config.cjs');
|
|
24
|
+
export function getEcosystemPath(instanceId = DEFAULT_INSTANCE_ID) {
|
|
25
|
+
return join(getConfigDir(instanceId), 'ecosystem.config.cjs');
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
function generateEcosystem(config) {
|
|
30
29
|
const nodePath = getNodePath();
|
|
31
30
|
const cliPath = getCliPath();
|
|
32
31
|
const cliDir = dirname(cliPath);
|
|
33
|
-
const logDir = getLogDir();
|
|
32
|
+
const logDir = getLogDir(config.instanceId);
|
|
33
|
+
const pm2AppName = getPm2AppName(config.instanceId);
|
|
34
34
|
|
|
35
35
|
const env = {};
|
|
36
|
+
if (config.instanceId) env.YEAFT_AGENT_INSTANCE = config.instanceId;
|
|
36
37
|
if (config.serverUrl) env.SERVER_URL = config.serverUrl;
|
|
37
38
|
if (config.agentName) env.AGENT_NAME = config.agentName;
|
|
38
39
|
if (config.agentSecret) env.AGENT_SECRET = config.agentSecret;
|
|
39
40
|
if (config.workDir) env.WORK_DIR = config.workDir;
|
|
41
|
+
if (config.yeaftDir) env.YEAFT_DIR = config.yeaftDir;
|
|
40
42
|
|
|
41
43
|
return `module.exports = {
|
|
42
44
|
apps: [{
|
|
43
|
-
name: '${
|
|
45
|
+
name: '${pm2AppName}',
|
|
44
46
|
script: '${cliPath.replace(/\\/g, '\\\\')}',
|
|
45
47
|
interpreter: '${nodePath.replace(/\\/g, '\\\\')}',
|
|
46
48
|
cwd: '${cliDir.replace(/\\/g, '\\\\')}',
|
|
@@ -61,15 +63,16 @@ function generateEcosystem(config) {
|
|
|
61
63
|
|
|
62
64
|
export function winInstall(config) {
|
|
63
65
|
ensurePm2();
|
|
64
|
-
const logDir = getLogDir();
|
|
66
|
+
const logDir = getLogDir(config.instanceId);
|
|
67
|
+
const pm2AppName = getPm2AppName(config.instanceId);
|
|
65
68
|
mkdirSync(logDir, { recursive: true });
|
|
66
69
|
|
|
67
70
|
// Generate ecosystem config
|
|
68
|
-
const ecoPath = getEcosystemPath();
|
|
71
|
+
const ecoPath = getEcosystemPath(config.instanceId);
|
|
69
72
|
writeFileSync(ecoPath, generateEcosystem(config));
|
|
70
73
|
|
|
71
74
|
// Stop existing instance if any
|
|
72
|
-
try { execSync(`pm2 delete ${
|
|
75
|
+
try { execSync(`pm2 delete ${pm2AppName}`, { stdio: 'pipe' }); } catch {}
|
|
73
76
|
|
|
74
77
|
// Start with pm2
|
|
75
78
|
execSync(`pm2 start "${ecoPath}"`, { stdio: 'inherit' });
|
|
@@ -81,7 +84,7 @@ export function winInstall(config) {
|
|
|
81
84
|
// pm2-startup doesn't work well on Windows, use Startup folder approach
|
|
82
85
|
const trayScript = join(dirname(getCliPath()), 'scripts', 'agent-tray.ps1');
|
|
83
86
|
const startupDir = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
84
|
-
const startupBat = join(startupDir, `${
|
|
87
|
+
const startupBat = join(startupDir, `${pm2AppName}.bat`);
|
|
85
88
|
// Resurrect pm2 processes + launch tray icon
|
|
86
89
|
let batContent = `@echo off\r\npm2 resurrect\r\n`;
|
|
87
90
|
if (existsSync(trayScript)) {
|
|
@@ -96,41 +99,43 @@ export function winInstall(config) {
|
|
|
96
99
|
}).unref();
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
console.log(`\nService installed and started
|
|
102
|
+
console.log(`\nService installed and started: ${pm2AppName}`);
|
|
100
103
|
console.log(` Ecosystem: ${ecoPath}`);
|
|
101
104
|
console.log(` Startup: ${startupBat}`);
|
|
102
105
|
console.log(`\nManage with:`);
|
|
103
|
-
console.log(` yeaft-agent status`);
|
|
104
|
-
console.log(` yeaft-agent logs`);
|
|
105
|
-
console.log(` yeaft-agent restart`);
|
|
106
|
-
console.log(` yeaft-agent uninstall`);
|
|
106
|
+
console.log(` yeaft-agent status --instance ${config.instanceId}`);
|
|
107
|
+
console.log(` yeaft-agent logs --instance ${config.instanceId}`);
|
|
108
|
+
console.log(` yeaft-agent restart --instance ${config.instanceId}`);
|
|
109
|
+
console.log(` yeaft-agent uninstall --instance ${config.instanceId}`);
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
export function winUninstall() {
|
|
110
|
-
|
|
112
|
+
export function winUninstall(instanceId = DEFAULT_INSTANCE_ID) {
|
|
113
|
+
const pm2AppName = getPm2AppName(instanceId);
|
|
114
|
+
try { execSync(`pm2 delete ${pm2AppName}`, { stdio: 'pipe' }); } catch {}
|
|
111
115
|
try { execSync('pm2 save', { stdio: 'pipe' }); } catch {}
|
|
112
116
|
// Clean up ecosystem config
|
|
113
|
-
const ecoPath = getEcosystemPath();
|
|
117
|
+
const ecoPath = getEcosystemPath(instanceId);
|
|
114
118
|
if (existsSync(ecoPath)) unlinkSync(ecoPath);
|
|
115
119
|
// Clean up Startup bat
|
|
116
|
-
const startupBat = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${
|
|
120
|
+
const startupBat = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${pm2AppName}.bat`);
|
|
117
121
|
if (existsSync(startupBat)) unlinkSync(startupBat);
|
|
118
122
|
// Clean up legacy files
|
|
119
|
-
const vbsPath = getWinWrapperPath();
|
|
120
|
-
const batPath = getWinBatPath();
|
|
123
|
+
const vbsPath = getWinWrapperPath(instanceId);
|
|
124
|
+
const batPath = getWinBatPath(instanceId);
|
|
121
125
|
if (existsSync(vbsPath)) unlinkSync(vbsPath);
|
|
122
126
|
if (existsSync(batPath)) unlinkSync(batPath);
|
|
123
127
|
const startupVbs = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${WIN_TASK_NAME}.vbs`);
|
|
124
128
|
if (existsSync(startupVbs)) unlinkSync(startupVbs);
|
|
125
|
-
console.log(
|
|
129
|
+
console.log(`Service uninstalled: ${pm2AppName}`);
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
export function winStart() {
|
|
132
|
+
export function winStart(instanceId = DEFAULT_INSTANCE_ID) {
|
|
133
|
+
const pm2AppName = getPm2AppName(instanceId);
|
|
129
134
|
try {
|
|
130
|
-
execSync(`pm2 start ${
|
|
135
|
+
execSync(`pm2 start ${pm2AppName}`, { stdio: 'inherit' });
|
|
131
136
|
} catch {
|
|
132
137
|
// Try ecosystem file
|
|
133
|
-
const ecoPath = getEcosystemPath();
|
|
138
|
+
const ecoPath = getEcosystemPath(instanceId);
|
|
134
139
|
if (existsSync(ecoPath)) {
|
|
135
140
|
execSync(`pm2 start "${ecoPath}"`, { stdio: 'inherit' });
|
|
136
141
|
} else {
|
|
@@ -140,17 +145,17 @@ export function winStart() {
|
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
export function winStop() {
|
|
148
|
+
export function winStop(instanceId = DEFAULT_INSTANCE_ID) {
|
|
144
149
|
try {
|
|
145
|
-
execSync(`pm2 stop ${
|
|
150
|
+
execSync(`pm2 stop ${getPm2AppName(instanceId)}`, { stdio: 'inherit' });
|
|
146
151
|
} catch {
|
|
147
152
|
console.error('Service not running or not installed.');
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
export function winRestart() {
|
|
156
|
+
export function winRestart(instanceId = DEFAULT_INSTANCE_ID) {
|
|
152
157
|
try {
|
|
153
|
-
execSync(`pm2 restart ${
|
|
158
|
+
execSync(`pm2 restart ${getPm2AppName(instanceId)}`, { stdio: 'inherit' });
|
|
154
159
|
} catch {
|
|
155
160
|
console.error('Service not running. Use "yeaft-agent start" to start.');
|
|
156
161
|
}
|
|
@@ -160,14 +165,15 @@ export function winRestart() {
|
|
|
160
165
|
* Query pm2 for the current service status.
|
|
161
166
|
* Returns { running: boolean, pid: string|null }.
|
|
162
167
|
*/
|
|
163
|
-
export function getWinServiceStatus() {
|
|
168
|
+
export function getWinServiceStatus(instanceId = DEFAULT_INSTANCE_ID) {
|
|
169
|
+
const pm2AppName = getPm2AppName(instanceId);
|
|
164
170
|
try {
|
|
165
171
|
const output = execSync('pm2 jlist', {
|
|
166
172
|
encoding: 'utf-8',
|
|
167
173
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
168
174
|
});
|
|
169
175
|
const apps = JSON.parse(output);
|
|
170
|
-
const app = Array.isArray(apps) && apps.find(a => a.name ===
|
|
176
|
+
const app = Array.isArray(apps) && apps.find(a => a.name === pm2AppName);
|
|
171
177
|
if (app) {
|
|
172
178
|
const running = app.pm2_env && app.pm2_env.status === 'online';
|
|
173
179
|
const pid = running ? app.pid : null;
|
|
@@ -179,22 +185,23 @@ export function getWinServiceStatus() {
|
|
|
179
185
|
}
|
|
180
186
|
}
|
|
181
187
|
|
|
182
|
-
export function winStatus() {
|
|
188
|
+
export function winStatus(instanceId = DEFAULT_INSTANCE_ID) {
|
|
183
189
|
try {
|
|
184
|
-
execSync(`pm2 describe ${
|
|
190
|
+
execSync(`pm2 describe ${getPm2AppName(instanceId)}`, { stdio: 'inherit' });
|
|
185
191
|
} catch {
|
|
186
192
|
console.log('Service is not installed.');
|
|
187
193
|
}
|
|
188
194
|
}
|
|
189
195
|
|
|
190
|
-
export function winLogs() {
|
|
191
|
-
const
|
|
196
|
+
export function winLogs(instanceId = DEFAULT_INSTANCE_ID) {
|
|
197
|
+
const pm2AppName = getPm2AppName(instanceId);
|
|
198
|
+
const child = spawn('pm2', ['logs', pm2AppName, '--lines', '100'], {
|
|
192
199
|
stdio: 'inherit',
|
|
193
200
|
shell: true
|
|
194
201
|
});
|
|
195
202
|
child.on('error', () => {
|
|
196
203
|
// Fallback to reading log file directly
|
|
197
|
-
const logFile = join(getLogDir(), 'out.log');
|
|
204
|
+
const logFile = join(getLogDir(instanceId), 'out.log');
|
|
198
205
|
if (existsSync(logFile)) {
|
|
199
206
|
console.log(readFileSync(logFile, 'utf-8'));
|
|
200
207
|
} else {
|
package/service.js
CHANGED
|
@@ -16,6 +16,16 @@ export {
|
|
|
16
16
|
saveServiceConfig,
|
|
17
17
|
loadServiceConfig,
|
|
18
18
|
parseServiceArgs,
|
|
19
|
+
SERVICE_NAME,
|
|
20
|
+
DEFAULT_INSTANCE_ID,
|
|
21
|
+
normalizeInstanceId,
|
|
22
|
+
isDefaultInstance,
|
|
23
|
+
validateInstanceId,
|
|
24
|
+
getInstanceIdFromArgs,
|
|
25
|
+
getServiceName,
|
|
26
|
+
getPm2AppName,
|
|
27
|
+
getLaunchdLabel,
|
|
28
|
+
getDefaultYeaftDir,
|
|
19
29
|
install,
|
|
20
30
|
uninstall,
|
|
21
31
|
start,
|