@agent-link/agent 0.1.287 → 0.1.288
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/dist/auto-start.d.ts +7 -0
- package/dist/auto-start.js +141 -0
- package/dist/auto-start.js.map +1 -0
- package/dist/cli.js +53 -13
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/launch-spec.d.ts +10 -0
- package/dist/launch-spec.js +34 -0
- package/dist/launch-spec.js.map +1 -1
- package/dist/service.d.ts +28 -4
- package/dist/service.js +366 -266
- package/dist/service.js.map +1 -1
- package/dist/start-launch-spec.js +5 -5
- package/dist/start-launch-spec.js.map +1 -1
- package/dist/upgrade-launch-spec.d.ts +3 -2
- package/dist/upgrade-launch-spec.js +24 -7
- package/dist/upgrade-launch-spec.js.map +1 -1
- package/package.json +1 -1
package/dist/service.js
CHANGED
|
@@ -1,20 +1,55 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync,
|
|
3
|
-
import {
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'fs';
|
|
3
|
+
import { dirname, join, resolve } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { CONFIG_DIR,
|
|
7
|
-
import {
|
|
6
|
+
import { CONFIG_DIR, clearRuntimeState, getLogDir, isProcessAlive, killProcess, loadRuntimeState, saveConfig, saveRuntimeState, } from './config.js';
|
|
7
|
+
import { computeLaunchSpecDigest, createLaunchSpec, createServiceLaunchSpec, getServiceLaunchSpecPath, saveLaunchSpec, saveLaunchSpecSnapshot, loadLaunchSpec, } from './launch-spec.js';
|
|
8
|
+
import { getCachedPathEnvVars, getResolvedExecPaths, warmExecPaths } from './sdk.js';
|
|
8
9
|
const SERVICE_NAME = 'agentlink';
|
|
9
10
|
const LAUNCHD_LABEL = 'com.agentlink.agent';
|
|
10
|
-
|
|
11
|
+
function getCliPath() {
|
|
12
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), 'cli.js');
|
|
13
|
+
}
|
|
14
|
+
function getServiceRoot() {
|
|
15
|
+
return process.env.AGENTLINK_SERVICE_ROOT
|
|
16
|
+
? resolve(process.env.AGENTLINK_SERVICE_ROOT)
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
function isSimulation() {
|
|
20
|
+
return getServiceRoot() !== null;
|
|
21
|
+
}
|
|
22
|
+
export function getServiceArtifactPath(platform = process.platform) {
|
|
23
|
+
const root = getServiceRoot();
|
|
24
|
+
if (root) {
|
|
25
|
+
if (platform === 'linux')
|
|
26
|
+
return join(root, `${SERVICE_NAME}.service`);
|
|
27
|
+
if (platform === 'darwin')
|
|
28
|
+
return join(root, `${LAUNCHD_LABEL}.plist`);
|
|
29
|
+
if (platform === 'win32')
|
|
30
|
+
return join(root, `${SERVICE_NAME}.bat`);
|
|
31
|
+
}
|
|
32
|
+
if (platform === 'linux') {
|
|
33
|
+
return join(homedir(), '.config', 'systemd', 'user', `${SERVICE_NAME}.service`);
|
|
34
|
+
}
|
|
35
|
+
if (platform === 'darwin') {
|
|
36
|
+
return join(homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
37
|
+
}
|
|
38
|
+
if (platform === 'win32') {
|
|
39
|
+
const startupDir = join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
40
|
+
return join(startupDir, `${SERVICE_NAME}.bat`);
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
43
|
+
}
|
|
44
|
+
export function getServiceEnvironment(hostType) {
|
|
11
45
|
return {
|
|
12
46
|
AGENTLINK_SERVICE: '1',
|
|
13
47
|
AGENTLINK_HOME: CONFIG_DIR,
|
|
14
48
|
...getCachedPathEnvVars(),
|
|
49
|
+
...(hostType ? { AGENTLINK_HOST_TYPE: hostType } : {}),
|
|
15
50
|
};
|
|
16
51
|
}
|
|
17
|
-
function
|
|
52
|
+
function escapeSystemd(value) {
|
|
18
53
|
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%');
|
|
19
54
|
}
|
|
20
55
|
function escapeXml(value) {
|
|
@@ -25,154 +60,54 @@ function escapeXml(value) {
|
|
|
25
60
|
.replace(/"/g, '"')
|
|
26
61
|
.replace(/'/g, ''');
|
|
27
62
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
return resolve(dirname(__filename), 'cli.js');
|
|
31
|
-
}
|
|
32
|
-
function getNodePath() {
|
|
33
|
-
return process.execPath;
|
|
63
|
+
function quoteSystemd(value) {
|
|
64
|
+
return `"${escapeSystemd(value)}"`;
|
|
34
65
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (config.msentra)
|
|
41
|
-
args += ' --msentra';
|
|
42
|
-
else if (config.entra)
|
|
43
|
-
args += ' --entra';
|
|
44
|
-
return args;
|
|
45
|
-
}
|
|
46
|
-
/** Build extra plist <string> entries for password (used in launchd template). */
|
|
47
|
-
function buildPlistExtraArgs(config) {
|
|
48
|
-
let entries = '';
|
|
49
|
-
if (config.password) {
|
|
50
|
-
entries += ` <string>--password</string>\n <string>${config.password}</string>\n`;
|
|
51
|
-
}
|
|
52
|
-
if (config.msentra) {
|
|
53
|
-
entries += ` <string>--msentra</string>\n`;
|
|
54
|
-
}
|
|
55
|
-
else if (config.entra) {
|
|
56
|
-
entries += ` <string>--entra</string>\n`;
|
|
66
|
+
function renderEnvironment(environment, format) {
|
|
67
|
+
if (format === 'systemd') {
|
|
68
|
+
return Object.entries(environment)
|
|
69
|
+
.map(([name, value]) => `Environment="${name}=${escapeSystemd(value)}"`)
|
|
70
|
+
.join('\n');
|
|
57
71
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return process.getuid?.() === 0;
|
|
63
|
-
}
|
|
64
|
-
function getSystemdUnitPath() {
|
|
65
|
-
if (isRoot()) {
|
|
66
|
-
return join('/etc/systemd/system', `${SERVICE_NAME}.service`);
|
|
72
|
+
if (format === 'xml') {
|
|
73
|
+
return Object.entries(environment)
|
|
74
|
+
.map(([name, value]) => ` <key>${escapeXml(name)}</key>\n <string>${escapeXml(value)}</string>`)
|
|
75
|
+
.join('\n');
|
|
67
76
|
}
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const cmd = isRoot()
|
|
72
|
-
? `systemctl ${args.join(' ')}`
|
|
73
|
-
: `systemctl --user ${args.join(' ')}`;
|
|
74
|
-
execSync(cmd);
|
|
77
|
+
return Object.entries(environment)
|
|
78
|
+
.map(([name, value]) => `set "${name}=${value.replace(/%/g, '%%')}"`)
|
|
79
|
+
.join('\r\n');
|
|
75
80
|
}
|
|
76
|
-
function
|
|
77
|
-
const nodePath =
|
|
81
|
+
export function renderServiceArtifact(spec, specPath, platform = process.platform) {
|
|
82
|
+
const nodePath = process.execPath;
|
|
78
83
|
const cliPath = getCliPath();
|
|
79
84
|
const logDir = getLogDir();
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
? ''
|
|
84
|
-
: `Environment=PATH=${nodeBinDir}:${homedir()}/.local/bin:${homedir()}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin\n`;
|
|
85
|
-
const environment = {
|
|
86
|
-
...getServiceEnvironment(),
|
|
87
|
-
AGENTLINK_HOST_TYPE: 'systemd',
|
|
88
|
-
};
|
|
89
|
-
const environmentSection = Object.entries(environment)
|
|
90
|
-
.map(([name, value]) => `Environment="${name}=${escapeSystemdEnvironment(value)}"`)
|
|
91
|
-
.join('\n');
|
|
92
|
-
return `[Unit]
|
|
85
|
+
const environment = getServiceEnvironment(spec.host.type);
|
|
86
|
+
if (platform === 'linux') {
|
|
87
|
+
return `[Unit]
|
|
93
88
|
Description=AgentLink Agent
|
|
94
89
|
After=network-online.target
|
|
95
90
|
Wants=network-online.target
|
|
96
91
|
|
|
97
92
|
[Service]
|
|
98
93
|
Type=simple
|
|
99
|
-
ExecStart=${nodePath} ${cliPath} start --
|
|
100
|
-
WorkingDirectory=${config.dir}
|
|
94
|
+
ExecStart=${quoteSystemd(nodePath)} ${quoteSystemd(cliPath)} start --launch-spec ${quoteSystemd(specPath)}
|
|
95
|
+
WorkingDirectory=${quoteSystemd(spec.config.dir)}
|
|
101
96
|
Restart=on-failure
|
|
102
97
|
RestartSec=10
|
|
103
|
-
${
|
|
104
|
-
|
|
98
|
+
${renderEnvironment(environment, 'systemd')}
|
|
99
|
+
StandardOutput=append:${logDir}/agent.log
|
|
105
100
|
StandardError=append:${logDir}/agent.err
|
|
106
101
|
|
|
107
102
|
[Install]
|
|
108
|
-
WantedBy
|
|
103
|
+
WantedBy=default.target
|
|
109
104
|
`;
|
|
110
|
-
}
|
|
111
|
-
function linuxInstall(config) {
|
|
112
|
-
const unitPath = getSystemdUnitPath();
|
|
113
|
-
mkdirSync(dirname(unitPath), { recursive: true });
|
|
114
|
-
mkdirSync(getLogDir(), { recursive: true });
|
|
115
|
-
writeFileSync(unitPath, generateSystemdUnit(config));
|
|
116
|
-
systemctl('daemon-reload');
|
|
117
|
-
systemctl('enable', SERVICE_NAME);
|
|
118
|
-
// Only start via systemd if no agent is already running
|
|
119
|
-
const state = loadRuntimeState();
|
|
120
|
-
if (state?.pid && isProcessAlive(state.pid)) {
|
|
121
|
-
console.log(`Agent is already running (PID ${state.pid}). Skipping start.`);
|
|
122
|
-
console.log(`Service will manage the agent on next boot.`);
|
|
123
105
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const jctl = isRoot() ? 'journalctl' : 'journalctl --user';
|
|
130
|
-
console.log(`Service installed and started (${scope}-level).`);
|
|
131
|
-
console.log(`\nUnit file: ${unitPath}`);
|
|
132
|
-
console.log('\nUseful commands:');
|
|
133
|
-
console.log(` ${ctl} status ${SERVICE_NAME}`);
|
|
134
|
-
console.log(` ${jctl} -u ${SERVICE_NAME} -f`);
|
|
135
|
-
if (!isRoot()) {
|
|
136
|
-
console.log('\nTo run when not logged in:');
|
|
137
|
-
console.log(' sudo loginctl enable-linger $(whoami)');
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
function linuxUninstall() {
|
|
141
|
-
try {
|
|
142
|
-
systemctl('stop', `${SERVICE_NAME} 2>/dev/null`);
|
|
143
|
-
}
|
|
144
|
-
catch { }
|
|
145
|
-
try {
|
|
146
|
-
systemctl('disable', `${SERVICE_NAME} 2>/dev/null`);
|
|
147
|
-
}
|
|
148
|
-
catch { }
|
|
149
|
-
const unitPath = getSystemdUnitPath();
|
|
150
|
-
if (existsSync(unitPath)) {
|
|
151
|
-
unlinkSync(unitPath);
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
systemctl('daemon-reload');
|
|
155
|
-
}
|
|
156
|
-
catch { }
|
|
157
|
-
console.log('Service uninstalled.');
|
|
158
|
-
}
|
|
159
|
-
// ── macOS (launchd plist) ──
|
|
160
|
-
function getLaunchdPlistPath() {
|
|
161
|
-
return join(homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
162
|
-
}
|
|
163
|
-
function generateLaunchdPlist(config) {
|
|
164
|
-
const nodePath = getNodePath();
|
|
165
|
-
const cliPath = getCliPath();
|
|
166
|
-
const logDir = getLogDir();
|
|
167
|
-
const extraEntries = buildPlistExtraArgs(config);
|
|
168
|
-
const environment = {
|
|
169
|
-
...getServiceEnvironment(),
|
|
170
|
-
AGENTLINK_HOST_TYPE: 'launchd',
|
|
171
|
-
};
|
|
172
|
-
const environmentEntries = Object.entries(environment)
|
|
173
|
-
.map(([name, value]) => ` <key>${name}</key>\n <string>${escapeXml(value)}</string>`)
|
|
174
|
-
.join('\n');
|
|
175
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
106
|
+
if (platform === 'darwin') {
|
|
107
|
+
const args = [nodePath, cliPath, 'start', '--launch-spec', specPath]
|
|
108
|
+
.map(value => ` <string>${escapeXml(value)}</string>`)
|
|
109
|
+
.join('\n');
|
|
110
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
176
111
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
177
112
|
<plist version="1.0">
|
|
178
113
|
<dict>
|
|
@@ -180,21 +115,13 @@ function generateLaunchdPlist(config) {
|
|
|
180
115
|
<string>${LAUNCHD_LABEL}</string>
|
|
181
116
|
<key>ProgramArguments</key>
|
|
182
117
|
<array>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<string>start</string>
|
|
186
|
-
<string>--server</string>
|
|
187
|
-
<string>${config.server}</string>
|
|
188
|
-
<string>--dir</string>
|
|
189
|
-
<string>${config.dir}</string>
|
|
190
|
-
<string>--name</string>
|
|
191
|
-
<string>${config.name}</string>
|
|
192
|
-
${extraEntries} </array>
|
|
118
|
+
${args}
|
|
119
|
+
</array>
|
|
193
120
|
<key>WorkingDirectory</key>
|
|
194
|
-
<string>${config.dir}</string>
|
|
121
|
+
<string>${escapeXml(spec.config.dir)}</string>
|
|
195
122
|
<key>EnvironmentVariables</key>
|
|
196
123
|
<dict>
|
|
197
|
-
${
|
|
124
|
+
${renderEnvironment(environment, 'xml')}
|
|
198
125
|
</dict>
|
|
199
126
|
<key>RunAtLoad</key>
|
|
200
127
|
<true/>
|
|
@@ -206,151 +133,324 @@ ${environmentEntries}
|
|
|
206
133
|
<key>ThrottleInterval</key>
|
|
207
134
|
<integer>10</integer>
|
|
208
135
|
<key>StandardOutPath</key>
|
|
209
|
-
<string>${logDir
|
|
136
|
+
<string>${escapeXml(join(logDir, 'agent.log'))}</string>
|
|
210
137
|
<key>StandardErrorPath</key>
|
|
211
|
-
<string>${logDir
|
|
138
|
+
<string>${escapeXml(join(logDir, 'agent.err'))}</string>
|
|
212
139
|
</dict>
|
|
213
140
|
</plist>
|
|
214
141
|
`;
|
|
142
|
+
}
|
|
143
|
+
if (platform === 'win32') {
|
|
144
|
+
return `@echo off\r\n${renderEnvironment(environment, 'bat')}\r\n"${nodePath}" "${cliPath}" start --launch-spec "${specPath}"\r\n`;
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
215
147
|
}
|
|
216
|
-
function
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
148
|
+
function writeTextAtomic(filePath, content) {
|
|
149
|
+
const previous = existsSync(filePath) ? readFileSync(filePath, 'utf8') : null;
|
|
150
|
+
if (previous === content)
|
|
151
|
+
return false;
|
|
152
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
153
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
154
|
+
writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600 });
|
|
155
|
+
try {
|
|
156
|
+
renameSync(tempPath, filePath);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
rmSync(tempPath, { force: true });
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
function runSystemctl(args) {
|
|
165
|
+
execFileSync('systemctl', ['--user', ...args], { stdio: 'ignore' });
|
|
166
|
+
}
|
|
167
|
+
function getLaunchdTarget() {
|
|
168
|
+
const uid = process.getuid?.();
|
|
169
|
+
if (uid === undefined)
|
|
170
|
+
throw new Error('launchd user id is unavailable');
|
|
171
|
+
return `gui/${uid}/${LAUNCHD_LABEL}`;
|
|
172
|
+
}
|
|
173
|
+
function getLegacySystemdUnitPath() {
|
|
174
|
+
return process.env.AGENTLINK_LEGACY_SYSTEMD_UNIT
|
|
175
|
+
? resolve(process.env.AGENTLINK_LEGACY_SYSTEMD_UNIT)
|
|
176
|
+
: join('/etc', 'systemd', 'system', `${SERVICE_NAME}.service`);
|
|
177
|
+
}
|
|
178
|
+
function removeLegacySystemdService(stopAgent) {
|
|
179
|
+
const legacyPath = getLegacySystemdUnitPath();
|
|
180
|
+
if (!existsSync(legacyPath))
|
|
181
|
+
return;
|
|
182
|
+
try {
|
|
183
|
+
if (stopAgent) {
|
|
184
|
+
execFileSync('systemctl', ['stop', SERVICE_NAME], { stdio: 'ignore' });
|
|
223
185
|
}
|
|
224
|
-
|
|
186
|
+
execFileSync('systemctl', ['disable', SERVICE_NAME], { stdio: 'ignore' });
|
|
187
|
+
unlinkSync(legacyPath);
|
|
188
|
+
execFileSync('systemctl', ['daemon-reload'], { stdio: 'ignore' });
|
|
225
189
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (state?.pid && isProcessAlive(state.pid)) {
|
|
230
|
-
console.log('Service installed. Agent is already running — skipping start.');
|
|
231
|
-
console.log(`Service will manage the agent on next boot.`);
|
|
190
|
+
catch {
|
|
191
|
+
throw new Error(`Cannot disable legacy system-level service at ${legacyPath}; `
|
|
192
|
+
+ 'run the service uninstall command with administrator privileges');
|
|
232
193
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
194
|
+
}
|
|
195
|
+
function startFromServiceLaunchSpec(specPath) {
|
|
196
|
+
execFileSync(process.execPath, [getCliPath(), 'start', '--launch-spec', specPath], {
|
|
197
|
+
stdio: 'inherit',
|
|
198
|
+
windowsHide: true,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function preserveRuntimeServiceSpecBeforeReplace(nextSpec) {
|
|
202
|
+
const state = loadRuntimeState();
|
|
203
|
+
const serviceSpecPath = getServiceLaunchSpecPath();
|
|
204
|
+
if (!state?.launchSpecPath
|
|
205
|
+
|| resolve(state.launchSpecPath) !== resolve(serviceSpecPath)
|
|
206
|
+
|| !existsSync(serviceSpecPath)) {
|
|
207
|
+
return;
|
|
236
208
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
209
|
+
const currentSpec = loadLaunchSpec(serviceSpecPath);
|
|
210
|
+
if (JSON.stringify(currentSpec) === JSON.stringify(nextSpec))
|
|
211
|
+
return;
|
|
212
|
+
const launchSpecPath = saveLaunchSpecSnapshot(currentSpec);
|
|
213
|
+
saveRuntimeState({
|
|
214
|
+
...state,
|
|
215
|
+
launchSpecPath,
|
|
216
|
+
launchSpecDigest: computeLaunchSpecDigest(currentSpec),
|
|
217
|
+
});
|
|
240
218
|
}
|
|
241
|
-
function
|
|
242
|
-
const
|
|
243
|
-
if (existsSync(
|
|
244
|
-
|
|
245
|
-
|
|
219
|
+
export function installServiceLaunchSpec(source, options = {}) {
|
|
220
|
+
const platform = options.platform ?? process.platform;
|
|
221
|
+
if (platform === 'linux' && !isSimulation() && existsSync(getLegacySystemdUnitPath())) {
|
|
222
|
+
throw new Error(`Legacy system-level service detected at ${getLegacySystemdUnitPath()}; `
|
|
223
|
+
+ 'remove it explicitly before enabling user-level auto-start');
|
|
224
|
+
}
|
|
225
|
+
const specPath = getServiceLaunchSpecPath();
|
|
226
|
+
const spec = createServiceLaunchSpec(source, platform);
|
|
227
|
+
const artifactPath = getServiceArtifactPath(platform);
|
|
228
|
+
const previousContent = existsSync(artifactPath) ? readFileSync(artifactPath, 'utf8') : null;
|
|
229
|
+
const existed = previousContent !== null;
|
|
230
|
+
const content = renderServiceArtifact(spec, specPath, platform);
|
|
231
|
+
preserveRuntimeServiceSpecBeforeReplace(spec);
|
|
232
|
+
const changed = writeTextAtomic(artifactPath, content);
|
|
233
|
+
// Write the referenced LaunchSpec only after the host entry is safely staged.
|
|
234
|
+
// If this fails, the host entry still resolves to the previous valid spec.
|
|
235
|
+
saveLaunchSpec(spec, specPath);
|
|
236
|
+
const state = loadRuntimeState();
|
|
237
|
+
const running = !!state?.pid && isProcessAlive(state.pid);
|
|
238
|
+
let pendingReload = platform === 'darwin'
|
|
239
|
+
&& running
|
|
240
|
+
&& existed
|
|
241
|
+
&& (changed || !previousContent.includes('--launch-spec'));
|
|
242
|
+
if (!isSimulation()) {
|
|
243
|
+
if (platform === 'linux') {
|
|
244
|
+
try {
|
|
245
|
+
runSystemctl(['unmask', SERVICE_NAME]);
|
|
246
|
+
}
|
|
247
|
+
catch { /* not masked */ }
|
|
248
|
+
runSystemctl(['daemon-reload']);
|
|
249
|
+
runSystemctl(['enable', SERVICE_NAME]);
|
|
250
|
+
if (options.startIfStopped !== false && !running)
|
|
251
|
+
runSystemctl(['start', SERVICE_NAME]);
|
|
252
|
+
}
|
|
253
|
+
else if (platform === 'darwin') {
|
|
254
|
+
execFileSync('launchctl', ['enable', getLaunchdTarget()], { stdio: 'ignore' });
|
|
255
|
+
if (!running) {
|
|
256
|
+
if (existed) {
|
|
257
|
+
try {
|
|
258
|
+
execFileSync('launchctl', ['unload', artifactPath], { stdio: 'ignore' });
|
|
259
|
+
}
|
|
260
|
+
catch { /* stale/unloaded plist */ }
|
|
261
|
+
}
|
|
262
|
+
if (options.startIfStopped !== false) {
|
|
263
|
+
execFileSync('launchctl', ['load', artifactPath], { stdio: 'ignore' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (pendingReload) {
|
|
267
|
+
// launchd keeps the loaded job definition independently from the file.
|
|
268
|
+
// Replacing it live would terminate this agent before it could reload
|
|
269
|
+
// the job, so defer the one-time legacy migration to the next login.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (platform === 'win32') {
|
|
273
|
+
if (options.startIfStopped !== false && !running)
|
|
274
|
+
startFromServiceLaunchSpec(specPath);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
246
278
|
}
|
|
247
|
-
catch { }
|
|
248
|
-
unlinkSync(plistPath);
|
|
249
279
|
}
|
|
250
|
-
|
|
280
|
+
if (options.persistPolicy !== false)
|
|
281
|
+
saveConfig({ autoStart: true });
|
|
282
|
+
return { artifactPath, specPath, changed, spec, pendingReload };
|
|
251
283
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
284
|
+
export function isServiceInstalled(platform = process.platform) {
|
|
285
|
+
try {
|
|
286
|
+
if (platform === 'linux' && !isSimulation() && existsSync(getLegacySystemdUnitPath())) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return existsSync(getServiceArtifactPath(platform));
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
256
294
|
}
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
295
|
+
export function serviceInstall(config) {
|
|
296
|
+
warmExecPaths();
|
|
297
|
+
const source = createLaunchSpec(config, {
|
|
298
|
+
mode: 'daemon',
|
|
299
|
+
hostType: 'standalone',
|
|
300
|
+
resolvedExecPaths: getResolvedExecPaths(),
|
|
301
|
+
});
|
|
302
|
+
const result = installServiceLaunchSpec(source, {
|
|
303
|
+
startIfStopped: true,
|
|
304
|
+
persistPolicy: true,
|
|
305
|
+
});
|
|
306
|
+
console.log('AgentLink auto-start installed.');
|
|
307
|
+
console.log(` LaunchSpec: ${result.specPath}`);
|
|
308
|
+
console.log(` Host entry: ${result.artifactPath}`);
|
|
269
309
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
console.log('Startup script installed.');
|
|
275
|
-
console.log(`\nStartup file: ${batPath}`);
|
|
276
|
-
// Only start the agent if one isn't already running
|
|
310
|
+
export function serviceUninstall(options = {}) {
|
|
311
|
+
const platform = options.platform ?? process.platform;
|
|
312
|
+
const artifactPath = getServiceArtifactPath(platform);
|
|
313
|
+
const stopAgent = options.stopAgent !== false;
|
|
277
314
|
const state = loadRuntimeState();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
315
|
+
const running = !!state?.pid && isProcessAlive(state.pid);
|
|
316
|
+
const serviceSpecPath = getServiceLaunchSpecPath();
|
|
317
|
+
const runtimeUsesServiceSpec = !!state?.launchSpecPath
|
|
318
|
+
&& resolve(state.launchSpecPath) === resolve(serviceSpecPath)
|
|
319
|
+
&& existsSync(serviceSpecPath);
|
|
320
|
+
let pendingReload = platform === 'darwin'
|
|
321
|
+
&& !stopAgent
|
|
322
|
+
&& running
|
|
323
|
+
&& runtimeUsesServiceSpec
|
|
324
|
+
&& existsSync(artifactPath);
|
|
325
|
+
const preserveRunningHostEntry = !stopAgent
|
|
326
|
+
&& running
|
|
327
|
+
&& runtimeUsesServiceSpec
|
|
328
|
+
&& (platform === 'linux' || platform === 'darwin');
|
|
329
|
+
if (!isSimulation()) {
|
|
330
|
+
if (platform === 'linux') {
|
|
331
|
+
removeLegacySystemdService(stopAgent);
|
|
332
|
+
if (stopAgent) {
|
|
333
|
+
try {
|
|
334
|
+
runSystemctl(['stop', SERVICE_NAME]);
|
|
335
|
+
}
|
|
336
|
+
catch { /* not active */ }
|
|
337
|
+
}
|
|
338
|
+
if (!stopAgent && running) {
|
|
339
|
+
// The unit file is intentionally retained for the current process and
|
|
340
|
+
// future upgrade diagnostics, so both controls must succeed before the
|
|
341
|
+
// opt-out can be recorded.
|
|
342
|
+
runSystemctl(['disable', SERVICE_NAME]);
|
|
343
|
+
runSystemctl(['mask', '--runtime', SERVICE_NAME]);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
try {
|
|
347
|
+
runSystemctl(['disable', SERVICE_NAME]);
|
|
348
|
+
}
|
|
349
|
+
catch { /* not enabled */ }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (platform === 'darwin') {
|
|
353
|
+
if (stopAgent && existsSync(artifactPath)) {
|
|
354
|
+
try {
|
|
355
|
+
execFileSync('launchctl', ['unload', artifactPath], { stdio: 'ignore' });
|
|
356
|
+
}
|
|
357
|
+
catch { /* not loaded */ }
|
|
358
|
+
}
|
|
359
|
+
else if (pendingReload) {
|
|
360
|
+
// The loaded job may continue until logout; removing the plist prevents
|
|
361
|
+
// it from loading in the next login session without killing this agent.
|
|
362
|
+
}
|
|
363
|
+
if (!stopAgent && running) {
|
|
364
|
+
execFileSync('launchctl', ['disable', getLaunchdTarget()], { stdio: 'ignore' });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else if (platform !== 'win32') {
|
|
368
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
369
|
+
}
|
|
281
370
|
}
|
|
282
|
-
|
|
283
|
-
|
|
371
|
+
if (runtimeUsesServiceSpec) {
|
|
372
|
+
const runningSpec = loadLaunchSpec(serviceSpecPath);
|
|
373
|
+
const standaloneSpec = {
|
|
374
|
+
...runningSpec,
|
|
375
|
+
mode: 'daemon',
|
|
376
|
+
host: { type: 'standalone' },
|
|
377
|
+
config: { ...runningSpec.config },
|
|
378
|
+
runtime: {
|
|
379
|
+
...runningSpec.runtime,
|
|
380
|
+
nodeArgs: [...runningSpec.runtime.nodeArgs],
|
|
381
|
+
resolvedExecPaths: { ...runningSpec.runtime.resolvedExecPaths },
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
const launchSpecPath = saveLaunchSpecSnapshot(standaloneSpec);
|
|
385
|
+
saveLaunchSpec(standaloneSpec);
|
|
386
|
+
saveRuntimeState({
|
|
387
|
+
...state,
|
|
388
|
+
launchSpecPath,
|
|
389
|
+
launchSpecDigest: computeLaunchSpecDigest(standaloneSpec),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (!preserveRunningHostEntry) {
|
|
393
|
+
if (existsSync(artifactPath))
|
|
394
|
+
unlinkSync(artifactPath);
|
|
395
|
+
if (existsSync(serviceSpecPath))
|
|
396
|
+
unlinkSync(serviceSpecPath);
|
|
397
|
+
}
|
|
398
|
+
if (!isSimulation() && platform === 'linux') {
|
|
284
399
|
try {
|
|
285
|
-
|
|
400
|
+
runSystemctl(['daemon-reload']);
|
|
286
401
|
}
|
|
287
|
-
catch {
|
|
288
|
-
|
|
402
|
+
catch { /* unit already gone */ }
|
|
403
|
+
}
|
|
404
|
+
if (stopAgent) {
|
|
405
|
+
if (state?.pid && isProcessAlive(state.pid)) {
|
|
406
|
+
killProcess(state.pid);
|
|
407
|
+
clearRuntimeState();
|
|
289
408
|
}
|
|
290
409
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const batPath = getStartupBatPath();
|
|
294
|
-
if (existsSync(batPath)) {
|
|
295
|
-
unlinkSync(batPath);
|
|
296
|
-
console.log(`Removed startup file: ${batPath}`);
|
|
410
|
+
if (options.persistPolicy !== false) {
|
|
411
|
+
saveConfig({ autoStart: false, autoStartPendingReload: pendingReload || undefined });
|
|
297
412
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
killProcess(state.pid);
|
|
303
|
-
clearRuntimeState();
|
|
304
|
-
}
|
|
305
|
-
console.log('Service uninstalled.');
|
|
413
|
+
console.log(pendingReload
|
|
414
|
+
? 'AgentLink auto-start disabled for the next login.'
|
|
415
|
+
: 'AgentLink auto-start uninstalled.');
|
|
416
|
+
return { pendingReload };
|
|
306
417
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
418
|
+
export function stopServiceForUpgrade(spec) {
|
|
419
|
+
if (isSimulation())
|
|
420
|
+
return spec.host.type === 'systemd' || spec.host.type === 'launchd';
|
|
421
|
+
if (spec.host.type === 'systemd') {
|
|
422
|
+
if (existsSync(getLegacySystemdUnitPath())) {
|
|
423
|
+
throw new Error(`Cannot upgrade legacy system-level service at ${getLegacySystemdUnitPath()}; `
|
|
424
|
+
+ 'migrate it to user-level auto-start first');
|
|
425
|
+
}
|
|
426
|
+
runSystemctl(['stop', SERVICE_NAME]);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
if (spec.host.type === 'launchd') {
|
|
430
|
+
const artifactPath = getServiceArtifactPath('darwin');
|
|
431
|
+
if (!existsSync(artifactPath)) {
|
|
432
|
+
throw new Error(`launchd plist not found: ${artifactPath}`);
|
|
433
|
+
}
|
|
434
|
+
execFileSync('launchctl', ['unload', artifactPath], { stdio: 'ignore' });
|
|
435
|
+
return true;
|
|
318
436
|
}
|
|
319
|
-
catch { /* swallow */ }
|
|
320
437
|
return false;
|
|
321
438
|
}
|
|
322
|
-
export function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
else if (p === 'darwin')
|
|
333
|
-
macInstall(config);
|
|
334
|
-
else if (p === 'win32')
|
|
335
|
-
winInstall(config);
|
|
336
|
-
else {
|
|
337
|
-
console.error(`Unsupported platform: ${p}`);
|
|
338
|
-
console.error('Run the agent directly: agentlink-client start --daemon');
|
|
339
|
-
process.exit(1);
|
|
439
|
+
export function restartServiceAfterUpgrade(spec) {
|
|
440
|
+
if (isSimulation())
|
|
441
|
+
return spec.host.type === 'systemd' || spec.host.type === 'launchd';
|
|
442
|
+
if (spec.host.type === 'systemd') {
|
|
443
|
+
if (existsSync(getLegacySystemdUnitPath())) {
|
|
444
|
+
throw new Error(`Cannot restart legacy system-level service at ${getLegacySystemdUnitPath()}; `
|
|
445
|
+
+ 'migrate it to user-level auto-start first');
|
|
446
|
+
}
|
|
447
|
+
runSystemctl(['start', SERVICE_NAME]);
|
|
448
|
+
return true;
|
|
340
449
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
console.log('Uninstalling AgentLink service...');
|
|
345
|
-
if (p === 'linux')
|
|
346
|
-
linuxUninstall();
|
|
347
|
-
else if (p === 'darwin')
|
|
348
|
-
macUninstall();
|
|
349
|
-
else if (p === 'win32')
|
|
350
|
-
winUninstall();
|
|
351
|
-
else {
|
|
352
|
-
console.error(`Unsupported platform: ${p}`);
|
|
353
|
-
process.exit(1);
|
|
450
|
+
if (spec.host.type === 'launchd') {
|
|
451
|
+
execFileSync('launchctl', ['load', getServiceArtifactPath('darwin')], { stdio: 'ignore' });
|
|
452
|
+
return true;
|
|
354
453
|
}
|
|
454
|
+
return false;
|
|
355
455
|
}
|
|
356
456
|
//# sourceMappingURL=service.js.map
|