@gricha/perry 0.0.1
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/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/router.js +1017 -0
- package/dist/agent/run.js +182 -0
- package/dist/agent/static.js +58 -0
- package/dist/agent/systemd.js +229 -0
- package/dist/agent/web/assets/index-9t2sFIJM.js +101 -0
- package/dist/agent/web/assets/index-CCFpTruF.css +1 -0
- package/dist/agent/web/index.html +14 -0
- package/dist/agent/web/vite.svg +1 -0
- package/dist/chat/handler.js +174 -0
- package/dist/chat/host-handler.js +170 -0
- package/dist/chat/host-opencode-handler.js +169 -0
- package/dist/chat/index.js +2 -0
- package/dist/chat/opencode-handler.js +177 -0
- package/dist/chat/opencode-websocket.js +95 -0
- package/dist/chat/websocket.js +100 -0
- package/dist/client/api.js +138 -0
- package/dist/client/config.js +34 -0
- package/dist/client/docker-proxy.js +103 -0
- package/dist/client/index.js +4 -0
- package/dist/client/proxy.js +96 -0
- package/dist/client/shell.js +71 -0
- package/dist/client/ws-shell.js +120 -0
- package/dist/config/loader.js +59 -0
- package/dist/docker/index.js +372 -0
- package/dist/docker/types.js +1 -0
- package/dist/index.js +475 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/metadata.js +55 -0
- package/dist/sessions/parser.js +553 -0
- package/dist/sessions/types.js +1 -0
- package/dist/shared/base-websocket.js +51 -0
- package/dist/shared/client-types.js +1 -0
- package/dist/shared/constants.js +11 -0
- package/dist/shared/types.js +5 -0
- package/dist/terminal/handler.js +86 -0
- package/dist/terminal/host-handler.js +76 -0
- package/dist/terminal/index.js +3 -0
- package/dist/terminal/types.js +8 -0
- package/dist/terminal/websocket.js +115 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/manager.js +475 -0
- package/dist/workspace/state.js +66 -0
- package/dist/workspace/types.js +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { RPCHandler } from '@orpc/server/node';
|
|
3
|
+
import { loadAgentConfig, getConfigDir, ensureConfigDir } from '../config/loader';
|
|
4
|
+
import { HOST_WORKSPACE_NAME } from '../shared/types';
|
|
5
|
+
import { DEFAULT_AGENT_PORT } from '../shared/constants';
|
|
6
|
+
import { WorkspaceManager } from '../workspace/manager';
|
|
7
|
+
import { containerRunning, getContainerName } from '../docker';
|
|
8
|
+
import { TerminalWebSocketServer } from '../terminal/websocket';
|
|
9
|
+
import { ChatWebSocketServer } from '../chat/websocket';
|
|
10
|
+
import { OpencodeWebSocketServer } from '../chat/opencode-websocket';
|
|
11
|
+
import { createRouter } from './router';
|
|
12
|
+
import { serveStatic } from './static';
|
|
13
|
+
import pkg from '../../package.json';
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
function sendJson(res, status, data) {
|
|
16
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
17
|
+
res.end(JSON.stringify(data));
|
|
18
|
+
}
|
|
19
|
+
function createAgentServer(configDir, config) {
|
|
20
|
+
let currentConfig = config;
|
|
21
|
+
const workspaces = new WorkspaceManager(configDir, currentConfig);
|
|
22
|
+
const isWorkspaceRunning = async (name) => {
|
|
23
|
+
if (name === HOST_WORKSPACE_NAME) {
|
|
24
|
+
return currentConfig.allowHostAccess === true;
|
|
25
|
+
}
|
|
26
|
+
return containerRunning(getContainerName(name));
|
|
27
|
+
};
|
|
28
|
+
const terminalServer = new TerminalWebSocketServer({
|
|
29
|
+
getContainerName,
|
|
30
|
+
isWorkspaceRunning,
|
|
31
|
+
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
32
|
+
});
|
|
33
|
+
const chatServer = new ChatWebSocketServer({
|
|
34
|
+
isWorkspaceRunning,
|
|
35
|
+
getConfig: () => currentConfig,
|
|
36
|
+
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
37
|
+
});
|
|
38
|
+
const opencodeServer = new OpencodeWebSocketServer({
|
|
39
|
+
isWorkspaceRunning,
|
|
40
|
+
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
41
|
+
});
|
|
42
|
+
const router = createRouter({
|
|
43
|
+
workspaces,
|
|
44
|
+
config: {
|
|
45
|
+
get: () => currentConfig,
|
|
46
|
+
set: (newConfig) => {
|
|
47
|
+
currentConfig = newConfig;
|
|
48
|
+
workspaces.updateConfig(newConfig);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
configDir,
|
|
52
|
+
stateDir: configDir,
|
|
53
|
+
startTime,
|
|
54
|
+
terminalServer,
|
|
55
|
+
});
|
|
56
|
+
const rpcHandler = new RPCHandler(router);
|
|
57
|
+
const server = createServer(async (req, res) => {
|
|
58
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
59
|
+
const method = req.method;
|
|
60
|
+
const pathname = url.pathname;
|
|
61
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
62
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
63
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
64
|
+
if (method === 'OPTIONS') {
|
|
65
|
+
res.writeHead(204);
|
|
66
|
+
res.end();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
if (pathname === '/health' && method === 'GET') {
|
|
71
|
+
sendJson(res, 200, { status: 'ok', version: pkg.version });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (pathname.startsWith('/rpc')) {
|
|
75
|
+
const { matched } = await rpcHandler.handle(req, res, {
|
|
76
|
+
prefix: '/rpc',
|
|
77
|
+
});
|
|
78
|
+
if (matched)
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const served = await serveStatic(req, res, pathname);
|
|
82
|
+
if (served)
|
|
83
|
+
return;
|
|
84
|
+
sendJson(res, 404, { error: 'Not found' });
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error('Request error:', err);
|
|
88
|
+
sendJson(res, 500, { error: 'Internal server error' });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
server.on('upgrade', async (request, socket, head) => {
|
|
92
|
+
const url = new URL(request.url || '/', 'http://localhost');
|
|
93
|
+
const terminalMatch = url.pathname.match(/^\/rpc\/terminal\/([^/]+)$/);
|
|
94
|
+
const chatMatch = url.pathname.match(/^\/rpc\/chat\/([^/]+)$/);
|
|
95
|
+
const opencodeMatch = url.pathname.match(/^\/rpc\/opencode\/([^/]+)$/);
|
|
96
|
+
if (terminalMatch) {
|
|
97
|
+
const workspaceName = decodeURIComponent(terminalMatch[1]);
|
|
98
|
+
await terminalServer.handleUpgrade(request, socket, head, workspaceName);
|
|
99
|
+
}
|
|
100
|
+
else if (chatMatch) {
|
|
101
|
+
const workspaceName = decodeURIComponent(chatMatch[1]);
|
|
102
|
+
await chatServer.handleUpgrade(request, socket, head, workspaceName);
|
|
103
|
+
}
|
|
104
|
+
else if (opencodeMatch) {
|
|
105
|
+
const workspaceName = decodeURIComponent(opencodeMatch[1]);
|
|
106
|
+
await opencodeServer.handleUpgrade(request, socket, head, workspaceName);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
110
|
+
socket.destroy();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return { server, terminalServer, chatServer, opencodeServer };
|
|
114
|
+
}
|
|
115
|
+
async function getProcessUsingPort(port) {
|
|
116
|
+
try {
|
|
117
|
+
const proc = Bun.spawn(['lsof', '-i', `:${port}`, '-t'], {
|
|
118
|
+
stdout: 'pipe',
|
|
119
|
+
stderr: 'pipe',
|
|
120
|
+
});
|
|
121
|
+
const output = await new Response(proc.stdout).text();
|
|
122
|
+
const pid = output.trim().split('\n')[0];
|
|
123
|
+
if (!pid)
|
|
124
|
+
return null;
|
|
125
|
+
const psProc = Bun.spawn(['ps', '-p', pid, '-o', 'pid=,comm=,args='], {
|
|
126
|
+
stdout: 'pipe',
|
|
127
|
+
stderr: 'pipe',
|
|
128
|
+
});
|
|
129
|
+
const psOutput = await new Response(psProc.stdout).text();
|
|
130
|
+
return psOutput.trim() || `PID ${pid}`;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function startAgent(options = {}) {
|
|
137
|
+
const configDir = options.configDir || getConfigDir();
|
|
138
|
+
await ensureConfigDir(configDir);
|
|
139
|
+
const config = await loadAgentConfig(configDir);
|
|
140
|
+
if (options.noHostAccess || process.env.PERRY_NO_HOST_ACCESS === 'true') {
|
|
141
|
+
config.allowHostAccess = false;
|
|
142
|
+
}
|
|
143
|
+
const port = options.port || parseInt(process.env.PERRY_PORT || '', 10) || config.port || DEFAULT_AGENT_PORT;
|
|
144
|
+
console.log(`[agent] Config directory: ${configDir}`);
|
|
145
|
+
console.log(`[agent] Starting on port ${port}...`);
|
|
146
|
+
const { server, terminalServer, chatServer, opencodeServer } = createAgentServer(configDir, config);
|
|
147
|
+
server.on('error', async (err) => {
|
|
148
|
+
if (err.code === 'EADDRINUSE') {
|
|
149
|
+
console.error(`[agent] Error: Port ${port} is already in use.`);
|
|
150
|
+
const processInfo = await getProcessUsingPort(port);
|
|
151
|
+
if (processInfo) {
|
|
152
|
+
console.error(`[agent] Process using port: ${processInfo}`);
|
|
153
|
+
}
|
|
154
|
+
console.error(`[agent] Try using a different port with: perry agent run --port <port>`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.error(`[agent] Server error: ${err.message}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
server.listen(port, '::', () => {
|
|
163
|
+
console.log(`[agent] Agent running at http://localhost:${port}`);
|
|
164
|
+
console.log(`[agent] oRPC endpoint: http://localhost:${port}/rpc`);
|
|
165
|
+
console.log(`[agent] WebSocket terminal: ws://localhost:${port}/rpc/terminal/:name`);
|
|
166
|
+
console.log(`[agent] WebSocket chat (Claude): ws://localhost:${port}/rpc/chat/:name`);
|
|
167
|
+
console.log(`[agent] WebSocket chat (OpenCode): ws://localhost:${port}/rpc/opencode/:name`);
|
|
168
|
+
});
|
|
169
|
+
const shutdown = () => {
|
|
170
|
+
console.log('[agent] Shutting down...');
|
|
171
|
+
chatServer.close();
|
|
172
|
+
opencodeServer.close();
|
|
173
|
+
terminalServer.close();
|
|
174
|
+
server.close(() => {
|
|
175
|
+
console.log('[agent] Server closed');
|
|
176
|
+
process.exit(0);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
process.on('SIGTERM', shutdown);
|
|
180
|
+
process.on('SIGINT', shutdown);
|
|
181
|
+
return new Promise(() => { });
|
|
182
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const MIME_TYPES = {
|
|
4
|
+
'.html': 'text/html',
|
|
5
|
+
'.js': 'application/javascript',
|
|
6
|
+
'.css': 'text/css',
|
|
7
|
+
'.json': 'application/json',
|
|
8
|
+
'.png': 'image/png',
|
|
9
|
+
'.jpg': 'image/jpeg',
|
|
10
|
+
'.jpeg': 'image/jpeg',
|
|
11
|
+
'.gif': 'image/gif',
|
|
12
|
+
'.svg': 'image/svg+xml',
|
|
13
|
+
'.ico': 'image/x-icon',
|
|
14
|
+
'.woff': 'font/woff',
|
|
15
|
+
'.woff2': 'font/woff2',
|
|
16
|
+
'.ttf': 'font/ttf',
|
|
17
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
18
|
+
};
|
|
19
|
+
function getWebDir() {
|
|
20
|
+
const distWeb = path.join(__dirname, 'web');
|
|
21
|
+
const rootDistWeb = path.resolve(__dirname, '../../dist/agent/web');
|
|
22
|
+
try {
|
|
23
|
+
require('fs').accessSync(path.join(distWeb, 'index.html'));
|
|
24
|
+
return distWeb;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return rootDistWeb;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function serveStatic(_req, res, pathname) {
|
|
31
|
+
const webDir = getWebDir();
|
|
32
|
+
const indexPath = path.join(webDir, 'index.html');
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(indexPath);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const ext = path.extname(pathname).toLowerCase();
|
|
40
|
+
const isAsset = ext && ext !== '.html';
|
|
41
|
+
if (isAsset) {
|
|
42
|
+
const filePath = path.join(webDir, pathname);
|
|
43
|
+
try {
|
|
44
|
+
const content = await fs.readFile(filePath);
|
|
45
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
46
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
47
|
+
res.end(content);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const content = await fs.readFile(indexPath);
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
56
|
+
res.end(content);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { DEFAULT_CONFIG_DIR } from '../shared/types';
|
|
6
|
+
import { DEFAULT_AGENT_PORT } from '../shared/constants';
|
|
7
|
+
const SERVICE_NAME = 'perry-agent';
|
|
8
|
+
const SERVICE_DESCRIPTION = 'Perry Agent Daemon';
|
|
9
|
+
function getSystemdUserDir() {
|
|
10
|
+
return path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
11
|
+
}
|
|
12
|
+
function getServicePath() {
|
|
13
|
+
return path.join(getSystemdUserDir(), `${SERVICE_NAME}.service`);
|
|
14
|
+
}
|
|
15
|
+
export function generateServiceFile(options = {}) {
|
|
16
|
+
const port = options.port || DEFAULT_AGENT_PORT;
|
|
17
|
+
const configDir = options.configDir || DEFAULT_CONFIG_DIR;
|
|
18
|
+
const nodePath = process.execPath;
|
|
19
|
+
const agentPath = path.resolve(__dirname, 'index.js');
|
|
20
|
+
const envLines = [
|
|
21
|
+
`Environment=PERRY_PORT=${port}`,
|
|
22
|
+
`Environment=PERRY_CONFIG_DIR=${configDir}`,
|
|
23
|
+
`Environment=NODE_ENV=production`,
|
|
24
|
+
];
|
|
25
|
+
if (options.noHostAccess) {
|
|
26
|
+
envLines.push(`Environment=PERRY_NO_HOST_ACCESS=true`);
|
|
27
|
+
}
|
|
28
|
+
return `[Unit]
|
|
29
|
+
Description=${SERVICE_DESCRIPTION}
|
|
30
|
+
After=network.target docker.service
|
|
31
|
+
Wants=docker.service
|
|
32
|
+
|
|
33
|
+
[Service]
|
|
34
|
+
Type=simple
|
|
35
|
+
ExecStart=${nodePath} ${agentPath}
|
|
36
|
+
Restart=on-failure
|
|
37
|
+
RestartSec=5
|
|
38
|
+
${envLines.join('\n')}
|
|
39
|
+
|
|
40
|
+
[Install]
|
|
41
|
+
WantedBy=default.target
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
async function runSystemctl(args) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const proc = spawn('systemctl', ['--user', ...args]);
|
|
47
|
+
let stdout = '';
|
|
48
|
+
let stderr = '';
|
|
49
|
+
proc.stdout.on('data', (data) => {
|
|
50
|
+
stdout += data;
|
|
51
|
+
});
|
|
52
|
+
proc.stderr.on('data', (data) => {
|
|
53
|
+
stderr += data;
|
|
54
|
+
});
|
|
55
|
+
proc.on('close', (code) => {
|
|
56
|
+
if (code === 0) {
|
|
57
|
+
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const err = new Error(`systemctl exited with code ${code}: ${stderr}`);
|
|
61
|
+
reject(err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
proc.on('error', reject);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export async function installService(options = {}) {
|
|
68
|
+
const serviceDir = getSystemdUserDir();
|
|
69
|
+
const servicePath = getServicePath();
|
|
70
|
+
const errors = [];
|
|
71
|
+
await fs.mkdir(serviceDir, { recursive: true });
|
|
72
|
+
const serviceContent = generateServiceFile(options);
|
|
73
|
+
await fs.writeFile(servicePath, serviceContent, 'utf-8');
|
|
74
|
+
console.log(`Service file written to: ${servicePath}`);
|
|
75
|
+
try {
|
|
76
|
+
await runSystemctl(['daemon-reload']);
|
|
77
|
+
console.log('Systemd daemon reloaded');
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
errors.push(`Failed to reload systemd daemon: ${msg}`);
|
|
82
|
+
console.error(`Error: Could not reload systemd daemon. ${msg}`);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await runSystemctl(['enable', SERVICE_NAME]);
|
|
86
|
+
console.log(`Service ${SERVICE_NAME} enabled`);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
errors.push(`Failed to enable service: ${msg}`);
|
|
91
|
+
console.error(`Error: Could not enable service. ${msg}`);
|
|
92
|
+
}
|
|
93
|
+
if (errors.length > 0) {
|
|
94
|
+
console.log('');
|
|
95
|
+
console.error('Installation failed with errors:');
|
|
96
|
+
for (const error of errors) {
|
|
97
|
+
console.error(` - ${error}`);
|
|
98
|
+
}
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log('Manual steps required:');
|
|
101
|
+
console.log(' systemctl --user daemon-reload');
|
|
102
|
+
console.log(` systemctl --user enable ${SERVICE_NAME}`);
|
|
103
|
+
console.log(` systemctl --user start ${SERVICE_NAME}`);
|
|
104
|
+
throw new Error('Installation failed: systemd configuration could not be completed');
|
|
105
|
+
}
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log('Installation complete! To start the agent:');
|
|
108
|
+
console.log(` systemctl --user start ${SERVICE_NAME}`);
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log('To check status:');
|
|
111
|
+
console.log(` systemctl --user status ${SERVICE_NAME}`);
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log('To view logs:');
|
|
114
|
+
console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
|
|
115
|
+
}
|
|
116
|
+
export async function uninstallService() {
|
|
117
|
+
const servicePath = getServicePath();
|
|
118
|
+
try {
|
|
119
|
+
await runSystemctl(['stop', SERVICE_NAME]);
|
|
120
|
+
console.log(`Service ${SERVICE_NAME} stopped`);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Service might not be running
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await runSystemctl(['disable', SERVICE_NAME]);
|
|
127
|
+
console.log(`Service ${SERVICE_NAME} disabled`);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Service might not be enabled
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await fs.unlink(servicePath);
|
|
134
|
+
console.log(`Service file removed: ${servicePath}`);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (err.code !== 'ENOENT') {
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await runSystemctl(['daemon-reload']);
|
|
143
|
+
console.log('Systemd daemon reloaded');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
console.warn('Warning: Could not reload systemd daemon');
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log('Uninstallation complete.');
|
|
150
|
+
}
|
|
151
|
+
export async function getServiceStatus() {
|
|
152
|
+
const servicePath = getServicePath();
|
|
153
|
+
let installed = false;
|
|
154
|
+
try {
|
|
155
|
+
await fs.access(servicePath);
|
|
156
|
+
installed = true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
installed = false;
|
|
160
|
+
}
|
|
161
|
+
let enabled = false;
|
|
162
|
+
let running = false;
|
|
163
|
+
if (installed) {
|
|
164
|
+
try {
|
|
165
|
+
await runSystemctl(['is-enabled', SERVICE_NAME]);
|
|
166
|
+
enabled = true;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
enabled = false;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
await runSystemctl(['is-active', SERVICE_NAME]);
|
|
173
|
+
running = true;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
running = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { installed, enabled, running };
|
|
180
|
+
}
|
|
181
|
+
export async function showStatus() {
|
|
182
|
+
const status = await getServiceStatus();
|
|
183
|
+
console.log(`Service: ${SERVICE_NAME}`);
|
|
184
|
+
console.log(` Installed: ${status.installed ? 'yes' : 'no'}`);
|
|
185
|
+
if (status.installed) {
|
|
186
|
+
console.log(` Enabled: ${status.enabled ? 'yes' : 'no'}`);
|
|
187
|
+
console.log(` Running: ${status.running ? 'yes' : 'no'}`);
|
|
188
|
+
if (status.running) {
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log('Service is running. View logs with:');
|
|
191
|
+
console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log('Service is not running. Start with:');
|
|
196
|
+
console.log(` systemctl --user start ${SERVICE_NAME}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log('Service not installed. Install with:');
|
|
202
|
+
console.log(' perry agent install');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export async function showLogs(options = {}) {
|
|
206
|
+
const status = await getServiceStatus();
|
|
207
|
+
if (!status.installed) {
|
|
208
|
+
console.error('Agent service is not installed.');
|
|
209
|
+
console.error('Install with: perry agent install');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const args = ['--user', '-u', SERVICE_NAME, '--no-pager'];
|
|
213
|
+
if (options.follow) {
|
|
214
|
+
args.push('-f');
|
|
215
|
+
}
|
|
216
|
+
if (options.lines) {
|
|
217
|
+
args.push('-n', String(options.lines));
|
|
218
|
+
}
|
|
219
|
+
const proc = spawn('journalctl', args, {
|
|
220
|
+
stdio: 'inherit',
|
|
221
|
+
});
|
|
222
|
+
proc.on('error', (err) => {
|
|
223
|
+
console.error(`Failed to run journalctl: ${err.message}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
|
226
|
+
proc.on('close', (code) => {
|
|
227
|
+
process.exit(code || 0);
|
|
228
|
+
});
|
|
229
|
+
}
|