shennian 0.2.89 → 0.2.90
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/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
|
@@ -1,512 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
import { execSync, spawn } from 'node:child_process';
|
|
8
|
-
import { randomUUID } from 'node:crypto';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { getShennianDir, loadConfig, resolveShennianPath, saveConfig } from '../config/index.js';
|
|
11
|
-
import { buildAugmentedPath } from '../env-path.js';
|
|
12
|
-
import { buildWindowsLauncherCommand, buildWindowsScheduledTaskXml, buildWindowsStartupVbs, escapeXml, isWindowsCmdScript, quoteCmdArg, quoteSystemdArg, } from './daemon-windows.js';
|
|
13
|
-
export { buildWindowsLauncherCommand, buildWindowsScheduledTaskXml, buildWindowsStartupVbs, } from './daemon-windows.js';
|
|
14
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const SHENNIAN_DIR = getShennianDir();
|
|
16
|
-
const PID_FILE = resolveShennianPath('daemon.pid');
|
|
17
|
-
const LOG_FILE = resolveShennianPath('daemon.log');
|
|
18
|
-
const REMOTE_ACCESS_DISABLED_FILE = resolveShennianPath('remote-access.disabled');
|
|
19
|
-
const LAUNCHER_FILE = resolveShennianPath('daemon-launcher.json');
|
|
20
|
-
const SHENNIAN_SCRIPT = path.resolve(__dirname, '../../bin/shennian.js');
|
|
21
|
-
const NODE_EXEC = process.execPath;
|
|
22
|
-
const SAFE_SNAPSHOT_ENV_KEYS = new Set([
|
|
23
|
-
'PATH',
|
|
24
|
-
'HOME',
|
|
25
|
-
'USER',
|
|
26
|
-
'LOGNAME',
|
|
27
|
-
'SHELL',
|
|
28
|
-
'TMPDIR',
|
|
29
|
-
'LANG',
|
|
30
|
-
'LC_ALL',
|
|
31
|
-
'LC_CTYPE',
|
|
32
|
-
'SSH_AUTH_SOCK',
|
|
33
|
-
'XDG_CONFIG_HOME',
|
|
34
|
-
'XDG_DATA_HOME',
|
|
35
|
-
'TEMP',
|
|
36
|
-
'TMP',
|
|
37
|
-
'APPDATA',
|
|
38
|
-
'LOCALAPPDATA',
|
|
39
|
-
'SHENNIAN_DESKTOP_SERVER_URL',
|
|
40
|
-
'SHENNIAN_HOME',
|
|
41
|
-
'SHENNIAN_NATIVE_FUSION_DISABLED',
|
|
42
|
-
]);
|
|
43
|
-
export function isSafeSnapshotEnvKey(key) {
|
|
44
|
-
return SAFE_SNAPSHOT_ENV_KEYS.has(key);
|
|
45
|
-
}
|
|
46
|
-
function getPlatform() {
|
|
47
|
-
const p = os.platform();
|
|
48
|
-
if (p === 'darwin' || p === 'linux' || p === 'win32')
|
|
49
|
-
return p;
|
|
50
|
-
return 'linux';
|
|
51
|
-
}
|
|
52
|
-
export function isEphemeralCliPath(candidate) {
|
|
53
|
-
const normalized = candidate.replace(/\\/g, '/').toLowerCase();
|
|
54
|
-
const tmp = os.tmpdir().replace(/\\/g, '/').toLowerCase();
|
|
55
|
-
return (normalized.includes('/_npx/') ||
|
|
56
|
-
normalized.includes('/npm-cache/_npx/') ||
|
|
57
|
-
normalized.includes('/pnpm/dlx/') ||
|
|
58
|
-
normalized.startsWith(tmp.endsWith('/') ? tmp : `${tmp}/`));
|
|
59
|
-
}
|
|
60
|
-
export function resolveServiceLaunchSpec(input) {
|
|
61
|
-
if (fs.existsSync(input.scriptPath) && !isEphemeralCliPath(input.scriptPath)) {
|
|
62
|
-
return {
|
|
63
|
-
command: input.nodeExec,
|
|
64
|
-
args: [input.scriptPath, 'run-service'],
|
|
65
|
-
mode: 'direct',
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (input.shennianCommandPath && !isEphemeralCliPath(input.shennianCommandPath)) {
|
|
69
|
-
return {
|
|
70
|
-
command: input.shennianCommandPath,
|
|
71
|
-
args: ['run-service'],
|
|
72
|
-
mode: 'global-shim',
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
if (input.npxPath) {
|
|
76
|
-
return {
|
|
77
|
-
command: input.npxPath,
|
|
78
|
-
args: ['--yes', 'shennian', 'run-service'],
|
|
79
|
-
mode: 'npx',
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
command: input.nodeExec,
|
|
84
|
-
args: [input.scriptPath, 'run-service'],
|
|
85
|
-
mode: 'direct',
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function parsePidRecord(raw) {
|
|
89
|
-
const trimmed = raw.trim();
|
|
90
|
-
if (!trimmed)
|
|
91
|
-
return null;
|
|
92
|
-
if (trimmed.startsWith('{')) {
|
|
93
|
-
try {
|
|
94
|
-
const parsed = JSON.parse(trimmed);
|
|
95
|
-
const pid = Number(parsed.pid);
|
|
96
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
97
|
-
return null;
|
|
98
|
-
return {
|
|
99
|
-
pid,
|
|
100
|
-
...(typeof parsed.instanceId === 'string' ? { instanceId: parsed.instanceId } : {}),
|
|
101
|
-
...(typeof parsed.version === 'string' ? { version: parsed.version } : {}),
|
|
102
|
-
...(parsed.launcher === 'desktop-managed' || parsed.launcher === 'global-cli' || parsed.launcher === 'unknown'
|
|
103
|
-
? { launcher: parsed.launcher }
|
|
104
|
-
: {}),
|
|
105
|
-
...(typeof parsed.startedAt === 'string' ? { startedAt: parsed.startedAt } : {}),
|
|
106
|
-
...(parsed.adopted === true ? { adopted: true } : {}),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const pid = parseInt(trimmed, 10);
|
|
114
|
-
return isNaN(pid) ? null : { pid };
|
|
115
|
-
}
|
|
116
|
-
function readPidRecord() {
|
|
117
|
-
try {
|
|
118
|
-
return parsePidRecord(fs.readFileSync(PID_FILE, 'utf-8'));
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
function readPid() {
|
|
125
|
-
return readPidRecord()?.pid ?? null;
|
|
126
|
-
}
|
|
127
|
-
function isRunning(pid) {
|
|
128
|
-
try {
|
|
129
|
-
process.kill(pid, 0);
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function waitForPidExitSync(pid, timeoutMs = 3000) {
|
|
137
|
-
const startedAt = Date.now();
|
|
138
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
139
|
-
if (!isRunning(pid))
|
|
140
|
-
return true;
|
|
141
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
142
|
-
}
|
|
143
|
-
return !isRunning(pid);
|
|
144
|
-
}
|
|
145
|
-
export function isRemoteAccessDisabled() {
|
|
146
|
-
return fs.existsSync(REMOTE_ACCESS_DISABLED_FILE);
|
|
147
|
-
}
|
|
148
|
-
export function clearRemoteAccessDisabled() {
|
|
149
|
-
try {
|
|
150
|
-
fs.unlinkSync(REMOTE_ACCESS_DISABLED_FILE);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// Remote access may already be enabled.
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
export function getCurrentProcessDaemonLauncher() {
|
|
157
|
-
return 'global-cli';
|
|
158
|
-
}
|
|
159
|
-
export function createDaemonInstanceId() {
|
|
160
|
-
return `${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
161
|
-
}
|
|
162
|
-
export function writeDaemonPid(pid, instanceId, meta = {}) {
|
|
163
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
164
|
-
fs.writeFileSync(PID_FILE, JSON.stringify({
|
|
165
|
-
pid,
|
|
166
|
-
instanceId,
|
|
167
|
-
...(meta.version ? { version: meta.version } : {}),
|
|
168
|
-
launcher: meta.launcher ?? getCurrentProcessDaemonLauncher(),
|
|
169
|
-
...(meta.adopted ? { adopted: true } : {}),
|
|
170
|
-
startedAt: new Date().toISOString(),
|
|
171
|
-
}, null, 2));
|
|
172
|
-
}
|
|
173
|
-
export function clearDaemonPidIfOwner(pid, instanceId) {
|
|
174
|
-
const current = readPidRecord();
|
|
175
|
-
if (current?.pid !== pid || current.instanceId !== instanceId)
|
|
176
|
-
return;
|
|
177
|
-
try {
|
|
178
|
-
fs.unlinkSync(PID_FILE);
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
// PID metadata may already be gone.
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
export function writeDaemonLauncher(pid, launcher = getCurrentProcessDaemonLauncher(), instanceId) {
|
|
185
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
186
|
-
fs.writeFileSync(LAUNCHER_FILE, JSON.stringify({
|
|
187
|
-
pid,
|
|
188
|
-
launcher,
|
|
189
|
-
...(instanceId ? { instanceId } : {}),
|
|
190
|
-
updatedAt: new Date().toISOString(),
|
|
191
|
-
}, null, 2));
|
|
192
|
-
}
|
|
193
|
-
export function clearDaemonLauncher(instanceId) {
|
|
194
|
-
if (instanceId) {
|
|
195
|
-
try {
|
|
196
|
-
const raw = JSON.parse(fs.readFileSync(LAUNCHER_FILE, 'utf-8'));
|
|
197
|
-
if (raw.instanceId !== instanceId)
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
fs.unlinkSync(LAUNCHER_FILE);
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
// Launcher metadata may already be absent.
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
export function recordStartedDaemon(childPid) {
|
|
212
|
-
if (childPid)
|
|
213
|
-
writeDaemonLauncher(childPid);
|
|
214
|
-
}
|
|
215
|
-
function readDaemonLauncher(pid, running) {
|
|
216
|
-
if (!pid || !running)
|
|
217
|
-
return 'unknown';
|
|
218
|
-
try {
|
|
219
|
-
const raw = JSON.parse(fs.readFileSync(LAUNCHER_FILE, 'utf-8'));
|
|
220
|
-
if (raw.pid !== pid)
|
|
221
|
-
return 'unknown';
|
|
222
|
-
if (raw.launcher === 'desktop-managed' || raw.launcher === 'global-cli')
|
|
223
|
-
return raw.launcher;
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
// Older daemons do not have launcher metadata.
|
|
227
|
-
}
|
|
228
|
-
return inferDaemonLauncherFromProcess(pid);
|
|
229
|
-
}
|
|
230
|
-
function inferDaemonLauncherFromProcess(pid) {
|
|
231
|
-
if (getPlatform() === 'win32')
|
|
232
|
-
return 'unknown';
|
|
233
|
-
try {
|
|
234
|
-
const command = execSync(`ps -p ${pid} -o command=`, {
|
|
235
|
-
encoding: 'utf-8',
|
|
236
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
237
|
-
timeout: 1000,
|
|
238
|
-
windowsHide: true,
|
|
239
|
-
}).replace(/\\/g, '/');
|
|
240
|
-
if (command.includes('/node_modules/shennian/') ||
|
|
241
|
-
command.includes('/bin/shennian') ||
|
|
242
|
-
command.includes(' shennian run-service') ||
|
|
243
|
-
command.includes(' shennian.js run-service')) {
|
|
244
|
-
return 'global-cli';
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
// Process command inspection is best effort.
|
|
249
|
-
}
|
|
250
|
-
return 'unknown';
|
|
251
|
-
}
|
|
252
|
-
export function isShennianRunServiceCommand(command) {
|
|
253
|
-
const normalized = command.replace(/\\/g, '/').trim();
|
|
254
|
-
return (/^(?:\S*\/)?node\s+\S*(?:\/node_modules\/shennian\/|\/dist\/bin\/shennian\.js|\/bin\/shennian)\s+run-service(?:\s|$)/.test(normalized) ||
|
|
255
|
-
/^(?:\S*\/)?shennian(?:\.cmd)?\s+run-service(?:\s|$)/.test(normalized) ||
|
|
256
|
-
/^(?:\S*\/)?npx(?:\.cmd)?\s+(?:--yes\s+)?shennian\s+run-service(?:\s|$)/.test(normalized));
|
|
257
|
-
}
|
|
258
|
-
export function findRunningDaemonProcessIds(excludePid = process.pid) {
|
|
259
|
-
if (getPlatform() === 'win32')
|
|
260
|
-
return [];
|
|
261
|
-
try {
|
|
262
|
-
const output = execSync('ps -axo pid=,command=', {
|
|
263
|
-
encoding: 'utf-8',
|
|
264
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
265
|
-
timeout: 1000,
|
|
266
|
-
windowsHide: true,
|
|
267
|
-
});
|
|
268
|
-
return output
|
|
269
|
-
.split(/\r?\n/)
|
|
270
|
-
.map((line) => {
|
|
271
|
-
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
272
|
-
if (!match)
|
|
273
|
-
return null;
|
|
274
|
-
const pid = Number(match[1]);
|
|
275
|
-
const command = match[2];
|
|
276
|
-
if (!Number.isInteger(pid) || pid === excludePid)
|
|
277
|
-
return null;
|
|
278
|
-
return isShennianRunServiceCommand(command) ? pid : null;
|
|
279
|
-
})
|
|
280
|
-
.filter((pid) => typeof pid === 'number');
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return [];
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
export function getDaemonStatus(opts = {}) {
|
|
287
|
-
let record = readPidRecord();
|
|
288
|
-
let pid = record?.pid ?? null;
|
|
289
|
-
let running = pid !== null && isRunning(pid);
|
|
290
|
-
let stale = pid !== null && !running;
|
|
291
|
-
let adopted = record?.adopted === true;
|
|
292
|
-
const config = loadConfig();
|
|
293
|
-
if (stale && opts.cleanupStale) {
|
|
294
|
-
try {
|
|
295
|
-
fs.unlinkSync(PID_FILE);
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
// Another process may have cleaned up the stale pid file first.
|
|
299
|
-
}
|
|
300
|
-
record = null;
|
|
301
|
-
pid = null;
|
|
302
|
-
running = false;
|
|
303
|
-
stale = false;
|
|
304
|
-
}
|
|
305
|
-
if (!running) {
|
|
306
|
-
const discoveredPid = findRunningDaemonProcessIds()[0];
|
|
307
|
-
if (discoveredPid) {
|
|
308
|
-
pid = discoveredPid;
|
|
309
|
-
running = true;
|
|
310
|
-
stale = false;
|
|
311
|
-
adopted = true;
|
|
312
|
-
if (opts.cleanupStale) {
|
|
313
|
-
const instanceId = createDaemonInstanceId();
|
|
314
|
-
writeDaemonPid(discoveredPid, instanceId, {
|
|
315
|
-
launcher: inferDaemonLauncherFromProcess(discoveredPid),
|
|
316
|
-
adopted: true,
|
|
317
|
-
});
|
|
318
|
-
writeDaemonLauncher(discoveredPid, inferDaemonLauncherFromProcess(discoveredPid));
|
|
319
|
-
record = { pid: discoveredPid, instanceId, adopted: true };
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
running,
|
|
325
|
-
pid,
|
|
326
|
-
stale,
|
|
327
|
-
remoteAccessDisabled: isRemoteAccessDisabled(),
|
|
328
|
-
launcher: readDaemonLauncher(pid, running),
|
|
329
|
-
platform: getPlatform(),
|
|
330
|
-
shennianDir: SHENNIAN_DIR,
|
|
331
|
-
pidFile: PID_FILE,
|
|
332
|
-
logFile: LOG_FILE,
|
|
333
|
-
...(record?.instanceId ? { instanceId: record.instanceId } : {}),
|
|
334
|
-
...(adopted ? { adopted: true } : {}),
|
|
335
|
-
...(config.machineId ? { machineId: config.machineId } : {}),
|
|
336
|
-
paired: Boolean(config.machineToken && config.machineId),
|
|
337
|
-
...(config.serverUrl ? { serverUrl: config.serverUrl } : {}),
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
function printJson(value) {
|
|
341
|
-
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
342
|
-
}
|
|
343
|
-
function persistServerUrlOverride(serverUrl) {
|
|
344
|
-
const normalized = serverUrl?.trim();
|
|
345
|
-
if (!normalized)
|
|
346
|
-
return;
|
|
347
|
-
const config = loadConfig();
|
|
348
|
-
config.serverUrl = normalized;
|
|
349
|
-
saveConfig(config);
|
|
350
|
-
}
|
|
351
|
-
function resolveServerUrlOverride(api) {
|
|
352
|
-
return api?.trim() || process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim() || undefined;
|
|
353
|
-
}
|
|
354
|
-
// ─── Service definitions ─────────────────────────────────────────────────────
|
|
355
|
-
const LAUNCHD_LABEL = 'com.shennian.agent';
|
|
356
|
-
const LAUNCHD_PLIST = path.join(os.homedir(), 'Library/LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
357
|
-
const WINDOWS_STARTUP_DIR = path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
358
|
-
const WINDOWS_STARTUP_CMD = resolveShennianPath('autostart.cmd');
|
|
359
|
-
const WINDOWS_LAUNCHER_VBS = resolveShennianPath('autostart.vbs');
|
|
360
|
-
const WINDOWS_TASK_XML = resolveShennianPath('autostart.xml');
|
|
361
|
-
const WINDOWS_STARTUP_VBS = path.join(WINDOWS_STARTUP_DIR, 'ShennianAgent.vbs');
|
|
362
|
-
function findCommandPath(binary) {
|
|
363
|
-
const command = getPlatform() === 'win32' ? `where ${binary}` : `command -v ${binary}`;
|
|
364
|
-
try {
|
|
365
|
-
const output = execSync(command, {
|
|
366
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
367
|
-
encoding: 'utf-8',
|
|
368
|
-
windowsHide: true,
|
|
369
|
-
});
|
|
370
|
-
const first = output
|
|
371
|
-
.split(/\r?\n/)
|
|
372
|
-
.map((line) => line.trim())
|
|
373
|
-
.find(Boolean);
|
|
374
|
-
return first ?? null;
|
|
375
|
-
}
|
|
376
|
-
catch {
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
function resolveNpxPath() {
|
|
381
|
-
const fileName = getPlatform() === 'win32' ? 'npx.cmd' : 'npx';
|
|
382
|
-
const sibling = path.join(path.dirname(NODE_EXEC), fileName);
|
|
383
|
-
if (fs.existsSync(sibling))
|
|
384
|
-
return sibling;
|
|
385
|
-
return findCommandPath('npx');
|
|
386
|
-
}
|
|
387
|
-
function resolveCurrentServiceLaunchSpec() {
|
|
388
|
-
return resolveServiceLaunchSpec({
|
|
389
|
-
nodeExec: NODE_EXEC,
|
|
390
|
-
scriptPath: SHENNIAN_SCRIPT,
|
|
391
|
-
shennianCommandPath: findCommandPath('shennian'),
|
|
392
|
-
npxPath: resolveNpxPath(),
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
function removeLegacyWindowsTask() {
|
|
396
|
-
try {
|
|
397
|
-
execSync(`schtasks /delete /tn "${WINDOWS_TASK_NAME}" /f`, { stdio: 'pipe', windowsHide: true });
|
|
398
|
-
}
|
|
399
|
-
catch {
|
|
400
|
-
// Ignore missing legacy Task Scheduler entries during migration.
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
function removeLegacyWindowsStartupFiles() {
|
|
404
|
-
try {
|
|
405
|
-
fs.unlinkSync(WINDOWS_STARTUP_VBS);
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
// Legacy Startup entry may already be absent.
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
function getWindowsTaskUserId() {
|
|
412
|
-
const domain = process.env.USERDOMAIN?.trim();
|
|
413
|
-
const computer = process.env.COMPUTERNAME?.trim();
|
|
414
|
-
const username = process.env.USERNAME?.trim() || os.userInfo().username;
|
|
415
|
-
if (domain && domain.toUpperCase() !== 'WORKGROUP')
|
|
416
|
-
return `${domain}\\${username}`;
|
|
417
|
-
if (computer)
|
|
418
|
-
return `${computer}\\${username}`;
|
|
419
|
-
return username;
|
|
420
|
-
}
|
|
421
|
-
function buildDetachedLaunchSpec(spec) {
|
|
422
|
-
if (getPlatform() === 'win32' && isWindowsCmdScript(spec.command)) {
|
|
423
|
-
const commandLine = [spec.command, ...spec.args].map(quoteCmdArg).join(' ');
|
|
424
|
-
return {
|
|
425
|
-
command: process.env.ComSpec || 'cmd.exe',
|
|
426
|
-
args: ['/d', '/s', '/c', `"${commandLine}"`],
|
|
427
|
-
windowsVerbatimArguments: true,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
return {
|
|
431
|
-
command: spec.command,
|
|
432
|
-
args: spec.args,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
export function buildDaemonSpawnOptions(launch, logFd, env = process.env) {
|
|
436
|
-
return {
|
|
437
|
-
detached: true,
|
|
438
|
-
stdio: ['ignore', logFd, logFd],
|
|
439
|
-
env,
|
|
440
|
-
windowsHide: true,
|
|
441
|
-
...(launch.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
function installWindowsScheduledTask() {
|
|
445
|
-
const launch = resolveCurrentServiceLaunchSpec();
|
|
446
|
-
fs.writeFileSync(WINDOWS_STARTUP_CMD, buildWindowsLauncherCommand(launch, LOG_FILE));
|
|
447
|
-
fs.writeFileSync(WINDOWS_LAUNCHER_VBS, buildWindowsStartupVbs(WINDOWS_STARTUP_CMD));
|
|
448
|
-
removeLegacyWindowsStartupFiles();
|
|
449
|
-
removeLegacyWindowsTask();
|
|
450
|
-
const taskXml = buildWindowsScheduledTaskXml({
|
|
451
|
-
userId: getWindowsTaskUserId(),
|
|
452
|
-
command: path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'wscript.exe'),
|
|
453
|
-
arguments: [quoteCmdArg(WINDOWS_LAUNCHER_VBS)],
|
|
454
|
-
});
|
|
455
|
-
fs.writeFileSync(WINDOWS_TASK_XML, taskXml, 'utf8');
|
|
456
|
-
try {
|
|
457
|
-
execSync(`schtasks /create /tn "${WINDOWS_TASK_NAME}" /xml "${WINDOWS_TASK_XML}" /f`, {
|
|
458
|
-
stdio: 'pipe',
|
|
459
|
-
windowsHide: true,
|
|
460
|
-
});
|
|
461
|
-
execSync(`schtasks /run /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe', windowsHide: true });
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
catch {
|
|
465
|
-
return false;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
export function captureEnvForService() {
|
|
469
|
-
const env = {};
|
|
470
|
-
for (const k of SAFE_SNAPSHOT_ENV_KEYS) {
|
|
471
|
-
if (process.env[k])
|
|
472
|
-
env[k] = process.env[k];
|
|
473
|
-
}
|
|
474
|
-
env.HOME ??= os.homedir();
|
|
475
|
-
env.PATH = buildAugmentedPath({ pathValue: env.PATH, env });
|
|
476
|
-
env.USER ??= os.userInfo().username;
|
|
477
|
-
if (getPlatform() === 'win32') {
|
|
478
|
-
const tempDir = process.env.TEMP || process.env.TMP || os.tmpdir();
|
|
479
|
-
env.TMPDIR ??= tempDir;
|
|
480
|
-
env.TEMP ??= tempDir;
|
|
481
|
-
env.TMP ??= tempDir;
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
env.TMPDIR ??= '/tmp';
|
|
485
|
-
}
|
|
486
|
-
return env;
|
|
487
|
-
}
|
|
488
|
-
function buildPlist() {
|
|
489
|
-
const env = captureEnvForService();
|
|
490
|
-
const launch = resolveCurrentServiceLaunchSpec();
|
|
491
|
-
const envEntries = Object.entries(env)
|
|
492
|
-
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
493
|
-
.join('\n');
|
|
494
|
-
const programArgs = [launch.command, ...launch.args]
|
|
495
|
-
.map((value) => ` <string>${escapeXml(value)}</string>`)
|
|
496
|
-
.join('\n');
|
|
497
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1
|
+
import s from"chalk";import i from"node:fs";import u from"node:path";import d from"node:os";import{execSync as c,spawn as oe}from"node:child_process";import{randomUUID as se}from"node:crypto";import{fileURLToPath as ce}from"node:url";import{getShennianDir as ae,loadConfig as H,resolveShennianPath as h,saveConfig as ue}from"../config/index.js";import{buildAugmentedPath as de}from"../env-path.js";import{buildWindowsLauncherCommand as le,buildWindowsScheduledTaskXml as pe,buildWindowsStartupVbs as me,escapeXml as b,isWindowsCmdScript as fe,quoteCmdArg as U,quoteSystemdArg as Se}from"./daemon-windows.js";import{buildWindowsLauncherCommand as sn,buildWindowsScheduledTaskXml as cn,buildWindowsStartupVbs as an}from"./daemon-windows.js";const he=u.dirname(ce(import.meta.url)),y=ae(),p=h("daemon.pid"),a=h("daemon.log"),N=h("remote-access.disabled"),D=h("daemon-launcher.json"),W=u.resolve(he,"../../bin/shennian.js"),F=process.execPath,M=new Set(["PATH","HOME","USER","LOGNAME","SHELL","TMPDIR","LANG","LC_ALL","LC_CTYPE","SSH_AUTH_SOCK","XDG_CONFIG_HOME","XDG_DATA_HOME","TEMP","TMP","APPDATA","LOCALAPPDATA","SHENNIAN_DESKTOP_SERVER_URL","SHENNIAN_HOME","SHENNIAN_NATIVE_FUSION_DISABLED"]);function ge(e){return M.has(e)}function l(){const e=d.platform();return e==="darwin"||e==="linux"||e==="win32"?e:"linux"}function x(e){const n=e.replace(/\\/g,"/").toLowerCase(),t=d.tmpdir().replace(/\\/g,"/").toLowerCase();return n.includes("/_npx/")||n.includes("/npm-cache/_npx/")||n.includes("/pnpm/dlx/")||n.startsWith(t.endsWith("/")?t:`${t}/`)}function ye(e){return i.existsSync(e.scriptPath)&&!x(e.scriptPath)?{command:e.nodeExec,args:[e.scriptPath,"run-service"],mode:"direct"}:e.shennianCommandPath&&!x(e.shennianCommandPath)?{command:e.shennianCommandPath,args:["run-service"],mode:"global-shim"}:e.npxPath?{command:e.npxPath,args:["--yes","shennian","run-service"],mode:"npx"}:{command:e.nodeExec,args:[e.scriptPath,"run-service"],mode:"direct"}}function we(e){const n=e.trim();if(!n)return null;if(n.startsWith("{"))try{const r=JSON.parse(n),o=Number(r.pid);return!Number.isInteger(o)||o<=0?null:{pid:o,...typeof r.instanceId=="string"?{instanceId:r.instanceId}:{},...typeof r.version=="string"?{version:r.version}:{},...r.launcher==="desktop-managed"||r.launcher==="global-cli"||r.launcher==="unknown"?{launcher:r.launcher}:{},...typeof r.startedAt=="string"?{startedAt:r.startedAt}:{},...r.adopted===!0?{adopted:!0}:{}}}catch{return null}const t=parseInt(n,10);return isNaN(t)?null:{pid:t}}function L(){try{return we(i.readFileSync(p,"utf-8"))}catch{return null}}function ve(){return L()?.pid??null}function w(e){try{return process.kill(e,0),!0}catch{return!1}}function ke(e,n=3e3){const t=Date.now();for(;Date.now()-t<n;){if(!w(e))return!0;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,100)}return!w(e)}function Pe(){return i.existsSync(N)}function Ae(){try{i.unlinkSync(N)}catch{}}function B(){return"global-cli"}function Ie(){return`${process.pid}-${Date.now()}-${se()}`}function De(e,n,t={}){i.mkdirSync(y,{recursive:!0}),i.writeFileSync(p,JSON.stringify({pid:e,instanceId:n,...t.version?{version:t.version}:{},launcher:t.launcher??B(),...t.adopted?{adopted:!0}:{},startedAt:new Date().toISOString()},null,2))}function nn(e,n){const t=L();if(!(t?.pid!==e||t.instanceId!==n))try{i.unlinkSync(p)}catch{}}function J(e,n=B(),t){i.mkdirSync(y,{recursive:!0}),i.writeFileSync(D,JSON.stringify({pid:e,launcher:n,...t?{instanceId:t}:{},updatedAt:new Date().toISOString()},null,2))}function $(e){if(e)try{if(JSON.parse(i.readFileSync(D,"utf-8")).instanceId!==e)return}catch{return}try{i.unlinkSync(D)}catch{}}function Ee(e){e&&J(e)}function be(e,n){if(!e||!n)return"unknown";try{const t=JSON.parse(i.readFileSync(D,"utf-8"));if(t.pid!==e)return"unknown";if(t.launcher==="desktop-managed"||t.launcher==="global-cli")return t.launcher}catch{}return T(e)}function T(e){if(l()==="win32")return"unknown";try{const n=c(`ps -p ${e} -o command=`,{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:1e3,windowsHide:!0}).replace(/\\/g,"/");if(n.includes("/node_modules/shennian/")||n.includes("/bin/shennian")||n.includes(" shennian run-service")||n.includes(" shennian.js run-service"))return"global-cli"}catch{}return"unknown"}function Ne(e){const n=e.replace(/\\/g,"/").trim();return/^(?:\S*\/)?node\s+\S*(?:\/node_modules\/shennian\/|\/dist\/bin\/shennian\.js|\/bin\/shennian)\s+run-service(?:\s|$)/.test(n)||/^(?:\S*\/)?shennian(?:\.cmd)?\s+run-service(?:\s|$)/.test(n)||/^(?:\S*\/)?npx(?:\.cmd)?\s+(?:--yes\s+)?shennian\s+run-service(?:\s|$)/.test(n)}function G(e=process.pid){if(l()==="win32")return[];try{return c("ps -axo pid=,command=",{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:1e3,windowsHide:!0}).split(/\r?\n/).map(t=>{const r=t.trim().match(/^(\d+)\s+(.+)$/);if(!r)return null;const o=Number(r[1]),S=r[2];return!Number.isInteger(o)||o===e?null:Ne(S)?o:null}).filter(t=>typeof t=="number")}catch{return[]}}function f(e={}){let n=L(),t=n?.pid??null,r=t!==null&&w(t),o=t!==null&&!r,S=n?.adopted===!0;const v=H();if(o&&e.cleanupStale){try{i.unlinkSync(p)}catch{}n=null,t=null,r=!1,o=!1}if(!r){const g=G()[0];if(g&&(t=g,r=!0,o=!1,S=!0,e.cleanupStale)){const C=Ie();De(g,C,{launcher:T(g),adopted:!0}),J(g,T(g)),n={pid:g,instanceId:C,adopted:!0}}}return{running:r,pid:t,stale:o,remoteAccessDisabled:Pe(),launcher:be(t,r),platform:l(),shennianDir:y,pidFile:p,logFile:a,...n?.instanceId?{instanceId:n.instanceId}:{},...S?{adopted:!0}:{},...v.machineId?{machineId:v.machineId}:{},paired:!!(v.machineToken&&v.machineId),...v.serverUrl?{serverUrl:v.serverUrl}:{}}}function k(e){process.stdout.write(`${JSON.stringify(e,null,2)}
|
|
2
|
+
`)}function V(e){const n=e?.trim();if(!n)return;const t=H();t.serverUrl=n,ue(t)}function K(e){return e?.trim()||process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim()||void 0}const X="com.shennian.agent",m=u.join(d.homedir(),"Library/LaunchAgents",`${X}.plist`),xe=u.join(d.homedir(),"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup"),O=h("autostart.cmd"),_=h("autostart.vbs"),R=h("autostart.xml"),Le=u.join(xe,"ShennianAgent.vbs");function q(e){const n=l()==="win32"?`where ${e}`:`command -v ${e}`;try{return c(n,{stdio:["ignore","pipe","pipe"],encoding:"utf-8",windowsHide:!0}).split(/\r?\n/).map(o=>o.trim()).find(Boolean)??null}catch{return null}}function $e(){const e=l()==="win32"?"npx.cmd":"npx",n=u.join(u.dirname(F),e);return i.existsSync(n)?n:q("npx")}function P(){return ye({nodeExec:F,scriptPath:W,shennianCommandPath:q("shennian"),npxPath:$e()})}function Y(){try{c(`schtasks /delete /tn "${A}" /f`,{stdio:"pipe",windowsHide:!0})}catch{}}function z(){try{i.unlinkSync(Le)}catch{}}function Te(){const e=process.env.USERDOMAIN?.trim(),n=process.env.COMPUTERNAME?.trim(),t=process.env.USERNAME?.trim()||d.userInfo().username;return e&&e.toUpperCase()!=="WORKGROUP"?`${e}\\${t}`:n?`${n}\\${t}`:t}function Oe(e){if(l()==="win32"&&fe(e.command)){const n=[e.command,...e.args].map(U).join(" ");return{command:process.env.ComSpec||"cmd.exe",args:["/d","/s","/c",`"${n}"`],windowsVerbatimArguments:!0}}return{command:e.command,args:e.args}}function _e(e,n,t=process.env){return{detached:!0,stdio:["ignore",n,n],env:t,windowsHide:!0,...e.windowsVerbatimArguments?{windowsVerbatimArguments:!0}:{}}}function Re(){const e=P();i.writeFileSync(O,le(e,a)),i.writeFileSync(_,me(O)),z(),Y();const n=pe({userId:Te(),command:u.join(process.env.SystemRoot||"C:\\Windows","System32","wscript.exe"),arguments:[U(_)]});i.writeFileSync(R,n,"utf8");try{return c(`schtasks /create /tn "${A}" /xml "${R}" /f`,{stdio:"pipe",windowsHide:!0}),c(`schtasks /run /tn "${A}"`,{stdio:"pipe",windowsHide:!0}),!0}catch{return!1}}function Q(){const e={};for(const n of M)process.env[n]&&(e[n]=process.env[n]);if(e.HOME??=d.homedir(),e.PATH=de({pathValue:e.PATH,env:e}),e.USER??=d.userInfo().username,l()==="win32"){const n=process.env.TEMP||process.env.TMP||d.tmpdir();e.TMPDIR??=n,e.TEMP??=n,e.TMP??=n}else e.TMPDIR??="/tmp";return e}function je(){const e=Q(),n=P(),t=Object.entries(e).map(([o,S])=>` <key>${b(o)}</key>
|
|
3
|
+
<string>${b(S)}</string>`).join(`
|
|
4
|
+
`),r=[n.command,...n.args].map(o=>` <string>${b(o)}</string>`).join(`
|
|
5
|
+
`);return`<?xml version="1.0" encoding="UTF-8"?>
|
|
498
6
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
499
7
|
<plist version="1.0">
|
|
500
8
|
<dict>
|
|
501
9
|
<key>Label</key>
|
|
502
|
-
<string>${
|
|
10
|
+
<string>${X}</string>
|
|
503
11
|
<key>ProgramArguments</key>
|
|
504
12
|
<array>
|
|
505
|
-
${
|
|
13
|
+
${r}
|
|
506
14
|
</array>
|
|
507
15
|
<key>EnvironmentVariables</key>
|
|
508
16
|
<dict>
|
|
509
|
-
${
|
|
17
|
+
${t}
|
|
510
18
|
</dict>
|
|
511
19
|
<key>RunAtLoad</key>
|
|
512
20
|
<true/>
|
|
@@ -515,526 +23,24 @@ ${envEntries}
|
|
|
515
23
|
<key>ThrottleInterval</key>
|
|
516
24
|
<integer>5</integer>
|
|
517
25
|
<key>StandardOutPath</key>
|
|
518
|
-
<string>${
|
|
26
|
+
<string>${a}</string>
|
|
519
27
|
<key>StandardErrorPath</key>
|
|
520
|
-
<string>${
|
|
28
|
+
<string>${a}</string>
|
|
521
29
|
</dict>
|
|
522
|
-
</plist
|
|
523
|
-
|
|
524
|
-
const SYSTEMD_UNIT = path.join(os.homedir(), '.config/systemd/user/shennian.service');
|
|
525
|
-
function buildSystemdUnit() {
|
|
526
|
-
const env = captureEnvForService();
|
|
527
|
-
const launch = resolveCurrentServiceLaunchSpec();
|
|
528
|
-
const envLines = Object.entries(env)
|
|
529
|
-
.map(([k, v]) => `Environment=${k}=${v}`)
|
|
530
|
-
.join('\n');
|
|
531
|
-
const execStart = [launch.command, ...launch.args].map(quoteSystemdArg).join(' ');
|
|
532
|
-
return `[Unit]
|
|
30
|
+
</plist>`}const E=u.join(d.homedir(),".config/systemd/user/shennian.service");function Ce(){const e=Q(),n=P(),t=Object.entries(e).map(([o,S])=>`Environment=${o}=${S}`).join(`
|
|
31
|
+
`),r=[n.command,...n.args].map(Se).join(" ");return`[Unit]
|
|
533
32
|
Description=Shennian Agent Daemon
|
|
534
33
|
After=network.target
|
|
535
34
|
|
|
536
35
|
[Service]
|
|
537
|
-
${
|
|
538
|
-
ExecStart=${
|
|
36
|
+
${t}
|
|
37
|
+
ExecStart=${r}
|
|
539
38
|
Restart=always
|
|
540
39
|
RestartSec=10
|
|
541
|
-
StandardOutput=append:${
|
|
542
|
-
StandardError=append:${
|
|
40
|
+
StandardOutput=append:${a}
|
|
41
|
+
StandardError=append:${a}
|
|
543
42
|
|
|
544
43
|
[Install]
|
|
545
|
-
WantedBy=default.target
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
/**
|
|
549
|
-
* Save current env vars so the daemon can load them at startup.
|
|
550
|
-
* This is critical for launchd/systemd which start with minimal env.
|
|
551
|
-
*/
|
|
552
|
-
export function saveEnvSnapshot() {
|
|
553
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
554
|
-
const snapshot = {};
|
|
555
|
-
for (const [k, v] of Object.entries(process.env)) {
|
|
556
|
-
if (v !== undefined && isSafeSnapshotEnvKey(k))
|
|
557
|
-
snapshot[k] = v;
|
|
558
|
-
}
|
|
559
|
-
fs.writeFileSync(resolveShennianPath('env.json'), JSON.stringify(snapshot, null, 2));
|
|
560
|
-
}
|
|
561
|
-
// ─── Exported helpers (used by pair.ts) ─────────────────────────────────────
|
|
562
|
-
export function startDaemonProcess(opts = {}) {
|
|
563
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
564
|
-
const status = getDaemonStatus({ cleanupStale: true });
|
|
565
|
-
if (status.pid !== null && status.running && !status.adopted) {
|
|
566
|
-
if (!opts.quiet) {
|
|
567
|
-
console.log(chalk.green(`✓ Background service already running (PID ${status.pid})`));
|
|
568
|
-
}
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
stopAdoptedDaemonForStart(status);
|
|
572
|
-
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
573
|
-
const launch = buildDetachedLaunchSpec(resolveCurrentServiceLaunchSpec());
|
|
574
|
-
const child = spawn(launch.command, launch.args, buildDaemonSpawnOptions(launch, logFd));
|
|
575
|
-
child.unref();
|
|
576
|
-
fs.closeSync(logFd);
|
|
577
|
-
recordStartedDaemon(child.pid);
|
|
578
|
-
if (!opts.quiet) {
|
|
579
|
-
console.log(chalk.green(`✓ Background service started (PID ${child.pid})`));
|
|
580
|
-
console.log(chalk.gray(` Logs: ${LOG_FILE}`));
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
function stopAdoptedDaemonForStart(status = getDaemonStatus({ cleanupStale: true })) {
|
|
584
|
-
if (status.pid === null || !status.running || !status.adopted)
|
|
585
|
-
return;
|
|
586
|
-
try {
|
|
587
|
-
process.kill(status.pid, 'SIGTERM');
|
|
588
|
-
waitForPidExitSync(status.pid);
|
|
589
|
-
try {
|
|
590
|
-
fs.unlinkSync(PID_FILE);
|
|
591
|
-
}
|
|
592
|
-
catch {
|
|
593
|
-
// The adopted daemon may have cleaned up or another start may have taken over.
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
catch {
|
|
597
|
-
// The adopted orphan may already have exited; continue with a clean start.
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Install the platform-native auto-start service.
|
|
602
|
-
* Returns true if the service was immediately started (so caller can skip startDaemonProcess).
|
|
603
|
-
*/
|
|
604
|
-
export function installService() {
|
|
605
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
606
|
-
const platform = getPlatform();
|
|
607
|
-
const spec = resolveCurrentServiceLaunchSpec();
|
|
608
|
-
if (spec.mode === 'direct' && isEphemeralCliPath(SHENNIAN_SCRIPT)) {
|
|
609
|
-
console.warn(chalk.yellow('⚠ Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n' +
|
|
610
|
-
' Run `npm install -g shennian@latest` for reliable auto-start.'));
|
|
611
|
-
}
|
|
612
|
-
switch (platform) {
|
|
613
|
-
case 'darwin': {
|
|
614
|
-
const dir = path.dirname(LAUNCHD_PLIST);
|
|
615
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
616
|
-
fs.writeFileSync(LAUNCHD_PLIST, buildPlist());
|
|
617
|
-
try {
|
|
618
|
-
// Unload first (ignore errors), then reload - this starts the service immediately
|
|
619
|
-
execSync(`launchctl unload "${LAUNCHD_PLIST}" 2>/dev/null; launchctl load -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe', windowsHide: true });
|
|
620
|
-
return true; // launchd started it; caller must NOT also call startDaemonProcess
|
|
621
|
-
}
|
|
622
|
-
catch {
|
|
623
|
-
try {
|
|
624
|
-
execSync(`launchctl load -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe', windowsHide: true });
|
|
625
|
-
return true;
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
// launchctl can fail on locked-down shells; caller falls back to manual start below.
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
case 'linux': {
|
|
634
|
-
const dir = path.dirname(SYSTEMD_UNIT);
|
|
635
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
636
|
-
fs.writeFileSync(SYSTEMD_UNIT, buildSystemdUnit());
|
|
637
|
-
try {
|
|
638
|
-
execSync('systemctl --user daemon-reload && systemctl --user enable shennian', {
|
|
639
|
-
stdio: 'pipe',
|
|
640
|
-
windowsHide: true,
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
catch {
|
|
644
|
-
// Some Linux environments lack systemd user services; keep best-effort behavior.
|
|
645
|
-
return false;
|
|
646
|
-
}
|
|
647
|
-
// Enable linger so the user systemd session (and thus this service) persists
|
|
648
|
-
// across reboots even without an active login session.
|
|
649
|
-
try {
|
|
650
|
-
execSync(`loginctl enable-linger ${os.userInfo().username}`, { stdio: 'pipe', windowsHide: true });
|
|
651
|
-
}
|
|
652
|
-
catch {
|
|
653
|
-
// loginctl is unavailable on some distros/containers; auto-start still works after login.
|
|
654
|
-
}
|
|
655
|
-
try {
|
|
656
|
-
execSync('systemctl --user restart shennian', { stdio: 'pipe', windowsHide: true });
|
|
657
|
-
return true;
|
|
658
|
-
}
|
|
659
|
-
catch {
|
|
660
|
-
return false;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
case 'win32': {
|
|
664
|
-
return installWindowsScheduledTask();
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
// ─── Subcommand implementations ──────────────────────────────────────────────
|
|
669
|
-
function stopDaemonProcess() {
|
|
670
|
-
const status = getDaemonStatus({ cleanupStale: true });
|
|
671
|
-
const pid = status.pid;
|
|
672
|
-
if (pid === null) {
|
|
673
|
-
return {};
|
|
674
|
-
}
|
|
675
|
-
if (!isRunning(pid)) {
|
|
676
|
-
try {
|
|
677
|
-
fs.unlinkSync(PID_FILE);
|
|
678
|
-
}
|
|
679
|
-
catch {
|
|
680
|
-
// Stale pid file is already gone.
|
|
681
|
-
}
|
|
682
|
-
return { stalePid: pid };
|
|
683
|
-
}
|
|
684
|
-
try {
|
|
685
|
-
process.kill(pid, 'SIGTERM');
|
|
686
|
-
return { stoppedPid: pid };
|
|
687
|
-
}
|
|
688
|
-
catch (err) {
|
|
689
|
-
return { error: err instanceof Error ? err.message : String(err) };
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
async function stopDaemonProcessAndWait(timeoutMs = 5000) {
|
|
693
|
-
const result = stopDaemonProcess();
|
|
694
|
-
if (!result.stoppedPid)
|
|
695
|
-
return result;
|
|
696
|
-
const stopped = await waitForPidExit(result.stoppedPid, timeoutMs);
|
|
697
|
-
if (!stopped) {
|
|
698
|
-
try {
|
|
699
|
-
process.kill(result.stoppedPid, 'SIGKILL');
|
|
700
|
-
await waitForPidExit(result.stoppedPid, 2000);
|
|
701
|
-
}
|
|
702
|
-
catch (err) {
|
|
703
|
-
return { ...result, error: err instanceof Error ? err.message : String(err) };
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
try {
|
|
707
|
-
fs.unlinkSync(PID_FILE);
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
710
|
-
// Best-effort cleanup after the process exits.
|
|
711
|
-
}
|
|
712
|
-
clearDaemonLauncher();
|
|
713
|
-
for (const orphanPid of findRunningDaemonProcessIds()) {
|
|
714
|
-
if (orphanPid === result.stoppedPid)
|
|
715
|
-
continue;
|
|
716
|
-
try {
|
|
717
|
-
process.kill(orphanPid, 'SIGTERM');
|
|
718
|
-
await waitForPidExit(orphanPid, 2000);
|
|
719
|
-
}
|
|
720
|
-
catch {
|
|
721
|
-
// Best-effort orphan cleanup; the main managed daemon has already stopped.
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
return result;
|
|
725
|
-
}
|
|
726
|
-
function enableRemoteAccess(opts = {}) {
|
|
727
|
-
persistServerUrlOverride(resolveServerUrlOverride(opts.api));
|
|
728
|
-
clearRemoteAccessDisabled();
|
|
729
|
-
stopAdoptedDaemonForStart();
|
|
730
|
-
const startedByService = installService();
|
|
731
|
-
if (!startedByService) {
|
|
732
|
-
startDaemonProcess({ quiet: opts.json });
|
|
733
|
-
}
|
|
734
|
-
if (opts.json) {
|
|
735
|
-
printJson(getDaemonStatus());
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
console.log(chalk.green('✓ Background service started'));
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
async function disableRemoteAccess(opts = {}) {
|
|
742
|
-
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
743
|
-
fs.writeFileSync(REMOTE_ACCESS_DISABLED_FILE, new Date().toISOString());
|
|
744
|
-
const platform = getPlatform();
|
|
745
|
-
switch (platform) {
|
|
746
|
-
case 'darwin': {
|
|
747
|
-
if (fs.existsSync(LAUNCHD_PLIST)) {
|
|
748
|
-
try {
|
|
749
|
-
execSync(`launchctl unload -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe', windowsHide: true });
|
|
750
|
-
}
|
|
751
|
-
catch {
|
|
752
|
-
// The job may already be unloaded; keep the plist for future re-enable.
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
break;
|
|
756
|
-
}
|
|
757
|
-
case 'linux': {
|
|
758
|
-
try {
|
|
759
|
-
execSync('systemctl --user disable --now shennian', { stdio: 'pipe', windowsHide: true });
|
|
760
|
-
}
|
|
761
|
-
catch {
|
|
762
|
-
// systemd user services may be unavailable; still stop the manual daemon.
|
|
763
|
-
}
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
766
|
-
case 'win32': {
|
|
767
|
-
try {
|
|
768
|
-
execSync(`schtasks /change /tn "${WINDOWS_TASK_NAME}" /disable`, { stdio: 'pipe', windowsHide: true });
|
|
769
|
-
}
|
|
770
|
-
catch {
|
|
771
|
-
// Task may not exist yet; still stop the manual daemon.
|
|
772
|
-
}
|
|
773
|
-
try {
|
|
774
|
-
execSync(`schtasks /end /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe', windowsHide: true });
|
|
775
|
-
}
|
|
776
|
-
catch {
|
|
777
|
-
// Task may not be running.
|
|
778
|
-
}
|
|
779
|
-
break;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
const result = await stopDaemonProcessAndWait();
|
|
783
|
-
const status = getDaemonStatus({ cleanupStale: true });
|
|
784
|
-
if (opts.json) {
|
|
785
|
-
printJson(result.error ? { ...status, error: result.error } : status);
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
if (result.error) {
|
|
789
|
-
console.error(chalk.red(`✗ Failed to stop: ${result.error}`));
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
console.log(chalk.green('✓ Background service stopped'));
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
function daemonStart(opts) {
|
|
796
|
-
saveEnvSnapshot();
|
|
797
|
-
enableRemoteAccess(opts);
|
|
798
|
-
}
|
|
799
|
-
async function daemonStop(opts = {}) {
|
|
800
|
-
await disableRemoteAccess(opts);
|
|
801
|
-
}
|
|
802
|
-
async function waitForPidExit(pid, timeoutMs = 5000) {
|
|
803
|
-
const startedAt = Date.now();
|
|
804
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
805
|
-
if (!isRunning(pid))
|
|
806
|
-
return true;
|
|
807
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
808
|
-
}
|
|
809
|
-
return !isRunning(pid);
|
|
810
|
-
}
|
|
811
|
-
async function daemonRestart(opts = {}) {
|
|
812
|
-
persistServerUrlOverride(resolveServerUrlOverride(opts.api));
|
|
813
|
-
const pid = readPid();
|
|
814
|
-
if (pid !== null && isRunning(pid)) {
|
|
815
|
-
try {
|
|
816
|
-
process.kill(pid, 'SIGTERM');
|
|
817
|
-
const stopped = await waitForPidExit(pid);
|
|
818
|
-
if (!stopped) {
|
|
819
|
-
process.kill(pid, 'SIGKILL');
|
|
820
|
-
await waitForPidExit(pid, 2000);
|
|
821
|
-
}
|
|
822
|
-
try {
|
|
823
|
-
fs.unlinkSync(PID_FILE);
|
|
824
|
-
}
|
|
825
|
-
catch {
|
|
826
|
-
// Best-effort cleanup after restart stop.
|
|
827
|
-
}
|
|
828
|
-
clearDaemonLauncher();
|
|
829
|
-
if (!opts.json) {
|
|
830
|
-
console.log(chalk.green(`✓ Background service stopped (PID ${pid})`));
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
catch (err) {
|
|
834
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
835
|
-
if (opts.json) {
|
|
836
|
-
printJson({ ...getDaemonStatus(), error: message });
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
console.error(chalk.red(`✗ Failed to stop: ${message}`));
|
|
840
|
-
}
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
else if (pid !== null) {
|
|
845
|
-
try {
|
|
846
|
-
fs.unlinkSync(PID_FILE);
|
|
847
|
-
}
|
|
848
|
-
catch {
|
|
849
|
-
// Stale pid file is already gone.
|
|
850
|
-
}
|
|
851
|
-
clearDaemonLauncher();
|
|
852
|
-
if (!opts.json) {
|
|
853
|
-
console.log(chalk.yellow(`⚠ Service process no longer exists, cleaned up stale PID ${pid}`));
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
if (!opts.json) {
|
|
858
|
-
console.log(chalk.gray('● Background service not running, starting it now'));
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
if (getPlatform() === 'linux') {
|
|
862
|
-
const startedByService = installService();
|
|
863
|
-
if (startedByService) {
|
|
864
|
-
if (opts.json)
|
|
865
|
-
printJson(getDaemonStatus());
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
startDaemonProcess({ quiet: opts.json });
|
|
870
|
-
if (opts.json)
|
|
871
|
-
printJson(getDaemonStatus());
|
|
872
|
-
}
|
|
873
|
-
function daemonStatus(opts = {}) {
|
|
874
|
-
const status = getDaemonStatus({ cleanupStale: true });
|
|
875
|
-
if (opts.json) {
|
|
876
|
-
printJson(status);
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
if (status.pid === null) {
|
|
880
|
-
console.log(chalk.gray('● Background service not running'));
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
if (status.running) {
|
|
884
|
-
console.log(chalk.green(`● Background service running (PID ${status.pid})`));
|
|
885
|
-
console.log(chalk.gray(` Logs: ${LOG_FILE}`));
|
|
886
|
-
}
|
|
887
|
-
else {
|
|
888
|
-
console.log(chalk.yellow(`● Background service stopped (PID ${status.pid} is stale)`));
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
function daemonLogs(opts) {
|
|
892
|
-
if (!fs.existsSync(LOG_FILE)) {
|
|
893
|
-
console.log(chalk.gray('No logs yet'));
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
try {
|
|
897
|
-
const out = execSync(os.platform() === 'win32'
|
|
898
|
-
? `powershell Get-Content -Tail ${opts.lines} "${LOG_FILE}"`
|
|
899
|
-
: `tail -n ${opts.lines} "${LOG_FILE}"`, { encoding: 'utf-8', windowsHide: true });
|
|
900
|
-
process.stdout.write(out);
|
|
901
|
-
}
|
|
902
|
-
catch {
|
|
903
|
-
const lines = fs.readFileSync(LOG_FILE, 'utf-8').split('\n').slice(-opts.lines);
|
|
904
|
-
console.log(lines.join('\n'));
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
async function daemonUninstall() {
|
|
908
|
-
await daemonStop();
|
|
909
|
-
const platform = getPlatform();
|
|
910
|
-
switch (platform) {
|
|
911
|
-
case 'darwin': {
|
|
912
|
-
if (fs.existsSync(LAUNCHD_PLIST)) {
|
|
913
|
-
try {
|
|
914
|
-
execSync(`launchctl unload -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe', windowsHide: true });
|
|
915
|
-
}
|
|
916
|
-
catch {
|
|
917
|
-
// Continue removing the plist even if launchctl already forgot about it.
|
|
918
|
-
}
|
|
919
|
-
fs.unlinkSync(LAUNCHD_PLIST);
|
|
920
|
-
console.log(chalk.green('✓ Auto-start uninstalled (launchd)'));
|
|
921
|
-
}
|
|
922
|
-
break;
|
|
923
|
-
}
|
|
924
|
-
case 'linux': {
|
|
925
|
-
try {
|
|
926
|
-
execSync('systemctl --user disable --now shennian', { stdio: 'pipe', windowsHide: true });
|
|
927
|
-
}
|
|
928
|
-
catch {
|
|
929
|
-
// Service may already be absent or systemd user services may be unavailable.
|
|
930
|
-
}
|
|
931
|
-
if (fs.existsSync(SYSTEMD_UNIT)) {
|
|
932
|
-
fs.unlinkSync(SYSTEMD_UNIT);
|
|
933
|
-
try {
|
|
934
|
-
execSync('systemctl --user daemon-reload', { stdio: 'pipe', windowsHide: true });
|
|
935
|
-
}
|
|
936
|
-
catch {
|
|
937
|
-
// Ignore reload errors during cleanup.
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
console.log(chalk.green('✓ Auto-start uninstalled (systemd)'));
|
|
941
|
-
break;
|
|
942
|
-
}
|
|
943
|
-
case 'win32': {
|
|
944
|
-
removeLegacyWindowsTask();
|
|
945
|
-
try {
|
|
946
|
-
fs.unlinkSync(WINDOWS_STARTUP_CMD);
|
|
947
|
-
}
|
|
948
|
-
catch {
|
|
949
|
-
// Launcher script may already be removed.
|
|
950
|
-
}
|
|
951
|
-
try {
|
|
952
|
-
fs.unlinkSync(WINDOWS_LAUNCHER_VBS);
|
|
953
|
-
}
|
|
954
|
-
catch {
|
|
955
|
-
// Launcher vbs may already be removed.
|
|
956
|
-
}
|
|
957
|
-
try {
|
|
958
|
-
fs.unlinkSync(WINDOWS_TASK_XML);
|
|
959
|
-
}
|
|
960
|
-
catch {
|
|
961
|
-
// Task xml may already be removed.
|
|
962
|
-
}
|
|
963
|
-
removeLegacyWindowsStartupFiles();
|
|
964
|
-
console.log(chalk.green('✓ Auto-start uninstalled (Task Scheduler)'));
|
|
965
|
-
break;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
// ─── Register command ─────────────────────────────────────────────────────────
|
|
970
|
-
export function registerDaemonCommand(program) {
|
|
971
|
-
program
|
|
972
|
-
.command('start')
|
|
973
|
-
.description('Start the background service')
|
|
974
|
-
.option('--json', 'Print machine-readable status JSON')
|
|
975
|
-
.option('--api <url>', 'Server URL override')
|
|
976
|
-
.action((opts) => daemonStart(opts));
|
|
977
|
-
program
|
|
978
|
-
.command('stop')
|
|
979
|
-
.description('Stop the background service')
|
|
980
|
-
.option('--json', 'Print machine-readable status JSON')
|
|
981
|
-
.action((opts) => daemonStop(opts));
|
|
982
|
-
program
|
|
983
|
-
.command('status')
|
|
984
|
-
.description('Show background service status')
|
|
985
|
-
.option('--json', 'Print machine-readable status JSON')
|
|
986
|
-
.action((opts) => daemonStatus(opts));
|
|
987
|
-
program
|
|
988
|
-
.command('logs')
|
|
989
|
-
.description('Show recent logs')
|
|
990
|
-
.option('-n, --lines <n>', 'Number of lines', '50')
|
|
991
|
-
.action((opts) => daemonLogs({ lines: parseInt(opts.lines, 10) }));
|
|
992
|
-
const daemon = program.command('daemon', { hidden: true }).description('Deprecated: use top-level start/stop/status/logs');
|
|
993
|
-
const warnDeprecated = (replacement, json) => {
|
|
994
|
-
if (!json)
|
|
995
|
-
console.error(chalk.yellow(`⚠ Deprecated command. Use \`shennian ${replacement}\` instead.`));
|
|
996
|
-
};
|
|
997
|
-
daemon
|
|
998
|
-
.command('start')
|
|
999
|
-
.description('Start the background service')
|
|
1000
|
-
.option('--json', 'Print machine-readable status JSON')
|
|
1001
|
-
.option('--api <url>', 'Server URL override')
|
|
1002
|
-
.action((opts) => {
|
|
1003
|
-
warnDeprecated('start', opts.json);
|
|
1004
|
-
daemonStart(opts);
|
|
1005
|
-
});
|
|
1006
|
-
daemon
|
|
1007
|
-
.command('stop')
|
|
1008
|
-
.description('Stop the background service')
|
|
1009
|
-
.option('--json', 'Print machine-readable status JSON')
|
|
1010
|
-
.action(async (opts) => {
|
|
1011
|
-
warnDeprecated('stop', opts.json);
|
|
1012
|
-
await daemonStop(opts);
|
|
1013
|
-
});
|
|
1014
|
-
daemon
|
|
1015
|
-
.command('restart')
|
|
1016
|
-
.description('Restart the background service')
|
|
1017
|
-
.option('--json', 'Print machine-readable status JSON')
|
|
1018
|
-
.option('--api <url>', 'Server URL override')
|
|
1019
|
-
.action((opts) => {
|
|
1020
|
-
warnDeprecated('stop && shennian start', opts.json);
|
|
1021
|
-
daemonRestart(opts);
|
|
1022
|
-
});
|
|
1023
|
-
daemon
|
|
1024
|
-
.command('status')
|
|
1025
|
-
.description('Show background service status')
|
|
1026
|
-
.option('--json', 'Print machine-readable status JSON')
|
|
1027
|
-
.action((opts) => {
|
|
1028
|
-
warnDeprecated('status', opts.json);
|
|
1029
|
-
daemonStatus(opts);
|
|
1030
|
-
});
|
|
1031
|
-
daemon
|
|
1032
|
-
.command('logs')
|
|
1033
|
-
.description('Show recent logs')
|
|
1034
|
-
.option('-n, --lines <n>', 'Number of lines', '50')
|
|
1035
|
-
.action((opts) => {
|
|
1036
|
-
warnDeprecated('logs');
|
|
1037
|
-
daemonLogs({ lines: parseInt(opts.lines, 10) });
|
|
1038
|
-
});
|
|
1039
|
-
daemon.command('uninstall').description('Uninstall auto-start service').action(daemonUninstall);
|
|
1040
|
-
}
|
|
44
|
+
WantedBy=default.target`}const A="ShennianAgent";function He(){i.mkdirSync(y,{recursive:!0});const e={};for(const[n,t]of Object.entries(process.env))t!==void 0&&ge(n)&&(e[n]=t);i.writeFileSync(h("env.json"),JSON.stringify(e,null,2))}function Z(e={}){i.mkdirSync(y,{recursive:!0});const n=f({cleanupStale:!0});if(n.pid!==null&&n.running&&!n.adopted){e.quiet||console.log(s.green(`\u2713 Background service already running (PID ${n.pid})`));return}ee(n);const t=i.openSync(a,"a"),r=Oe(P()),o=oe(r.command,r.args,_e(r,t));o.unref(),i.closeSync(t),Ee(o.pid),e.quiet||(console.log(s.green(`\u2713 Background service started (PID ${o.pid})`)),console.log(s.gray(` Logs: ${a}`)))}function ee(e=f({cleanupStale:!0})){if(!(e.pid===null||!e.running||!e.adopted))try{process.kill(e.pid,"SIGTERM"),ke(e.pid);try{i.unlinkSync(p)}catch{}}catch{}}function ne(){i.mkdirSync(y,{recursive:!0});const e=l();switch(P().mode==="direct"&&x(W)&&console.warn(s.yellow("\u26A0 Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n Run `npm install -g shennian@latest` for reliable auto-start.")),e){case"darwin":{const t=u.dirname(m);i.mkdirSync(t,{recursive:!0}),i.writeFileSync(m,je());try{return c(`launchctl unload "${m}" 2>/dev/null; launchctl load -w "${m}"`,{stdio:"pipe",windowsHide:!0}),!0}catch{try{return c(`launchctl load -w "${m}"`,{stdio:"pipe",windowsHide:!0}),!0}catch{}}return!1}case"linux":{const t=u.dirname(E);i.mkdirSync(t,{recursive:!0}),i.writeFileSync(E,Ce());try{c("systemctl --user daemon-reload && systemctl --user enable shennian",{stdio:"pipe",windowsHide:!0})}catch{return!1}try{c(`loginctl enable-linger ${d.userInfo().username}`,{stdio:"pipe",windowsHide:!0})}catch{}try{return c("systemctl --user restart shennian",{stdio:"pipe",windowsHide:!0}),!0}catch{return!1}}case"win32":return Re()}}function Ue(){const n=f({cleanupStale:!0}).pid;if(n===null)return{};if(!w(n)){try{i.unlinkSync(p)}catch{}return{stalePid:n}}try{return process.kill(n,"SIGTERM"),{stoppedPid:n}}catch(t){return{error:t instanceof Error?t.message:String(t)}}}async function We(e=5e3){const n=Ue();if(!n.stoppedPid)return n;if(!await I(n.stoppedPid,e))try{process.kill(n.stoppedPid,"SIGKILL"),await I(n.stoppedPid,2e3)}catch(r){return{...n,error:r instanceof Error?r.message:String(r)}}try{i.unlinkSync(p)}catch{}$();for(const r of G())if(r!==n.stoppedPid)try{process.kill(r,"SIGTERM"),await I(r,2e3)}catch{}return n}function Fe(e={}){V(K(e.api)),Ae(),ee(),ne()||Z({quiet:e.json}),e.json?k(f()):console.log(s.green("\u2713 Background service started"))}async function Me(e={}){switch(i.mkdirSync(y,{recursive:!0}),i.writeFileSync(N,new Date().toISOString()),l()){case"darwin":{if(i.existsSync(m))try{c(`launchctl unload -w "${m}"`,{stdio:"pipe",windowsHide:!0})}catch{}break}case"linux":{try{c("systemctl --user disable --now shennian",{stdio:"pipe",windowsHide:!0})}catch{}break}case"win32":{try{c(`schtasks /change /tn "${A}" /disable`,{stdio:"pipe",windowsHide:!0})}catch{}try{c(`schtasks /end /tn "${A}"`,{stdio:"pipe",windowsHide:!0})}catch{}break}}const t=await We(),r=f({cleanupStale:!0});if(e.json){k(t.error?{...r,error:t.error}:r);return}t.error?console.error(s.red(`\u2717 Failed to stop: ${t.error}`)):console.log(s.green("\u2713 Background service stopped"))}function te(e){He(),Fe(e)}async function j(e={}){await Me(e)}async function I(e,n=5e3){const t=Date.now();for(;Date.now()-t<n;){if(!w(e))return!0;await new Promise(r=>setTimeout(r,100))}return!w(e)}async function Be(e={}){V(K(e.api));const n=ve();if(n!==null&&w(n))try{process.kill(n,"SIGTERM"),await I(n)||(process.kill(n,"SIGKILL"),await I(n,2e3));try{i.unlinkSync(p)}catch{}$(),e.json||console.log(s.green(`\u2713 Background service stopped (PID ${n})`))}catch(t){const r=t instanceof Error?t.message:String(t);e.json?k({...f(),error:r}):console.error(s.red(`\u2717 Failed to stop: ${r}`));return}else if(n!==null){try{i.unlinkSync(p)}catch{}$(),e.json||console.log(s.yellow(`\u26A0 Service process no longer exists, cleaned up stale PID ${n}`))}else e.json||console.log(s.gray("\u25CF Background service not running, starting it now"));if(l()==="linux"&&ne()){e.json&&k(f());return}Z({quiet:e.json}),e.json&&k(f())}function re(e={}){const n=f({cleanupStale:!0});if(e.json){k(n);return}if(n.pid===null){console.log(s.gray("\u25CF Background service not running"));return}n.running?(console.log(s.green(`\u25CF Background service running (PID ${n.pid})`)),console.log(s.gray(` Logs: ${a}`))):console.log(s.yellow(`\u25CF Background service stopped (PID ${n.pid} is stale)`))}function ie(e){if(!i.existsSync(a)){console.log(s.gray("No logs yet"));return}try{const n=c(d.platform()==="win32"?`powershell Get-Content -Tail ${e.lines} "${a}"`:`tail -n ${e.lines} "${a}"`,{encoding:"utf-8",windowsHide:!0});process.stdout.write(n)}catch{const n=i.readFileSync(a,"utf-8").split(`
|
|
45
|
+
`).slice(-e.lines);console.log(n.join(`
|
|
46
|
+
`))}}async function Je(){switch(await j(),l()){case"darwin":{if(i.existsSync(m)){try{c(`launchctl unload -w "${m}"`,{stdio:"pipe",windowsHide:!0})}catch{}i.unlinkSync(m),console.log(s.green("\u2713 Auto-start uninstalled (launchd)"))}break}case"linux":{try{c("systemctl --user disable --now shennian",{stdio:"pipe",windowsHide:!0})}catch{}if(i.existsSync(E)){i.unlinkSync(E);try{c("systemctl --user daemon-reload",{stdio:"pipe",windowsHide:!0})}catch{}}console.log(s.green("\u2713 Auto-start uninstalled (systemd)"));break}case"win32":{Y();try{i.unlinkSync(O)}catch{}try{i.unlinkSync(_)}catch{}try{i.unlinkSync(R)}catch{}z(),console.log(s.green("\u2713 Auto-start uninstalled (Task Scheduler)"));break}}}function tn(e){e.command("start").description("Start the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>te(r)),e.command("stop").description("Stop the background service").option("--json","Print machine-readable status JSON").action(r=>j(r)),e.command("status").description("Show background service status").option("--json","Print machine-readable status JSON").action(r=>re(r)),e.command("logs").description("Show recent logs").option("-n, --lines <n>","Number of lines","50").action(r=>ie({lines:parseInt(r.lines,10)}));const n=e.command("daemon",{hidden:!0}).description("Deprecated: use top-level start/stop/status/logs"),t=(r,o)=>{o||console.error(s.yellow(`\u26A0 Deprecated command. Use \`shennian ${r}\` instead.`))};n.command("start").description("Start the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>{t("start",r.json),te(r)}),n.command("stop").description("Stop the background service").option("--json","Print machine-readable status JSON").action(async r=>{t("stop",r.json),await j(r)}),n.command("restart").description("Restart the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>{t("stop && shennian start",r.json),Be(r)}),n.command("status").description("Show background service status").option("--json","Print machine-readable status JSON").action(r=>{t("status",r.json),re(r)}),n.command("logs").description("Show recent logs").option("-n, --lines <n>","Number of lines","50").action(r=>{t("logs"),ie({lines:parseInt(r.lines,10)})}),n.command("uninstall").description("Uninstall auto-start service").action(Je)}export{_e as buildDaemonSpawnOptions,sn as buildWindowsLauncherCommand,cn as buildWindowsScheduledTaskXml,an as buildWindowsStartupVbs,Q as captureEnvForService,$ as clearDaemonLauncher,nn as clearDaemonPidIfOwner,Ae as clearRemoteAccessDisabled,Ie as createDaemonInstanceId,G as findRunningDaemonProcessIds,B as getCurrentProcessDaemonLauncher,f as getDaemonStatus,ne as installService,x as isEphemeralCliPath,Pe as isRemoteAccessDisabled,ge as isSafeSnapshotEnvKey,Ne as isShennianRunServiceCommand,Ee as recordStartedDaemon,tn as registerDaemonCommand,ye as resolveServiceLaunchSpec,He as saveEnvSnapshot,Z as startDaemonProcess,J as writeDaemonLauncher,De as writeDaemonPid};
|