shennian 0.2.5 → 0.2.6
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/package.json +1 -1
- package/dist/bin/shennian.d.ts +0 -2
- package/dist/bin/shennian.js +0 -2
- package/dist/src/agents/adapter.d.ts +0 -35
- package/dist/src/agents/adapter.js +0 -19
- package/dist/src/agents/claude.d.ts +0 -25
- package/dist/src/agents/claude.js +0 -234
- package/dist/src/agents/codex.d.ts +0 -49
- package/dist/src/agents/codex.js +0 -794
- package/dist/src/agents/command-spec.d.ts +0 -36
- package/dist/src/agents/command-spec.js +0 -303
- package/dist/src/agents/cursor.d.ts +0 -22
- package/dist/src/agents/cursor.js +0 -248
- package/dist/src/agents/custom.d.ts +0 -30
- package/dist/src/agents/custom.js +0 -210
- package/dist/src/agents/detect.d.ts +0 -9
- package/dist/src/agents/detect.js +0 -46
- package/dist/src/agents/gemini.d.ts +0 -17
- package/dist/src/agents/gemini.js +0 -172
- package/dist/src/agents/model-registry/cache.d.ts +0 -4
- package/dist/src/agents/model-registry/cache.js +0 -37
- package/dist/src/agents/model-registry/discovery.d.ts +0 -4
- package/dist/src/agents/model-registry/discovery.js +0 -161
- package/dist/src/agents/model-registry/parsers.d.ts +0 -11
- package/dist/src/agents/model-registry/parsers.js +0 -349
- package/dist/src/agents/model-registry/runner.d.ts +0 -6
- package/dist/src/agents/model-registry/runner.js +0 -29
- package/dist/src/agents/model-registry/service.d.ts +0 -6
- package/dist/src/agents/model-registry/service.js +0 -71
- package/dist/src/agents/model-registry/types.d.ts +0 -30
- package/dist/src/agents/model-registry/types.js +0 -8
- package/dist/src/agents/model-registry.d.ts +0 -13
- package/dist/src/agents/model-registry.js +0 -15
- package/dist/src/agents/openclaw.d.ts +0 -20
- package/dist/src/agents/openclaw.js +0 -273
- package/dist/src/agents/pi.d.ts +0 -51
- package/dist/src/agents/pi.js +0 -793
- package/dist/src/commands/agent.d.ts +0 -2
- package/dist/src/commands/agent.js +0 -131
- package/dist/src/commands/daemon.d.ts +0 -35
- package/dist/src/commands/daemon.js +0 -612
- package/dist/src/commands/pair-qr.d.ts +0 -4
- package/dist/src/commands/pair-qr.js +0 -6
- package/dist/src/commands/pair.d.ts +0 -8
- package/dist/src/commands/pair.js +0 -162
- package/dist/src/commands/upgrade.d.ts +0 -5
- package/dist/src/commands/upgrade.js +0 -145
- package/dist/src/config/index.d.ts +0 -32
- package/dist/src/config/index.js +0 -31
- package/dist/src/fs/handler.d.ts +0 -26
- package/dist/src/fs/handler.js +0 -130
- package/dist/src/fs/security.d.ts +0 -2
- package/dist/src/fs/security.js +0 -32
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -277
- package/dist/src/log-reporter.d.ts +0 -18
- package/dist/src/log-reporter.js +0 -16
- package/dist/src/native-fusion/parsers.d.ts +0 -12
- package/dist/src/native-fusion/parsers.js +0 -687
- package/dist/src/native-fusion/service.d.ts +0 -25
- package/dist/src/native-fusion/service.js +0 -176
- package/dist/src/native-fusion/state.d.ts +0 -3
- package/dist/src/native-fusion/state.js +0 -22
- package/dist/src/native-fusion/types.d.ts +0 -24
- package/dist/src/native-fusion/types.js +0 -1
- package/dist/src/region.d.ts +0 -15
- package/dist/src/region.js +0 -99
- package/dist/src/relay/client.d.ts +0 -62
- package/dist/src/relay/client.js +0 -311
- package/dist/src/session/handlers/agents.d.ts +0 -4
- package/dist/src/session/handlers/agents.js +0 -55
- package/dist/src/session/handlers/chat.d.ts +0 -4
- package/dist/src/session/handlers/chat.js +0 -326
- package/dist/src/session/handlers/control.d.ts +0 -5
- package/dist/src/session/handlers/control.js +0 -57
- package/dist/src/session/handlers/fs.d.ts +0 -10
- package/dist/src/session/handlers/fs.js +0 -256
- package/dist/src/session/manager.d.ts +0 -28
- package/dist/src/session/manager.js +0 -174
- package/dist/src/session/store.d.ts +0 -7
- package/dist/src/session/store.js +0 -46
- package/dist/src/session/types.d.ts +0 -35
- package/dist/src/session/types.js +0 -4
- package/dist/src/upgrade/engine.d.ts +0 -64
- package/dist/src/upgrade/engine.js +0 -238
|
@@ -1,612 +0,0 @@
|
|
|
1
|
-
// @arch docs/architecture/cli/daemon.md#守护进程管理
|
|
2
|
-
// @test src/__tests__/daemon-autostart.test.ts
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
import { execSync, spawn } from 'node:child_process';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { getShennianDir, resolveShennianPath } from '../config/index.js';
|
|
10
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const SHENNIAN_DIR = getShennianDir();
|
|
12
|
-
const PID_FILE = resolveShennianPath('daemon.pid');
|
|
13
|
-
const LOG_FILE = resolveShennianPath('daemon.log');
|
|
14
|
-
const SHENNIAN_SCRIPT = path.resolve(__dirname, '../../bin/shennian.js');
|
|
15
|
-
const NODE_EXEC = process.execPath;
|
|
16
|
-
function getPlatform() {
|
|
17
|
-
const p = os.platform();
|
|
18
|
-
if (p === 'darwin' || p === 'linux' || p === 'win32')
|
|
19
|
-
return p;
|
|
20
|
-
return 'linux';
|
|
21
|
-
}
|
|
22
|
-
export function isEphemeralCliPath(candidate) {
|
|
23
|
-
const normalized = candidate.replace(/\\/g, '/').toLowerCase();
|
|
24
|
-
const tmp = os.tmpdir().replace(/\\/g, '/').toLowerCase();
|
|
25
|
-
return (normalized.includes('/_npx/') ||
|
|
26
|
-
normalized.includes('/npm-cache/_npx/') ||
|
|
27
|
-
normalized.includes('/pnpm/dlx/') ||
|
|
28
|
-
normalized.startsWith(tmp.endsWith('/') ? tmp : `${tmp}/`));
|
|
29
|
-
}
|
|
30
|
-
export function resolveServiceLaunchSpec(input) {
|
|
31
|
-
if (fs.existsSync(input.scriptPath) && !isEphemeralCliPath(input.scriptPath)) {
|
|
32
|
-
return {
|
|
33
|
-
command: input.nodeExec,
|
|
34
|
-
args: [input.scriptPath, 'start'],
|
|
35
|
-
mode: 'direct',
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
if (input.shennianCommandPath && !isEphemeralCliPath(input.shennianCommandPath)) {
|
|
39
|
-
return {
|
|
40
|
-
command: input.shennianCommandPath,
|
|
41
|
-
args: ['start'],
|
|
42
|
-
mode: 'global-shim',
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (input.npxPath) {
|
|
46
|
-
return {
|
|
47
|
-
command: input.npxPath,
|
|
48
|
-
args: ['--yes', 'shennian', 'start'],
|
|
49
|
-
mode: 'npx',
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
command: input.nodeExec,
|
|
54
|
-
args: [input.scriptPath, 'start'],
|
|
55
|
-
mode: 'direct',
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
// ─── PID helpers ────────────────────────────────────────────────────────────
|
|
59
|
-
function readPid() {
|
|
60
|
-
try {
|
|
61
|
-
const raw = fs.readFileSync(PID_FILE, 'utf-8').trim();
|
|
62
|
-
const pid = parseInt(raw, 10);
|
|
63
|
-
return isNaN(pid) ? null : pid;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function isRunning(pid) {
|
|
70
|
-
try {
|
|
71
|
-
process.kill(pid, 0);
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// ─── Service definitions ─────────────────────────────────────────────────────
|
|
79
|
-
const LAUNCHD_LABEL = 'com.shennian.agent';
|
|
80
|
-
const LAUNCHD_PLIST = path.join(os.homedir(), 'Library/LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
81
|
-
const WINDOWS_STARTUP_DIR = path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
82
|
-
const WINDOWS_STARTUP_CMD = resolveShennianPath('autostart.cmd');
|
|
83
|
-
const WINDOWS_LAUNCHER_VBS = resolveShennianPath('autostart.vbs');
|
|
84
|
-
const WINDOWS_TASK_XML = resolveShennianPath('autostart.xml');
|
|
85
|
-
const WINDOWS_STARTUP_VBS = path.join(WINDOWS_STARTUP_DIR, 'ShennianAgent.vbs');
|
|
86
|
-
function findCommandPath(binary) {
|
|
87
|
-
const command = getPlatform() === 'win32' ? `where ${binary}` : `command -v ${binary}`;
|
|
88
|
-
try {
|
|
89
|
-
const output = execSync(command, {
|
|
90
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
91
|
-
encoding: 'utf-8',
|
|
92
|
-
});
|
|
93
|
-
const first = output
|
|
94
|
-
.split(/\r?\n/)
|
|
95
|
-
.map((line) => line.trim())
|
|
96
|
-
.find(Boolean);
|
|
97
|
-
return first ?? null;
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function resolveNpxPath() {
|
|
104
|
-
const fileName = getPlatform() === 'win32' ? 'npx.cmd' : 'npx';
|
|
105
|
-
const sibling = path.join(path.dirname(NODE_EXEC), fileName);
|
|
106
|
-
if (fs.existsSync(sibling))
|
|
107
|
-
return sibling;
|
|
108
|
-
return findCommandPath('npx');
|
|
109
|
-
}
|
|
110
|
-
function resolveCurrentServiceLaunchSpec() {
|
|
111
|
-
return resolveServiceLaunchSpec({
|
|
112
|
-
nodeExec: NODE_EXEC,
|
|
113
|
-
scriptPath: SHENNIAN_SCRIPT,
|
|
114
|
-
shennianCommandPath: findCommandPath('shennian'),
|
|
115
|
-
npxPath: resolveNpxPath(),
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
function escapeXml(text) {
|
|
119
|
-
return text
|
|
120
|
-
.replace(/&/g, '&')
|
|
121
|
-
.replace(/</g, '<')
|
|
122
|
-
.replace(/>/g, '>')
|
|
123
|
-
.replace(/"/g, '"')
|
|
124
|
-
.replace(/'/g, ''');
|
|
125
|
-
}
|
|
126
|
-
function quoteSystemdArg(text) {
|
|
127
|
-
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
128
|
-
}
|
|
129
|
-
function quoteCmdArg(text) {
|
|
130
|
-
return `"${text.replace(/"/g, '""')}"`;
|
|
131
|
-
}
|
|
132
|
-
function escapeVbsString(text) {
|
|
133
|
-
return text.replace(/"/g, '""');
|
|
134
|
-
}
|
|
135
|
-
function escapeXmlValue(text) {
|
|
136
|
-
return escapeXml(text);
|
|
137
|
-
}
|
|
138
|
-
function isWindowsCmdScript(filePath) {
|
|
139
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
140
|
-
return ext === '.cmd' || ext === '.bat';
|
|
141
|
-
}
|
|
142
|
-
export function buildWindowsLauncherCommand(spec, logFile) {
|
|
143
|
-
const commandLine = [spec.command, ...spec.args].map(quoteCmdArg).join(' ');
|
|
144
|
-
const invocation = isWindowsCmdScript(spec.command) ? `call ${commandLine}` : commandLine;
|
|
145
|
-
return [
|
|
146
|
-
'@echo off',
|
|
147
|
-
'setlocal',
|
|
148
|
-
`${invocation} >> ${quoteCmdArg(logFile)} 2>&1`,
|
|
149
|
-
'',
|
|
150
|
-
].join('\r\n');
|
|
151
|
-
}
|
|
152
|
-
export function buildWindowsStartupVbs(cmdPath) {
|
|
153
|
-
return [
|
|
154
|
-
'Set shell = CreateObject("WScript.Shell")',
|
|
155
|
-
`shell.Run Chr(34) & "${escapeVbsString(cmdPath)}" & Chr(34), 0, False`,
|
|
156
|
-
'',
|
|
157
|
-
].join('\r\n');
|
|
158
|
-
}
|
|
159
|
-
export function buildWindowsScheduledTaskXml(input) {
|
|
160
|
-
const joinedArgs = input.arguments.join(' ');
|
|
161
|
-
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
162
|
-
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
163
|
-
<RegistrationInfo>
|
|
164
|
-
<Description>Shennian Agent Daemon</Description>
|
|
165
|
-
</RegistrationInfo>
|
|
166
|
-
<Triggers>
|
|
167
|
-
<LogonTrigger>
|
|
168
|
-
<Enabled>true</Enabled>
|
|
169
|
-
<UserId>${escapeXmlValue(input.userId)}</UserId>
|
|
170
|
-
</LogonTrigger>
|
|
171
|
-
<EventTrigger>
|
|
172
|
-
<Enabled>true</Enabled>
|
|
173
|
-
<Subscription><![CDATA[
|
|
174
|
-
<QueryList>
|
|
175
|
-
<Query Id="0" Path="System">
|
|
176
|
-
<Select Path="System">*[System[Provider[@Name='Microsoft-Windows-Power-Troubleshooter'] and EventID=1]]</Select>
|
|
177
|
-
</Query>
|
|
178
|
-
</QueryList>]]></Subscription>
|
|
179
|
-
</EventTrigger>
|
|
180
|
-
</Triggers>
|
|
181
|
-
<Principals>
|
|
182
|
-
<Principal id="Author">
|
|
183
|
-
<UserId>${escapeXmlValue(input.userId)}</UserId>
|
|
184
|
-
<LogonType>InteractiveToken</LogonType>
|
|
185
|
-
<RunLevel>LeastPrivilege</RunLevel>
|
|
186
|
-
</Principal>
|
|
187
|
-
</Principals>
|
|
188
|
-
<Settings>
|
|
189
|
-
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
190
|
-
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
191
|
-
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
192
|
-
<AllowHardTerminate>true</AllowHardTerminate>
|
|
193
|
-
<StartWhenAvailable>true</StartWhenAvailable>
|
|
194
|
-
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
195
|
-
<IdleSettings>
|
|
196
|
-
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
197
|
-
<RestartOnIdle>false</RestartOnIdle>
|
|
198
|
-
</IdleSettings>
|
|
199
|
-
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
200
|
-
<Enabled>true</Enabled>
|
|
201
|
-
<Hidden>true</Hidden>
|
|
202
|
-
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
203
|
-
<WakeToRun>false</WakeToRun>
|
|
204
|
-
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
205
|
-
<Priority>7</Priority>
|
|
206
|
-
<RestartOnFailure>
|
|
207
|
-
<Interval>PT1M</Interval>
|
|
208
|
-
<Count>3</Count>
|
|
209
|
-
</RestartOnFailure>
|
|
210
|
-
</Settings>
|
|
211
|
-
<Actions Context="Author">
|
|
212
|
-
<Exec>
|
|
213
|
-
<Command>${escapeXmlValue(input.command)}</Command>
|
|
214
|
-
<Arguments>${escapeXmlValue(joinedArgs)}</Arguments>
|
|
215
|
-
</Exec>
|
|
216
|
-
</Actions>
|
|
217
|
-
</Task>`;
|
|
218
|
-
}
|
|
219
|
-
function removeLegacyWindowsTask() {
|
|
220
|
-
try {
|
|
221
|
-
execSync(`schtasks /delete /tn "${WINDOWS_TASK_NAME}" /f`, { stdio: 'pipe' });
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
// Ignore missing legacy Task Scheduler entries during migration.
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
function removeLegacyWindowsStartupFiles() {
|
|
228
|
-
try {
|
|
229
|
-
fs.unlinkSync(WINDOWS_STARTUP_VBS);
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
// Legacy Startup entry may already be absent.
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
function getWindowsTaskUserId() {
|
|
236
|
-
const domain = process.env.USERDOMAIN?.trim();
|
|
237
|
-
const computer = process.env.COMPUTERNAME?.trim();
|
|
238
|
-
const username = process.env.USERNAME?.trim() || os.userInfo().username;
|
|
239
|
-
if (domain && domain.toUpperCase() !== 'WORKGROUP')
|
|
240
|
-
return `${domain}\\${username}`;
|
|
241
|
-
if (computer)
|
|
242
|
-
return `${computer}\\${username}`;
|
|
243
|
-
return username;
|
|
244
|
-
}
|
|
245
|
-
function buildDetachedLaunchSpec(spec) {
|
|
246
|
-
if (getPlatform() === 'win32' && isWindowsCmdScript(spec.command)) {
|
|
247
|
-
const commandLine = [spec.command, ...spec.args].map(quoteCmdArg).join(' ');
|
|
248
|
-
return {
|
|
249
|
-
command: process.env.ComSpec || 'cmd.exe',
|
|
250
|
-
args: ['/d', '/s', '/c', `"${commandLine}"`],
|
|
251
|
-
windowsVerbatimArguments: true,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
command: spec.command,
|
|
256
|
-
args: spec.args,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
function installWindowsScheduledTask() {
|
|
260
|
-
const launch = resolveCurrentServiceLaunchSpec();
|
|
261
|
-
fs.writeFileSync(WINDOWS_STARTUP_CMD, buildWindowsLauncherCommand(launch, LOG_FILE));
|
|
262
|
-
fs.writeFileSync(WINDOWS_LAUNCHER_VBS, buildWindowsStartupVbs(WINDOWS_STARTUP_CMD));
|
|
263
|
-
removeLegacyWindowsStartupFiles();
|
|
264
|
-
removeLegacyWindowsTask();
|
|
265
|
-
const taskXml = buildWindowsScheduledTaskXml({
|
|
266
|
-
userId: getWindowsTaskUserId(),
|
|
267
|
-
command: path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'wscript.exe'),
|
|
268
|
-
arguments: [quoteCmdArg(WINDOWS_LAUNCHER_VBS)],
|
|
269
|
-
});
|
|
270
|
-
fs.writeFileSync(WINDOWS_TASK_XML, taskXml, 'utf8');
|
|
271
|
-
try {
|
|
272
|
-
execSync(`schtasks /create /tn "${WINDOWS_TASK_NAME}" /xml "${WINDOWS_TASK_XML}" /f`, { stdio: 'pipe' });
|
|
273
|
-
execSync(`schtasks /run /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe' });
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
export function captureEnvForService() {
|
|
281
|
-
const keep = [
|
|
282
|
-
'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TMPDIR', 'LANG',
|
|
283
|
-
'LC_ALL', 'LC_CTYPE', 'SSH_AUTH_SOCK', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME',
|
|
284
|
-
'TEMP', 'TMP',
|
|
285
|
-
'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL',
|
|
286
|
-
'OPENAI_API_KEY', 'OPENAI_BASE_URL',
|
|
287
|
-
'GEMINI_API_KEY', 'GOOGLE_API_KEY',
|
|
288
|
-
];
|
|
289
|
-
const env = {};
|
|
290
|
-
for (const k of keep) {
|
|
291
|
-
if (process.env[k])
|
|
292
|
-
env[k] = process.env[k];
|
|
293
|
-
}
|
|
294
|
-
env.HOME ??= os.homedir();
|
|
295
|
-
env.PATH ??= '/usr/local/bin:/usr/bin:/bin';
|
|
296
|
-
env.USER ??= os.userInfo().username;
|
|
297
|
-
if (getPlatform() === 'win32') {
|
|
298
|
-
const tempDir = process.env.TEMP || process.env.TMP || os.tmpdir();
|
|
299
|
-
env.TMPDIR ??= tempDir;
|
|
300
|
-
env.TEMP ??= tempDir;
|
|
301
|
-
env.TMP ??= tempDir;
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
env.TMPDIR ??= '/tmp';
|
|
305
|
-
}
|
|
306
|
-
return env;
|
|
307
|
-
}
|
|
308
|
-
function buildPlist() {
|
|
309
|
-
const env = captureEnvForService();
|
|
310
|
-
const launch = resolveCurrentServiceLaunchSpec();
|
|
311
|
-
const envEntries = Object.entries(env)
|
|
312
|
-
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
313
|
-
.join('\n');
|
|
314
|
-
const programArgs = [launch.command, ...launch.args]
|
|
315
|
-
.map((value) => ` <string>${escapeXml(value)}</string>`)
|
|
316
|
-
.join('\n');
|
|
317
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
318
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
319
|
-
<plist version="1.0">
|
|
320
|
-
<dict>
|
|
321
|
-
<key>Label</key>
|
|
322
|
-
<string>${LAUNCHD_LABEL}</string>
|
|
323
|
-
<key>ProgramArguments</key>
|
|
324
|
-
<array>
|
|
325
|
-
${programArgs}
|
|
326
|
-
</array>
|
|
327
|
-
<key>EnvironmentVariables</key>
|
|
328
|
-
<dict>
|
|
329
|
-
${envEntries}
|
|
330
|
-
</dict>
|
|
331
|
-
<key>RunAtLoad</key>
|
|
332
|
-
<true/>
|
|
333
|
-
<key>KeepAlive</key>
|
|
334
|
-
<true/>
|
|
335
|
-
<key>ThrottleInterval</key>
|
|
336
|
-
<integer>5</integer>
|
|
337
|
-
<key>StandardOutPath</key>
|
|
338
|
-
<string>${LOG_FILE}</string>
|
|
339
|
-
<key>StandardErrorPath</key>
|
|
340
|
-
<string>${LOG_FILE}</string>
|
|
341
|
-
</dict>
|
|
342
|
-
</plist>`;
|
|
343
|
-
}
|
|
344
|
-
const SYSTEMD_UNIT = path.join(os.homedir(), '.config/systemd/user/shennian.service');
|
|
345
|
-
function buildSystemdUnit() {
|
|
346
|
-
const env = captureEnvForService();
|
|
347
|
-
const launch = resolveCurrentServiceLaunchSpec();
|
|
348
|
-
const envLines = Object.entries(env)
|
|
349
|
-
.map(([k, v]) => `Environment=${k}=${v}`)
|
|
350
|
-
.join('\n');
|
|
351
|
-
const execStart = [launch.command, ...launch.args].map(quoteSystemdArg).join(' ');
|
|
352
|
-
return `[Unit]
|
|
353
|
-
Description=Shennian Agent Daemon
|
|
354
|
-
After=network.target
|
|
355
|
-
|
|
356
|
-
[Service]
|
|
357
|
-
${envLines}
|
|
358
|
-
ExecStart=${execStart}
|
|
359
|
-
Restart=always
|
|
360
|
-
RestartSec=10
|
|
361
|
-
StandardOutput=append:${LOG_FILE}
|
|
362
|
-
StandardError=append:${LOG_FILE}
|
|
363
|
-
|
|
364
|
-
[Install]
|
|
365
|
-
WantedBy=default.target`;
|
|
366
|
-
}
|
|
367
|
-
const WINDOWS_TASK_NAME = 'ShennianAgent';
|
|
368
|
-
/**
|
|
369
|
-
* Save current env vars so the daemon can load them at startup.
|
|
370
|
-
* This is critical for launchd/systemd which start with minimal env.
|
|
371
|
-
*/
|
|
372
|
-
export function saveEnvSnapshot() {
|
|
373
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
374
|
-
const snapshot = {};
|
|
375
|
-
for (const [k, v] of Object.entries(process.env)) {
|
|
376
|
-
if (v !== undefined)
|
|
377
|
-
snapshot[k] = v;
|
|
378
|
-
}
|
|
379
|
-
fs.writeFileSync(resolveShennianPath('env.json'), JSON.stringify(snapshot, null, 2));
|
|
380
|
-
}
|
|
381
|
-
// ─── Exported helpers (used by pair.ts) ─────────────────────────────────────
|
|
382
|
-
export function startDaemonProcess() {
|
|
383
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
384
|
-
const pid = readPid();
|
|
385
|
-
if (pid !== null && isRunning(pid)) {
|
|
386
|
-
console.log(chalk.green(`✓ Background service already running (PID ${pid})`));
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
390
|
-
const launch = buildDetachedLaunchSpec(resolveCurrentServiceLaunchSpec());
|
|
391
|
-
const child = spawn(launch.command, launch.args, {
|
|
392
|
-
detached: true,
|
|
393
|
-
stdio: ['ignore', logFd, logFd],
|
|
394
|
-
env: process.env,
|
|
395
|
-
...(launch.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
396
|
-
});
|
|
397
|
-
child.unref();
|
|
398
|
-
fs.closeSync(logFd);
|
|
399
|
-
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
400
|
-
console.log(chalk.green(`✓ Background service started (PID ${child.pid})`));
|
|
401
|
-
console.log(chalk.gray(` Logs: ${LOG_FILE}`));
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Install the platform-native auto-start service.
|
|
405
|
-
* Returns true if the service was immediately started (so caller can skip startDaemonProcess).
|
|
406
|
-
*/
|
|
407
|
-
export function installService() {
|
|
408
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
409
|
-
const platform = getPlatform();
|
|
410
|
-
const spec = resolveCurrentServiceLaunchSpec();
|
|
411
|
-
if (spec.mode === 'direct' && isEphemeralCliPath(SHENNIAN_SCRIPT)) {
|
|
412
|
-
console.warn(chalk.yellow('⚠ Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n'
|
|
413
|
-
+ ' Run `npm install -g shennian` for reliable auto-start.'));
|
|
414
|
-
}
|
|
415
|
-
switch (platform) {
|
|
416
|
-
case 'darwin': {
|
|
417
|
-
const dir = path.dirname(LAUNCHD_PLIST);
|
|
418
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
419
|
-
fs.writeFileSync(LAUNCHD_PLIST, buildPlist());
|
|
420
|
-
try {
|
|
421
|
-
// Unload first (ignore errors), then reload - this starts the service immediately
|
|
422
|
-
execSync(`launchctl unload "${LAUNCHD_PLIST}" 2>/dev/null; launchctl load -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe' });
|
|
423
|
-
return true; // launchd started it; caller must NOT also call startDaemonProcess
|
|
424
|
-
}
|
|
425
|
-
catch {
|
|
426
|
-
try {
|
|
427
|
-
execSync(`launchctl load -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe' });
|
|
428
|
-
return true;
|
|
429
|
-
}
|
|
430
|
-
catch {
|
|
431
|
-
// launchctl can fail on locked-down shells; caller falls back to manual start below.
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
case 'linux': {
|
|
437
|
-
const dir = path.dirname(SYSTEMD_UNIT);
|
|
438
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
439
|
-
fs.writeFileSync(SYSTEMD_UNIT, buildSystemdUnit());
|
|
440
|
-
try {
|
|
441
|
-
execSync('systemctl --user daemon-reload && systemctl --user enable shennian', { stdio: 'pipe' });
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
// Some Linux environments lack systemd user services; keep best-effort behavior.
|
|
445
|
-
}
|
|
446
|
-
// Enable linger so the user systemd session (and thus this service) persists
|
|
447
|
-
// across reboots even without an active login session.
|
|
448
|
-
try {
|
|
449
|
-
execSync(`loginctl enable-linger ${os.userInfo().username}`, { stdio: 'pipe' });
|
|
450
|
-
}
|
|
451
|
-
catch {
|
|
452
|
-
// loginctl is unavailable on some distros/containers; auto-start still works after login.
|
|
453
|
-
}
|
|
454
|
-
return false; // systemd enable doesn't start immediately; caller should startDaemonProcess
|
|
455
|
-
}
|
|
456
|
-
case 'win32': {
|
|
457
|
-
return installWindowsScheduledTask();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// ─── Subcommand implementations ──────────────────────────────────────────────
|
|
462
|
-
function daemonStop() {
|
|
463
|
-
const pid = readPid();
|
|
464
|
-
if (pid === null) {
|
|
465
|
-
console.log(chalk.yellow('⚠ No running background service found'));
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (!isRunning(pid)) {
|
|
469
|
-
try {
|
|
470
|
-
fs.unlinkSync(PID_FILE);
|
|
471
|
-
}
|
|
472
|
-
catch {
|
|
473
|
-
// Stale pid file is already gone.
|
|
474
|
-
}
|
|
475
|
-
console.log(chalk.yellow('⚠ Service process no longer exists, cleaned up'));
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
try {
|
|
479
|
-
process.kill(pid, 'SIGTERM');
|
|
480
|
-
try {
|
|
481
|
-
fs.unlinkSync(PID_FILE);
|
|
482
|
-
}
|
|
483
|
-
catch {
|
|
484
|
-
// Best-effort cleanup after SIGTERM.
|
|
485
|
-
}
|
|
486
|
-
console.log(chalk.green(`✓ Background service stopped (PID ${pid})`));
|
|
487
|
-
}
|
|
488
|
-
catch (err) {
|
|
489
|
-
console.error(chalk.red(`✗ Failed to stop: ${err instanceof Error ? err.message : String(err)}`));
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
function daemonStatus() {
|
|
493
|
-
const pid = readPid();
|
|
494
|
-
if (pid === null) {
|
|
495
|
-
console.log(chalk.gray('● Background service not running'));
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
if (isRunning(pid)) {
|
|
499
|
-
console.log(chalk.green(`● Background service running (PID ${pid})`));
|
|
500
|
-
console.log(chalk.gray(` Logs: ${LOG_FILE}`));
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
console.log(chalk.yellow(`● Background service stopped (PID ${pid} is stale)`));
|
|
504
|
-
try {
|
|
505
|
-
fs.unlinkSync(PID_FILE);
|
|
506
|
-
}
|
|
507
|
-
catch {
|
|
508
|
-
// Another process may have cleaned up the stale pid file first.
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
function daemonLogs(opts) {
|
|
513
|
-
if (!fs.existsSync(LOG_FILE)) {
|
|
514
|
-
console.log(chalk.gray('No logs yet'));
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
try {
|
|
518
|
-
const out = execSync(os.platform() === 'win32'
|
|
519
|
-
? `powershell Get-Content -Tail ${opts.lines} "${LOG_FILE}"`
|
|
520
|
-
: `tail -n ${opts.lines} "${LOG_FILE}"`, { encoding: 'utf-8' });
|
|
521
|
-
process.stdout.write(out);
|
|
522
|
-
}
|
|
523
|
-
catch {
|
|
524
|
-
const lines = fs.readFileSync(LOG_FILE, 'utf-8').split('\n').slice(-opts.lines);
|
|
525
|
-
console.log(lines.join('\n'));
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
function daemonUninstall() {
|
|
529
|
-
daemonStop();
|
|
530
|
-
const platform = getPlatform();
|
|
531
|
-
switch (platform) {
|
|
532
|
-
case 'darwin': {
|
|
533
|
-
if (fs.existsSync(LAUNCHD_PLIST)) {
|
|
534
|
-
try {
|
|
535
|
-
execSync(`launchctl unload -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe' });
|
|
536
|
-
}
|
|
537
|
-
catch {
|
|
538
|
-
// Continue removing the plist even if launchctl already forgot about it.
|
|
539
|
-
}
|
|
540
|
-
fs.unlinkSync(LAUNCHD_PLIST);
|
|
541
|
-
console.log(chalk.green('✓ Auto-start uninstalled (launchd)'));
|
|
542
|
-
}
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
case 'linux': {
|
|
546
|
-
try {
|
|
547
|
-
execSync('systemctl --user disable --now shennian', { stdio: 'pipe' });
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
// Service may already be absent or systemd user services may be unavailable.
|
|
551
|
-
}
|
|
552
|
-
if (fs.existsSync(SYSTEMD_UNIT)) {
|
|
553
|
-
fs.unlinkSync(SYSTEMD_UNIT);
|
|
554
|
-
try {
|
|
555
|
-
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
556
|
-
}
|
|
557
|
-
catch {
|
|
558
|
-
// Ignore reload errors during cleanup.
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
console.log(chalk.green('✓ Auto-start uninstalled (systemd)'));
|
|
562
|
-
break;
|
|
563
|
-
}
|
|
564
|
-
case 'win32': {
|
|
565
|
-
removeLegacyWindowsTask();
|
|
566
|
-
try {
|
|
567
|
-
fs.unlinkSync(WINDOWS_STARTUP_CMD);
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
// Launcher script may already be removed.
|
|
571
|
-
}
|
|
572
|
-
try {
|
|
573
|
-
fs.unlinkSync(WINDOWS_LAUNCHER_VBS);
|
|
574
|
-
}
|
|
575
|
-
catch {
|
|
576
|
-
// Launcher vbs may already be removed.
|
|
577
|
-
}
|
|
578
|
-
try {
|
|
579
|
-
fs.unlinkSync(WINDOWS_TASK_XML);
|
|
580
|
-
}
|
|
581
|
-
catch {
|
|
582
|
-
// Task xml may already be removed.
|
|
583
|
-
}
|
|
584
|
-
removeLegacyWindowsStartupFiles();
|
|
585
|
-
console.log(chalk.green('✓ Auto-start uninstalled (Task Scheduler)'));
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
// ─── Register command ─────────────────────────────────────────────────────────
|
|
591
|
-
export function registerDaemonCommand(program) {
|
|
592
|
-
const daemon = program
|
|
593
|
-
.command('daemon')
|
|
594
|
-
.description('Manage the background service');
|
|
595
|
-
daemon
|
|
596
|
-
.command('stop')
|
|
597
|
-
.description('Stop the background service')
|
|
598
|
-
.action(daemonStop);
|
|
599
|
-
daemon
|
|
600
|
-
.command('status')
|
|
601
|
-
.description('Show background service status')
|
|
602
|
-
.action(daemonStatus);
|
|
603
|
-
daemon
|
|
604
|
-
.command('logs')
|
|
605
|
-
.description('Show recent logs')
|
|
606
|
-
.option('-n, --lines <n>', 'Number of lines', '50')
|
|
607
|
-
.action((opts) => daemonLogs({ lines: parseInt(opts.lines, 10) }));
|
|
608
|
-
daemon
|
|
609
|
-
.command('uninstall')
|
|
610
|
-
.description('Uninstall auto-start service')
|
|
611
|
-
.action(daemonUninstall);
|
|
612
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Command } from 'commander';
|
|
2
|
-
export declare function runPairFlow(opts: {
|
|
3
|
-
serverUrl: string;
|
|
4
|
-
machineName: string;
|
|
5
|
-
force?: boolean;
|
|
6
|
-
}): Promise<void>;
|
|
7
|
-
export declare function runSmartStart(serverUrl: string | undefined, machineName: string): Promise<void>;
|
|
8
|
-
export declare function registerPairCommand(program: Command): void;
|