@yeaft/webchat-agent 1.0.357 → 1.0.359
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 +40 -7
- package/index.js +15 -5
- package/local-run.js +45 -6
- package/local-runtime/server/handlers/agent-output.js +8 -1
- package/local-runtime/server/ws-agent.js +22 -2
- package/local-runtime/version.json +1 -1
- package/local-service.js +219 -0
- package/package.json +1 -1
- package/service/config.js +17 -0
- package/service/index.js +1 -0
- package/service.js +1 -0
package/cli.js
CHANGED
|
@@ -64,13 +64,7 @@ if (command === 'doctor') {
|
|
|
64
64
|
} else if (command === 'llm') {
|
|
65
65
|
await handleLlmCommand(subArgs);
|
|
66
66
|
} else if (command === 'local') {
|
|
67
|
-
|
|
68
|
-
const { runLocal } = await import('./local-run.js');
|
|
69
|
-
await runLocal(subArgs);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error(`Local run failed: ${error.message}`);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
67
|
+
await handleLocalCommand(subArgs);
|
|
74
68
|
} else if (command === 'upgrade') {
|
|
75
69
|
await upgrade(subArgs);
|
|
76
70
|
} else if (command === '--version' || command === '-v') {
|
|
@@ -84,6 +78,37 @@ if (command === 'doctor') {
|
|
|
84
78
|
parseAndStart(args);
|
|
85
79
|
}
|
|
86
80
|
|
|
81
|
+
/** Dispatch the local foreground and managed-service command family. */
|
|
82
|
+
export async function handleLocalCommand(subArgs, options = {}) {
|
|
83
|
+
const printHelpFn = options.printHelp || printHelp;
|
|
84
|
+
const warn = options.warn || console.warn;
|
|
85
|
+
const loadLocalService = options.loadLocalService || (() => import('./local-service.js'));
|
|
86
|
+
const loadLocalRun = options.loadLocalRun || (() => import('./local-run.js'));
|
|
87
|
+
try {
|
|
88
|
+
const localCommand = subArgs[0];
|
|
89
|
+
if (localCommand === '--help' || localCommand === '-h') {
|
|
90
|
+
printHelpFn();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
warnDeprecatedInstanceArg(subArgs, warn);
|
|
94
|
+
if (SERVICE_COMMANDS.includes(localCommand)) {
|
|
95
|
+
const { handleLocalServiceCommand } = await loadLocalService();
|
|
96
|
+
await handleLocalServiceCommand(localCommand, subArgs.slice(1));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const { runLocal } = await loadLocalRun();
|
|
100
|
+
await runLocal(subArgs);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const message = `Local run failed: ${error.message}`;
|
|
103
|
+
if (options.onError) {
|
|
104
|
+
options.onError(message, error);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.error(message);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
function printHelp() {
|
|
88
113
|
console.log(`
|
|
89
114
|
${pkg.name} v${pkg.version}
|
|
@@ -91,6 +116,11 @@ function printHelp() {
|
|
|
91
116
|
Usage:
|
|
92
117
|
yeaft-agent [options] Run agent in foreground
|
|
93
118
|
yeaft-agent local [options] Run local Web UI, server, and agent
|
|
119
|
+
yeaft-agent local --background Run local mode in the background
|
|
120
|
+
yeaft-agent local install [options] Install local mode as a managed service
|
|
121
|
+
yeaft-agent local uninstall [options] Remove the local managed service
|
|
122
|
+
yeaft-agent local start|stop|restart|status|logs [options]
|
|
123
|
+
Control the local managed service
|
|
94
124
|
yeaft-agent install [options] Install as system service
|
|
95
125
|
yeaft-agent uninstall [options] Remove system service
|
|
96
126
|
yeaft-agent start [options] Start installed service
|
|
@@ -108,6 +138,7 @@ function printHelp() {
|
|
|
108
138
|
--server <url> WebSocket server URL (default: ws://localhost:3456)
|
|
109
139
|
--name <name> Agent name and instance id (default: computer name; invalid chars become -)
|
|
110
140
|
--port <port> Local server port (local command only; default: 6868)
|
|
141
|
+
--background, -d Detach local mode after spawning it (local command only)
|
|
111
142
|
--secret <secret> Agent secret for authentication
|
|
112
143
|
--work-dir <dir> Default working directory (default: cwd)
|
|
113
144
|
--yeaft-dir <dir> Yeaft data directory for this instance
|
|
@@ -124,6 +155,8 @@ function printHelp() {
|
|
|
124
155
|
Examples:
|
|
125
156
|
yeaft-agent local
|
|
126
157
|
yeaft-agent local --name my-worker --port 7000
|
|
158
|
+
yeaft-agent local --name my-worker --background
|
|
159
|
+
yeaft-agent local install --name my-worker --port 7000
|
|
127
160
|
yeaft-agent --server wss://your-server.com --name my-worker --secret xxx
|
|
128
161
|
yeaft-agent install --server wss://your-server.com --name my-worker --secret xxx
|
|
129
162
|
yeaft-agent install --server wss://your-server.com --name my-worker-2 --secret xxx
|
package/index.js
CHANGED
|
@@ -10,7 +10,14 @@ import { exec } from 'child_process';
|
|
|
10
10
|
import { promisify } from 'util';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import ctx from './context.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getDefaultAgentName,
|
|
15
|
+
getDefaultYeaftDir,
|
|
16
|
+
resolveRuntimeIdentity,
|
|
17
|
+
getConfigPath,
|
|
18
|
+
loadServiceConfig,
|
|
19
|
+
shouldLoadLegacyLocalConfig,
|
|
20
|
+
} from './service.js';
|
|
14
21
|
import { loadNodePty } from './terminal.js';
|
|
15
22
|
import { connect } from './connection.js';
|
|
16
23
|
import { loadMcpServers } from './mcp.js';
|
|
@@ -37,7 +44,8 @@ const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
|
37
44
|
ctx.agentVersion = pkg.version;
|
|
38
45
|
ctx.pkgName = pkg.name;
|
|
39
46
|
|
|
40
|
-
//
|
|
47
|
+
// Legacy direct launches may still read cwd/.claude-agent.json. Explicit
|
|
48
|
+
// service instances must stay scoped to their standard per-instance config.
|
|
41
49
|
const LOCAL_CONFIG_FILE = join(process.cwd(), '.claude-agent.json');
|
|
42
50
|
const IS_LOCAL_RUN = process.env.YEAFT_LOCAL_RUN === 'true';
|
|
43
51
|
const DEFAULT_AGENT_NAME = getDefaultAgentName();
|
|
@@ -56,8 +64,10 @@ function loadConfig() {
|
|
|
56
64
|
// must not inherit a remote agent's persisted configuration.
|
|
57
65
|
if (IS_LOCAL_RUN) return defaults;
|
|
58
66
|
|
|
59
|
-
// Priority 1: Local .claude-agent.json (backward compat)
|
|
60
|
-
|
|
67
|
+
// Priority 1: Local .claude-agent.json (backward compat for unscoped launches only).
|
|
68
|
+
// A named service can share its cwd with an unrelated legacy launch, so the
|
|
69
|
+
// process-level instance identity must fence this unscoped file out.
|
|
70
|
+
if (shouldLoadLegacyLocalConfig(process.env) && existsSync(LOCAL_CONFIG_FILE)) {
|
|
61
71
|
try {
|
|
62
72
|
const saved = JSON.parse(readFileSync(LOCAL_CONFIG_FILE, 'utf-8'));
|
|
63
73
|
const { agentId, ...rest } = saved;
|
|
@@ -77,7 +87,7 @@ function loadConfig() {
|
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
function saveConfig(config) {
|
|
80
|
-
if (IS_LOCAL_RUN) return;
|
|
90
|
+
if (IS_LOCAL_RUN || !shouldLoadLegacyLocalConfig(process.env)) return;
|
|
81
91
|
writeFileSync(LOCAL_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
82
92
|
}
|
|
83
93
|
|
package/local-run.js
CHANGED
|
@@ -5,18 +5,23 @@ import { createServer } from 'net';
|
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { WebSocket } from 'ws';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveServiceInstanceId, resolveYeaftDir } from './service/config.js';
|
|
9
9
|
|
|
10
10
|
const DEFAULT_PORT = 6868;
|
|
11
11
|
const LOCAL_HOST = '127.0.0.1';
|
|
12
12
|
|
|
13
13
|
export function parseLocalArgs(args, env = process.env) {
|
|
14
|
-
const options = {
|
|
14
|
+
const options = {
|
|
15
|
+
name: resolveServiceInstanceId(args, env),
|
|
16
|
+
port: DEFAULT_PORT,
|
|
17
|
+
background: false,
|
|
18
|
+
yeaftDir: null,
|
|
19
|
+
};
|
|
15
20
|
for (let i = 0; i < args.length; i++) {
|
|
16
21
|
const arg = args[i];
|
|
17
22
|
const value = args[i + 1];
|
|
18
|
-
if (arg === '--name') {
|
|
19
|
-
if (!value || value.startsWith('-')) throw new Error(
|
|
23
|
+
if (arg === '--name' || arg === '--instance' || arg === '--yeaft-dir') {
|
|
24
|
+
if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`);
|
|
20
25
|
i++;
|
|
21
26
|
} else if (arg === '--port') {
|
|
22
27
|
if (!value || value.startsWith('-')) throw new Error('--port requires a value');
|
|
@@ -24,11 +29,13 @@ export function parseLocalArgs(args, env = process.env) {
|
|
|
24
29
|
options.port = Number(value);
|
|
25
30
|
if (options.port < 1 || options.port > 65535) throw new Error(`Invalid port: ${value}`);
|
|
26
31
|
i++;
|
|
32
|
+
} else if (arg === '--background' || arg === '-d') {
|
|
33
|
+
options.background = true;
|
|
27
34
|
} else {
|
|
28
35
|
throw new Error(`Unknown local option: ${arg}`);
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
|
-
|
|
38
|
+
options.yeaftDir = resolveYeaftDir(args, env, options.name);
|
|
32
39
|
return options;
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -127,8 +134,12 @@ function readAgentList(wsUrl) {
|
|
|
127
134
|
|
|
128
135
|
export async function runLocal(args, options = {}) {
|
|
129
136
|
const config = parseLocalArgs(args);
|
|
137
|
+
if (config.background && options.backgroundHandled !== true) {
|
|
138
|
+
return launchLocalInBackground(args, options);
|
|
139
|
+
}
|
|
130
140
|
const paths = options.paths || runtimePaths();
|
|
131
141
|
const dataDir = options.dataDir || join(homedir(), '.yeaft', 'server');
|
|
142
|
+
const yeaftDir = options.yeaftDir || config.yeaftDir;
|
|
132
143
|
const url = `http://${LOCAL_HOST}:${config.port}`;
|
|
133
144
|
const children = new Set();
|
|
134
145
|
const signalHandlers = new Map();
|
|
@@ -173,6 +184,7 @@ export async function runLocal(args, options = {}) {
|
|
|
173
184
|
SERVER_DATA_DIR: dataDir,
|
|
174
185
|
PERF_TRACE_DIR: join(dataDir, 'perf-traces'),
|
|
175
186
|
SKIP_AUTH: 'true',
|
|
187
|
+
YEAFT_LOCAL_RUN: 'true',
|
|
176
188
|
WEB_DIR: paths.webDir,
|
|
177
189
|
},
|
|
178
190
|
});
|
|
@@ -189,6 +201,7 @@ export async function runLocal(args, options = {}) {
|
|
|
189
201
|
AGENT_NAME: config.name,
|
|
190
202
|
YEAFT_AGENT_INSTANCE: config.name,
|
|
191
203
|
AGENT_SECRET: '',
|
|
204
|
+
YEAFT_DIR: yeaftDir,
|
|
192
205
|
YEAFT_LOCAL_RUN: 'true',
|
|
193
206
|
YEAFT_SKIP_STARTUP_INSTALLS: 'true',
|
|
194
207
|
},
|
|
@@ -204,7 +217,7 @@ export async function runLocal(args, options = {}) {
|
|
|
204
217
|
server.once('exit', fail('Local server'));
|
|
205
218
|
agent.once('exit', fail('Local agent'));
|
|
206
219
|
|
|
207
|
-
await waitForAgent(`ws://${LOCAL_HOST}:${config.port}`, config.name, agent);
|
|
220
|
+
await (options.waitForAgent || waitForAgent)(`ws://${LOCAL_HOST}:${config.port}`, config.name, agent);
|
|
208
221
|
|
|
209
222
|
console.log(`Yeaft local is available at ${url}`);
|
|
210
223
|
return { url, server, agent, stop };
|
|
@@ -213,3 +226,29 @@ export async function runLocal(args, options = {}) {
|
|
|
213
226
|
throw error;
|
|
214
227
|
}
|
|
215
228
|
}
|
|
229
|
+
|
|
230
|
+
function localDaemonArgs(args) {
|
|
231
|
+
return args.filter(arg => arg !== '--background' && arg !== '-d');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function launchLocalInBackground(args, options = {}) {
|
|
235
|
+
const config = parseLocalArgs(args);
|
|
236
|
+
const spawnProcess = options.spawn || spawn;
|
|
237
|
+
const cliPath = options.cliPath || join(dirname(fileURLToPath(import.meta.url)), 'cli.js');
|
|
238
|
+
const child = spawnProcess(process.execPath, [cliPath, 'local', ...localDaemonArgs(args)], {
|
|
239
|
+
detached: true,
|
|
240
|
+
stdio: 'ignore',
|
|
241
|
+
windowsHide: true,
|
|
242
|
+
env: {
|
|
243
|
+
...process.env,
|
|
244
|
+
YEAFT_LOCAL_RUN_BACKGROUND: 'true',
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
child.unref();
|
|
248
|
+
const url = `http://${LOCAL_HOST}:${config.port}`;
|
|
249
|
+
const result = { url, pid: child.pid, background: true };
|
|
250
|
+
if (options.quiet !== true) {
|
|
251
|
+
console.log(`Yeaft local is starting in the background at ${url} (PID ${child.pid}).`);
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
@@ -674,7 +674,14 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
674
674
|
}
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
|
-
|
|
677
|
+
// Nested `session_list_updated` events carry the same authoritative
|
|
678
|
+
// snapshot as the top-level alias below. Re-project the server-owned
|
|
679
|
+
// catalog after reconciliation so the unified sidebar updates in both
|
|
680
|
+
// local no-auth and deployed runtimes. Roster changes are handled by
|
|
681
|
+
// their own metadata branch above.
|
|
682
|
+
if ((event?.type === 'session_list_updated' || catalogChanged) && agent.ownerId) {
|
|
683
|
+
await broadcastSessionCatalog(agent.ownerId);
|
|
684
|
+
}
|
|
678
685
|
break;
|
|
679
686
|
}
|
|
680
687
|
|
|
@@ -4,6 +4,7 @@ import { CONFIG } from './config.js';
|
|
|
4
4
|
import { verifyAgent } from './auth.js';
|
|
5
5
|
import { encodeKey } from './encryption.js';
|
|
6
6
|
import { agents, pendingAgentConnections } from './context.js';
|
|
7
|
+
import { userDb } from './database.js';
|
|
7
8
|
import {
|
|
8
9
|
parseMessage, broadcastAgentList, clearAgentDirCache
|
|
9
10
|
} from './ws-utils.js';
|
|
@@ -114,18 +115,37 @@ export function handleAgentConnection(ws, url) {
|
|
|
114
115
|
|
|
115
116
|
const capabilities = Array.isArray(msg.capabilities) ? msg.capabilities : urlCapabilities;
|
|
116
117
|
const agentVersion = msg.version || null;
|
|
118
|
+
// Local no-auth mode still has one durable browser owner. This makes
|
|
119
|
+
// the server-side Session catalog persistent without changing generic
|
|
120
|
+
// development-server behavior, which remains ownerless.
|
|
121
|
+
const localOwner = skipAgentAuth && process.env.YEAFT_LOCAL_RUN === 'true'
|
|
122
|
+
? userDb.getOrCreate('dev-user')
|
|
123
|
+
: null;
|
|
124
|
+
const ownerId = localOwner?.id || authResult.userId;
|
|
125
|
+
const ownerUsername = localOwner?.username || authResult.username;
|
|
117
126
|
// Authenticated Agents use an owner-scoped key. SKIP_AUTH preserves
|
|
118
127
|
// its historical unscoped id while still receiving version metadata.
|
|
119
128
|
resolvedAgentId = skipAgentAuth
|
|
120
129
|
? clientAgentId
|
|
121
|
-
: buildAgentMapKey(
|
|
130
|
+
: buildAgentMapKey(ownerId, pending.instanceId || pending.agentId || pending.agentName);
|
|
122
131
|
if (!claimAgentConnection(resolvedAgentId, connectionGeneration)) {
|
|
123
132
|
resolvedAgentId = null;
|
|
124
133
|
pruneAgentConnectionGenerations();
|
|
125
134
|
ws.close(1008, 'Superseded by a newer Agent connection');
|
|
126
135
|
return;
|
|
127
136
|
}
|
|
128
|
-
completeAgentRegistration(
|
|
137
|
+
completeAgentRegistration(
|
|
138
|
+
ws,
|
|
139
|
+
resolvedAgentId,
|
|
140
|
+
pending.agentName,
|
|
141
|
+
pending.workDir,
|
|
142
|
+
authResult.sessionKey,
|
|
143
|
+
capabilities,
|
|
144
|
+
ownerId,
|
|
145
|
+
ownerUsername,
|
|
146
|
+
agentVersion,
|
|
147
|
+
pending.instanceId || pending.agentId || pending.agentName,
|
|
148
|
+
);
|
|
129
149
|
pruneAgentConnectionGenerations();
|
|
130
150
|
}
|
|
131
151
|
} catch (e) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.359"}
|
package/local-service.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { homedir, platform } from 'os';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import {
|
|
7
|
+
getConfigDir,
|
|
8
|
+
getDefaultYeaftDir,
|
|
9
|
+
getNodePath,
|
|
10
|
+
resolveServiceInstanceId,
|
|
11
|
+
resolveYeaftDir,
|
|
12
|
+
validateInstanceId,
|
|
13
|
+
} from './service/config.js';
|
|
14
|
+
|
|
15
|
+
const LOCAL_SERVICE_PREFIX = 'yeaft-local';
|
|
16
|
+
const DEFAULT_PORT = 6868;
|
|
17
|
+
const SYSTEMD_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
18
|
+
|
|
19
|
+
/** Parse local service identity, runtime port, and resolved persistent data root. */
|
|
20
|
+
export function parseLocalServiceArgs(args, env = process.env, options = {}) {
|
|
21
|
+
const parsed = {
|
|
22
|
+
name: resolveServiceInstanceId(args, env),
|
|
23
|
+
port: DEFAULT_PORT,
|
|
24
|
+
yeaftDir: null,
|
|
25
|
+
};
|
|
26
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
27
|
+
const arg = args[index];
|
|
28
|
+
const value = args[index + 1];
|
|
29
|
+
if (arg === '--name' || arg === '--instance' || arg === '--yeaft-dir') {
|
|
30
|
+
if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`);
|
|
31
|
+
index += 1;
|
|
32
|
+
} else if (arg === '--port') {
|
|
33
|
+
if (!value || value.startsWith('-')) throw new Error('--port requires a value');
|
|
34
|
+
if (!/^\d+$/.test(value)) throw new Error(`Invalid port: ${value}`);
|
|
35
|
+
parsed.port = Number(value);
|
|
36
|
+
if (parsed.port < 1 || parsed.port > 65535) throw new Error(`Invalid port: ${value}`);
|
|
37
|
+
index += 1;
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(`Unknown local service option: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
parsed.name = validateInstanceId(parsed.name);
|
|
43
|
+
const existing = options.existing || null;
|
|
44
|
+
const hasExplicitYeaftDir = args.includes('--yeaft-dir') || Boolean(env.YEAFT_DIR);
|
|
45
|
+
parsed.yeaftDir = hasExplicitYeaftDir
|
|
46
|
+
? resolveYeaftDir(args, env, parsed.name)
|
|
47
|
+
: existing?.yeaftDir || getDefaultYeaftDir(parsed.name);
|
|
48
|
+
assertSystemdValue(parsed.yeaftDir, 'Yeaft data directory');
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertSystemdValue(value, label) {
|
|
53
|
+
if (SYSTEMD_CONTROL_CHARACTERS.test(String(value))) {
|
|
54
|
+
throw new Error(`${label} cannot contain control characters when installing a systemd service`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getLocalServiceName(name) {
|
|
59
|
+
return `${LOCAL_SERVICE_PREFIX}@${validateInstanceId(name)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getLocalServiceConfigPath(name) {
|
|
63
|
+
return join(getConfigDir(name), 'local.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getLocalSystemdServicePath(name) {
|
|
67
|
+
return join(homedir(), '.config', 'systemd', 'user', `${getLocalServiceName(name)}.service`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getLocalSystemdUnitPath(name) {
|
|
71
|
+
return getLocalSystemdServicePath(name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function shellQuote(value) {
|
|
75
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function systemdEscape(value) {
|
|
79
|
+
assertSystemdValue(value, 'systemd unit value');
|
|
80
|
+
return String(value)
|
|
81
|
+
.replaceAll('%', '%%')
|
|
82
|
+
.replaceAll('\\', '\\\\')
|
|
83
|
+
.replaceAll('"', '\\"');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Read the persisted local service settings, including its resolved data root. */
|
|
87
|
+
export function readLocalServiceConfig(name) {
|
|
88
|
+
const path = getLocalServiceConfigPath(name);
|
|
89
|
+
if (!existsSync(path)) return null;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
92
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
93
|
+
const configName = validateInstanceId(parsed.name || name);
|
|
94
|
+
return {
|
|
95
|
+
name: configName,
|
|
96
|
+
port: Number(parsed.port) || DEFAULT_PORT,
|
|
97
|
+
yeaftDir: typeof parsed.yeaftDir === 'string' && parsed.yeaftDir
|
|
98
|
+
? parsed.yeaftDir
|
|
99
|
+
: getDefaultYeaftDir(configName),
|
|
100
|
+
};
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Persist the local service settings selected at install time. */
|
|
107
|
+
export function writeLocalServiceConfig(config) {
|
|
108
|
+
const path = getLocalServiceConfigPath(config.name);
|
|
109
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
110
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requireLocalServiceConfig(name) {
|
|
114
|
+
const config = readLocalServiceConfig(name);
|
|
115
|
+
if (!config) {
|
|
116
|
+
throw new Error(`Local service not installed for ${name}. Run "yeaft-agent local install --name ${name}" first.`);
|
|
117
|
+
}
|
|
118
|
+
return config;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function generateLocalSystemdUnit(config, options = {}) {
|
|
122
|
+
const nodePath = getNodePath();
|
|
123
|
+
const cliPath = options.cliPath || join(dirname(fileURLToPath(import.meta.url)), 'cli.js');
|
|
124
|
+
const localDataDir = options.dataDir || join(homedir(), '.yeaft', 'server');
|
|
125
|
+
const yeaftDir = config.yeaftDir || getDefaultYeaftDir(config.name);
|
|
126
|
+
const logDir = join(getConfigDir(config.name), 'logs');
|
|
127
|
+
const workingDirectory = options.workingDirectory || dirname(cliPath);
|
|
128
|
+
const command = [
|
|
129
|
+
shellQuote(nodePath),
|
|
130
|
+
shellQuote(cliPath),
|
|
131
|
+
'local',
|
|
132
|
+
'--name',
|
|
133
|
+
shellQuote(config.name),
|
|
134
|
+
'--port',
|
|
135
|
+
String(config.port),
|
|
136
|
+
].join(' ');
|
|
137
|
+
return `[Unit]
|
|
138
|
+
Description=Yeaft Local Web UI (${config.name})
|
|
139
|
+
After=network-online.target
|
|
140
|
+
Wants=network-online.target
|
|
141
|
+
|
|
142
|
+
[Service]
|
|
143
|
+
Type=simple
|
|
144
|
+
ExecStart=${command}
|
|
145
|
+
Restart=on-failure
|
|
146
|
+
RestartSec=5
|
|
147
|
+
WorkingDirectory=${workingDirectory}
|
|
148
|
+
Environment="YEAFT_LOCAL_RUN=true"
|
|
149
|
+
Environment="YEAFT_AGENT_INSTANCE=${systemdEscape(config.name)}"
|
|
150
|
+
Environment="YEAFT_DIR=${systemdEscape(yeaftDir)}"
|
|
151
|
+
Environment="SERVER_DATA_DIR=${systemdEscape(localDataDir)}"
|
|
152
|
+
StandardOutput=append:${logDir}/out.log
|
|
153
|
+
StandardError=append:${logDir}/error.log
|
|
154
|
+
|
|
155
|
+
[Install]
|
|
156
|
+
WantedBy=default.target
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function installLinux(config) {
|
|
161
|
+
const path = getLocalSystemdServicePath(config.name);
|
|
162
|
+
const logDir = join(getConfigDir(config.name), 'logs');
|
|
163
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
164
|
+
mkdirSync(logDir, { recursive: true });
|
|
165
|
+
writeFileSync(path, generateLocalSystemdUnit(config));
|
|
166
|
+
execSync('systemctl --user daemon-reload');
|
|
167
|
+
execSync(`systemctl --user enable --now ${getLocalServiceName(config.name)}`);
|
|
168
|
+
console.log(`Local service installed and started: ${getLocalServiceName(config.name)}`);
|
|
169
|
+
console.log(`Open http://127.0.0.1:${config.port}`);
|
|
170
|
+
console.log(`For boot without an active login session: sudo loginctl enable-linger $(whoami)`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function uninstallLinux(name) {
|
|
174
|
+
const serviceName = getLocalServiceName(name);
|
|
175
|
+
const path = getLocalSystemdServicePath(name);
|
|
176
|
+
try { execSync(`systemctl --user disable --now ${serviceName}`, { stdio: 'ignore' }); } catch {}
|
|
177
|
+
if (existsSync(path)) unlinkSync(path);
|
|
178
|
+
try { execSync('systemctl --user daemon-reload'); } catch {}
|
|
179
|
+
console.log(`Local service uninstalled: ${serviceName}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function controlLinux(command, name) {
|
|
183
|
+
const serviceName = getLocalServiceName(name);
|
|
184
|
+
if (command === 'status') {
|
|
185
|
+
try { execSync(`systemctl --user status ${serviceName} --no-pager`, { stdio: 'inherit' }); } catch {}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (command === 'logs') {
|
|
189
|
+
execSync(`journalctl --user -u ${serviceName} -f --no-pager -n 100`, { stdio: 'inherit' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
execSync(`systemctl --user ${command} ${serviceName}`, { stdio: 'inherit' });
|
|
193
|
+
const verb = command === 'start' ? 'started'
|
|
194
|
+
: command === 'stop' ? 'stopped'
|
|
195
|
+
: command === 'restart' ? 'restarted'
|
|
196
|
+
: command;
|
|
197
|
+
console.log(`Local service ${verb}: ${serviceName}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function handleLocalServiceCommand(command, args = []) {
|
|
201
|
+
const identity = parseLocalServiceArgs(args);
|
|
202
|
+
if (platform() !== 'linux') {
|
|
203
|
+
throw new Error(`Local managed service is currently supported on Linux only (current platform: ${platform()}). Use \`yeaft-agent local --background\` on this platform.`);
|
|
204
|
+
}
|
|
205
|
+
if (command === 'install') {
|
|
206
|
+
const config = parseLocalServiceArgs(args, process.env, {
|
|
207
|
+
existing: readLocalServiceConfig(identity.name),
|
|
208
|
+
});
|
|
209
|
+
writeLocalServiceConfig(config);
|
|
210
|
+
installLinux(config);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (command === 'uninstall') {
|
|
214
|
+
uninstallLinux(identity.name);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const config = requireLocalServiceConfig(identity.name);
|
|
218
|
+
controlLinux(command, config.name);
|
|
219
|
+
}
|
package/package.json
CHANGED
package/service/config.js
CHANGED
|
@@ -98,6 +98,10 @@ export function resolveRuntimeIdentity(fileConfig = {}, env = process.env) {
|
|
|
98
98
|
return { agentName, instanceId };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export function shouldLoadLegacyLocalConfig(env = process.env) {
|
|
102
|
+
return !String(env.YEAFT_AGENT_INSTANCE || '').trim();
|
|
103
|
+
}
|
|
104
|
+
|
|
101
105
|
export function resolveServiceInstanceId(args = [], env = process.env, options = {}) {
|
|
102
106
|
const { deprecatedInstanceId, explicitName } = readIdentityArgs(args);
|
|
103
107
|
if (explicitName) return explicitName;
|
|
@@ -108,6 +112,19 @@ export function resolveServiceInstanceId(args = [], env = process.env, options =
|
|
|
108
112
|
return validateInstanceId(options.fallbackName || getDefaultAgentName());
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
/** Resolve the CLI/env/default Yeaft data root for one Agent instance. */
|
|
116
|
+
export function resolveYeaftDir(args = [], env = process.env, instanceId = DEFAULT_INSTANCE_ID) {
|
|
117
|
+
let explicitYeaftDir = null;
|
|
118
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
119
|
+
if (args[index] !== '--yeaft-dir') continue;
|
|
120
|
+
const value = args[index + 1];
|
|
121
|
+
if (!value || value.startsWith('-')) throw new Error('--yeaft-dir requires a value');
|
|
122
|
+
explicitYeaftDir = value;
|
|
123
|
+
index += 1;
|
|
124
|
+
}
|
|
125
|
+
return explicitYeaftDir || env.YEAFT_DIR || getDefaultYeaftDir(instanceId);
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
/** Legacy alias for resolveServiceInstanceId(). */
|
|
112
129
|
export function getInstanceIdFromArgs(args = [], env = process.env, options = {}) {
|
|
113
130
|
return resolveServiceInstanceId(args, env, options);
|
package/service/index.js
CHANGED