@yeaft/webchat-agent 1.0.24 → 1.0.26

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
@@ -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 Remove system service
68
- yeaft-agent start Start installed service
69
- yeaft-agent stop Stop installed service
70
- yeaft-agent restart Restart installed service
71
- yeaft-agent status Show service status
72
- yeaft-agent logs View service logs (follow mode)
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 status
95
- yeaft-agent logs
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;
@@ -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
- // 使用 agentName 作为唯一标识(不再使用随机 UUID)
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: ctx.CONFIG.agentName, // 直接用名称作为 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
- // 只保存基本配置(不再保存 agentId,因为现在用 agentName 作为 ID)
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, homedir } from 'os';
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 || join(homedir(), '.yeaft');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
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
- // Standard config/log directory per platform
44
- export function getConfigDir() {
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
- export function getLogDir() {
52
- return join(getConfigDir(), 'logs');
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 dir = getConfigDir();
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 configPath = getConfigPath();
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
- return JSON.parse(readFileSync(configPath, 'utf-8'));
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 --server/--name/--secret/--work-dir from args, merge with existing config
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 existing = loadServiceConfig() || {};
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
- SERVICE_NAME, getConfigDir, getLogDir, getConfigPath,
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 { dir, created } = initYeaftDir();
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 ${SERVICE_NAME} service...`);
56
- console.log(` Server: ${config.serverUrl}`);
57
- console.log(` Name: ${config.agentName || '(auto)'}`);
58
- console.log(` WorkDir: ${config.workDir || '(home)'}`);
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
- console.log(`Uninstalling ${SERVICE_NAME} service...`);
73
- if (os === 'linux') linuxUninstall();
74
- else if (os === 'darwin') macUninstall();
75
- else if (os === 'win32') winUninstall();
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
- ensureInstalled();
81
- if (os === 'linux') linuxStart();
82
- else if (os === 'darwin') macStart();
83
- else if (os === 'win32') winStart();
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
- ensureInstalled();
88
- if (os === 'linux') linuxStop();
89
- else if (os === 'darwin') macStop();
90
- else if (os === 'win32') winStop();
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
- ensureInstalled();
95
- if (os === 'linux') linuxRestart();
96
- else if (os === 'darwin') macRestart();
97
- else if (os === 'win32') winRestart();
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
- if (os === 'linux') linuxStatus();
102
- else if (os === 'darwin') macStatus();
103
- else if (os === 'win32') winStatus();
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
- if (os === 'linux') linuxLogs();
108
- else if (os === 'darwin') macLogs();
109
- else if (os === 'win32') winLogs();
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 { SERVICE_NAME, getLogDir, getNodePath, getCliPath } from './config.js';
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', `${SERVICE_NAME}.service`);
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:${getLogDir()}/out.log
43
- StandardError=append:${getLogDir()}/error.log
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 servicePath = getSystemdServicePath();
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 ${SERVICE_NAME}`);
57
- execSync(`systemctl --user start ${SERVICE_NAME}`);
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
- try { execSync(`systemctl --user stop ${SERVICE_NAME} 2>/dev/null`); } catch {}
70
- try { execSync(`systemctl --user disable ${SERVICE_NAME} 2>/dev/null`); } catch {}
71
- const servicePath = getSystemdServicePath();
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('Service uninstalled.');
80
+ console.log(`Service uninstalled: ${serviceName}`);
75
81
  }
76
82
 
77
- export function linuxStart() {
78
- execSync(`systemctl --user start ${SERVICE_NAME}`, { stdio: 'inherit' });
79
- console.log('Service started.');
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
- execSync(`systemctl --user stop ${SERVICE_NAME}`, { stdio: 'inherit' });
84
- console.log('Service stopped.');
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
- execSync(`systemctl --user restart ${SERVICE_NAME}`, { stdio: 'inherit' });
89
- console.log('Service restarted.');
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 ${SERVICE_NAME}`, {
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 ${SERVICE_NAME} --property=MainPID --value`, {
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 ${SERVICE_NAME}`, { stdio: 'inherit' });
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 ${SERVICE_NAME} -f --no-pager -n 100`, { stdio: 'inherit' });
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', 'com.yeaft.agent.plist');
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>com.yeaft.agent</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('Service installed and started.');
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('Service uninstalled.');
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('Service started.');
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('Service stopped.');
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('launchctl list | grep com.yeaft.agent', {
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 {
@@ -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 { SERVICE_NAME, getConfigDir, getLogDir, getNodePath, getCliPath } from './config.js';
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: '${PM2_APP_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 ${PM2_APP_NAME}`, { stdio: 'pipe' }); } catch {}
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, `${PM2_APP_NAME}.bat`);
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
- try { execSync(`pm2 delete ${PM2_APP_NAME}`, { stdio: 'pipe' }); } catch {}
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', `${PM2_APP_NAME}.bat`);
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('Service uninstalled.');
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 ${PM2_APP_NAME}`, { stdio: 'inherit' });
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 ${PM2_APP_NAME}`, { stdio: 'inherit' });
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 ${PM2_APP_NAME}`, { stdio: 'inherit' });
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 === 'yeaft-agent');
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 ${PM2_APP_NAME}`, { stdio: 'inherit' });
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 child = spawn('pm2', ['logs', PM2_APP_NAME, '--lines', '100'], {
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,