z-clean 0.1.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.
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+
8
+ const PLIST_NAME = 'com.zclean.hourly';
9
+ const PLIST_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents');
10
+ const PLIST_PATH = path.join(PLIST_DIR, `${PLIST_NAME}.plist`);
11
+
12
+ /**
13
+ * Resolve the full path to the zclean binary.
14
+ * Tries: npx global, npm global, local install.
15
+ */
16
+ function resolveZcleanBin() {
17
+ // If installed globally via npm
18
+ try {
19
+ const npmBin = execSync('npm bin -g', { encoding: 'utf-8', timeout: 5000 }).trim();
20
+ const globalPath = path.join(npmBin, 'zclean');
21
+ if (fs.existsSync(globalPath)) return globalPath;
22
+ } catch { /* ignore */ }
23
+
24
+ // Check common locations
25
+ const candidates = [
26
+ path.join(os.homedir(), '.local', 'bin', 'zclean'),
27
+ '/usr/local/bin/zclean',
28
+ path.join(os.homedir(), 'node_modules', '.bin', 'zclean'),
29
+ ];
30
+
31
+ for (const candidate of candidates) {
32
+ if (fs.existsSync(candidate)) return candidate;
33
+ }
34
+
35
+ // Fallback to npx
36
+ return 'npx zclean';
37
+ }
38
+
39
+ /**
40
+ * Generate the launchd plist XML.
41
+ */
42
+ function generatePlist(binPath) {
43
+ const parts = binPath.split(' ');
44
+ const programArgs = parts
45
+ .concat(['--yes'])
46
+ .map((arg) => ` <string>${arg}</string>`)
47
+ .join('\n');
48
+
49
+ return `<?xml version="1.0" encoding="UTF-8"?>
50
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
51
+ <plist version="1.0">
52
+ <dict>
53
+ <key>Label</key>
54
+ <string>${PLIST_NAME}</string>
55
+
56
+ <key>ProgramArguments</key>
57
+ <array>
58
+ ${programArgs}
59
+ </array>
60
+
61
+ <key>StartInterval</key>
62
+ <integer>3600</integer>
63
+
64
+ <key>RunAtLoad</key>
65
+ <false/>
66
+
67
+ <key>StandardOutPath</key>
68
+ <string>${path.join(os.homedir(), '.zclean', 'launchd.log')}</string>
69
+
70
+ <key>StandardErrorPath</key>
71
+ <string>${path.join(os.homedir(), '.zclean', 'launchd.log')}</string>
72
+
73
+ <key>EnvironmentVariables</key>
74
+ <dict>
75
+ <key>PATH</key>
76
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${path.join(os.homedir(), '.local', 'bin')}</string>
77
+ </dict>
78
+ </dict>
79
+ </plist>
80
+ `;
81
+ }
82
+
83
+ /**
84
+ * Install the launchd agent.
85
+ *
86
+ * @returns {{ installed: boolean, message: string }}
87
+ */
88
+ function installLaunchd() {
89
+ if (os.platform() !== 'darwin') {
90
+ return { installed: false, message: 'launchd is macOS only.' };
91
+ }
92
+
93
+ // Ensure LaunchAgents directory exists
94
+ if (!fs.existsSync(PLIST_DIR)) {
95
+ fs.mkdirSync(PLIST_DIR, { recursive: true });
96
+ }
97
+
98
+ const binPath = resolveZcleanBin();
99
+ const plist = generatePlist(binPath);
100
+
101
+ // Unload existing if present
102
+ if (fs.existsSync(PLIST_PATH)) {
103
+ try {
104
+ execSync(`launchctl bootout gui/${process.getuid()} ${PLIST_PATH}`, {
105
+ encoding: 'utf-8',
106
+ timeout: 5000,
107
+ });
108
+ } catch {
109
+ // Might not be loaded
110
+ }
111
+ }
112
+
113
+ // Write plist
114
+ fs.writeFileSync(PLIST_PATH, plist, 'utf-8');
115
+
116
+ // Load
117
+ try {
118
+ execSync(`launchctl bootstrap gui/${process.getuid()} ${PLIST_PATH}`, {
119
+ encoding: 'utf-8',
120
+ timeout: 5000,
121
+ });
122
+ } catch (err) {
123
+ return {
124
+ installed: true,
125
+ message: `Plist written to ${PLIST_PATH} but launchctl load failed: ${err.message}. Try: launchctl bootstrap gui/$(id -u) ${PLIST_PATH}`,
126
+ };
127
+ }
128
+
129
+ return {
130
+ installed: true,
131
+ message: `Hourly launchd agent installed: ${PLIST_PATH}`,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Remove the launchd agent.
137
+ *
138
+ * @returns {{ removed: boolean, message: string }}
139
+ */
140
+ function removeLaunchd() {
141
+ if (os.platform() !== 'darwin') {
142
+ return { removed: false, message: 'launchd is macOS only.' };
143
+ }
144
+
145
+ if (!fs.existsSync(PLIST_PATH)) {
146
+ return { removed: false, message: 'Plist not found. Already uninstalled.' };
147
+ }
148
+
149
+ try {
150
+ execSync(`launchctl bootout gui/${process.getuid()} ${PLIST_PATH}`, {
151
+ encoding: 'utf-8',
152
+ timeout: 5000,
153
+ });
154
+ } catch {
155
+ // Might not be loaded
156
+ }
157
+
158
+ fs.unlinkSync(PLIST_PATH);
159
+
160
+ return {
161
+ removed: true,
162
+ message: `launchd agent removed: ${PLIST_PATH}`,
163
+ };
164
+ }
165
+
166
+ module.exports = { installLaunchd, removeLaunchd, PLIST_PATH };
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+
8
+ const SYSTEMD_USER_DIR = path.join(os.homedir(), '.config', 'systemd', 'user');
9
+ const SERVICE_NAME = 'zclean';
10
+ const SERVICE_PATH = path.join(SYSTEMD_USER_DIR, `${SERVICE_NAME}.service`);
11
+ const TIMER_PATH = path.join(SYSTEMD_USER_DIR, `${SERVICE_NAME}.timer`);
12
+
13
+ /**
14
+ * Resolve the full path to the zclean binary.
15
+ */
16
+ function resolveZcleanBin() {
17
+ try {
18
+ const npmBin = execSync('npm bin -g', { encoding: 'utf-8', timeout: 5000 }).trim();
19
+ const globalPath = path.join(npmBin, 'zclean');
20
+ if (fs.existsSync(globalPath)) return globalPath;
21
+ } catch { /* ignore */ }
22
+
23
+ const candidates = [
24
+ path.join(os.homedir(), '.local', 'bin', 'zclean'),
25
+ '/usr/local/bin/zclean',
26
+ path.join(os.homedir(), '.local', 'share', 'npm', 'bin', 'zclean'),
27
+ ];
28
+
29
+ for (const candidate of candidates) {
30
+ if (fs.existsSync(candidate)) return candidate;
31
+ }
32
+
33
+ return path.join(os.homedir(), '.local', 'bin', 'zclean');
34
+ }
35
+
36
+ /**
37
+ * Generate the systemd service unit file.
38
+ */
39
+ function generateService(binPath) {
40
+ return `[Unit]
41
+ Description=zclean - AI coding tool zombie process cleaner
42
+ Documentation=https://github.com/whynowlab/zclean
43
+
44
+ [Service]
45
+ Type=oneshot
46
+ ExecStart=${binPath} --yes
47
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin:%h/.local/bin
48
+ StandardOutput=append:%h/.zclean/systemd.log
49
+ StandardError=append:%h/.zclean/systemd.log
50
+ `;
51
+ }
52
+
53
+ /**
54
+ * Generate the systemd timer unit file.
55
+ */
56
+ function generateTimer() {
57
+ return `[Unit]
58
+ Description=Run zclean hourly
59
+ Documentation=https://github.com/whynowlab/zclean
60
+
61
+ [Timer]
62
+ OnCalendar=hourly
63
+ Persistent=true
64
+ RandomizedDelaySec=300
65
+
66
+ [Install]
67
+ WantedBy=timers.target
68
+ `;
69
+ }
70
+
71
+ /**
72
+ * Install the systemd user timer.
73
+ *
74
+ * @returns {{ installed: boolean, message: string }}
75
+ */
76
+ function installSystemd() {
77
+ if (os.platform() !== 'linux') {
78
+ return { installed: false, message: 'systemd is Linux only.' };
79
+ }
80
+
81
+ // Ensure systemd user directory exists
82
+ if (!fs.existsSync(SYSTEMD_USER_DIR)) {
83
+ fs.mkdirSync(SYSTEMD_USER_DIR, { recursive: true });
84
+ }
85
+
86
+ const binPath = resolveZcleanBin();
87
+
88
+ // Write service and timer files
89
+ fs.writeFileSync(SERVICE_PATH, generateService(binPath), 'utf-8');
90
+ fs.writeFileSync(TIMER_PATH, generateTimer(), 'utf-8');
91
+
92
+ // Reload and enable
93
+ const messages = [];
94
+ try {
95
+ execSync('systemctl --user daemon-reload', { encoding: 'utf-8', timeout: 5000 });
96
+ execSync(`systemctl --user enable --now ${SERVICE_NAME}.timer`, {
97
+ encoding: 'utf-8',
98
+ timeout: 5000,
99
+ });
100
+ messages.push(`Timer installed and enabled: ${TIMER_PATH}`);
101
+ } catch (err) {
102
+ messages.push(`Files written but systemctl failed: ${err.message}`);
103
+ messages.push(`Try manually: systemctl --user enable --now ${SERVICE_NAME}.timer`);
104
+ }
105
+
106
+ // Check linger
107
+ try {
108
+ const lingerDir = `/var/lib/systemd/linger/${os.userInfo().username}`;
109
+ if (!fs.existsSync(lingerDir)) {
110
+ messages.push(`Note: enable linger for timer to run without login: loginctl enable-linger ${os.userInfo().username}`);
111
+ }
112
+ } catch {
113
+ messages.push(`Note: run 'loginctl enable-linger' to ensure timer runs without login session.`);
114
+ }
115
+
116
+ return {
117
+ installed: true,
118
+ message: messages.join('\n'),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Remove the systemd user timer and service.
124
+ *
125
+ * @returns {{ removed: boolean, message: string }}
126
+ */
127
+ function removeSystemd() {
128
+ if (os.platform() !== 'linux') {
129
+ return { removed: false, message: 'systemd is Linux only.' };
130
+ }
131
+
132
+ // Disable timer
133
+ try {
134
+ execSync(`systemctl --user disable --now ${SERVICE_NAME}.timer`, {
135
+ encoding: 'utf-8',
136
+ timeout: 5000,
137
+ });
138
+ } catch {
139
+ // Might not be running
140
+ }
141
+
142
+ // Remove files
143
+ let removed = false;
144
+ for (const filepath of [SERVICE_PATH, TIMER_PATH]) {
145
+ if (fs.existsSync(filepath)) {
146
+ fs.unlinkSync(filepath);
147
+ removed = true;
148
+ }
149
+ }
150
+
151
+ // Reload daemon
152
+ try {
153
+ execSync('systemctl --user daemon-reload', { encoding: 'utf-8', timeout: 5000 });
154
+ } catch { /* ignore */ }
155
+
156
+ return {
157
+ removed,
158
+ message: removed ? 'systemd timer and service removed.' : 'Files not found. Already uninstalled.',
159
+ };
160
+ }
161
+
162
+ module.exports = { installSystemd, removeSystemd, SERVICE_PATH, TIMER_PATH };
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const fs = require('fs');
7
+
8
+ const TASK_NAME = 'zclean-hourly';
9
+
10
+ /**
11
+ * Resolve the full path to the zclean binary on Windows.
12
+ */
13
+ function resolveZcleanBin() {
14
+ // Check npm global
15
+ try {
16
+ const npmPrefix = execSync('npm prefix -g', { encoding: 'utf-8', timeout: 5000 }).trim();
17
+ const candidate = path.join(npmPrefix, 'zclean.cmd');
18
+ if (fs.existsSync(candidate)) return candidate;
19
+ const candidate2 = path.join(npmPrefix, 'node_modules', '.bin', 'zclean.cmd');
20
+ if (fs.existsSync(candidate2)) return candidate2;
21
+ } catch { /* ignore */ }
22
+
23
+ // Check AppData local
24
+ const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
25
+ const npmGlobal = path.join(appData, 'npm', 'zclean.cmd');
26
+ if (fs.existsSync(npmGlobal)) return npmGlobal;
27
+
28
+ return 'npx zclean';
29
+ }
30
+
31
+ /**
32
+ * Install a Windows Task Scheduler hourly task.
33
+ *
34
+ * Uses `schtasks /create` with user-scoped hourly task.
35
+ *
36
+ * @returns {{ installed: boolean, message: string }}
37
+ */
38
+ function installTaskScheduler() {
39
+ if (os.platform() !== 'win32') {
40
+ return { installed: false, message: 'Task Scheduler is Windows only.' };
41
+ }
42
+
43
+ const binPath = resolveZcleanBin();
44
+
45
+ // Build schtasks command
46
+ // /SC HOURLY — run every hour
47
+ // /TN — task name
48
+ // /TR — task to run
49
+ // /F — force overwrite if exists
50
+ const command = [
51
+ 'schtasks', '/create',
52
+ '/TN', `"${TASK_NAME}"`,
53
+ '/SC', 'HOURLY',
54
+ '/TR', `"${binPath} --yes"`,
55
+ '/F',
56
+ ].join(' ');
57
+
58
+ try {
59
+ execSync(command, { encoding: 'utf-8', timeout: 10000 });
60
+ return {
61
+ installed: true,
62
+ message: `Task Scheduler task created: ${TASK_NAME} (hourly)`,
63
+ };
64
+ } catch (err) {
65
+ return {
66
+ installed: false,
67
+ message: `Failed to create scheduled task: ${err.message}\nTry running as administrator.`,
68
+ };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Remove the Windows Task Scheduler task.
74
+ *
75
+ * @returns {{ removed: boolean, message: string }}
76
+ */
77
+ function removeTaskScheduler() {
78
+ if (os.platform() !== 'win32') {
79
+ return { removed: false, message: 'Task Scheduler is Windows only.' };
80
+ }
81
+
82
+ try {
83
+ execSync(`schtasks /delete /TN "${TASK_NAME}" /F`, {
84
+ encoding: 'utf-8',
85
+ timeout: 10000,
86
+ });
87
+ return {
88
+ removed: true,
89
+ message: `Task Scheduler task removed: ${TASK_NAME}`,
90
+ };
91
+ } catch (err) {
92
+ if (err.message.includes('does not exist') || err.message.includes('ERROR')) {
93
+ return { removed: false, message: 'Task not found. Already uninstalled.' };
94
+ }
95
+ return {
96
+ removed: false,
97
+ message: `Failed to remove task: ${err.message}`,
98
+ };
99
+ }
100
+ }
101
+
102
+ module.exports = { installTaskScheduler, removeTaskScheduler, TASK_NAME };
package/src/killer.js ADDED
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const os = require('os');
5
+ const { appendLog } = require('./config');
6
+
7
+ const platform = os.platform();
8
+
9
+ /**
10
+ * Safely kill a list of zombie processes.
11
+ *
12
+ * Before killing each process, re-verifies:
13
+ * 1. PID still exists
14
+ * 2. Process start time matches scan time
15
+ * 3. Command line matches scan
16
+ *
17
+ * Kill sequence:
18
+ * macOS/Linux: SIGTERM -> wait -> SIGKILL
19
+ * Windows: taskkill -> wait -> taskkill /F
20
+ *
21
+ * @param {Array} zombies — array from scanner.scan()
22
+ * @param {object} config — loaded config
23
+ * @returns {{ killed: Array, failed: Array, skipped: Array }}
24
+ */
25
+ function killZombies(zombies, config) {
26
+ const timeout = (config.sigterm_timeout || 10) * 1000;
27
+ const results = { killed: [], failed: [], skipped: [] };
28
+
29
+ for (const proc of zombies) {
30
+ // Re-verify before killing
31
+ const verification = verifyProcess(proc);
32
+ if (!verification.valid) {
33
+ results.skipped.push({
34
+ ...proc,
35
+ skipReason: verification.reason,
36
+ });
37
+ continue;
38
+ }
39
+
40
+ // Attempt kill
41
+ const killResult = killProcess(proc.pid, timeout);
42
+
43
+ if (killResult.success) {
44
+ results.killed.push(proc);
45
+ // Log for manual recovery
46
+ appendLog({
47
+ action: 'kill',
48
+ pid: proc.pid,
49
+ name: proc.name,
50
+ cmd: proc.cmd,
51
+ reason: proc.reason,
52
+ memFreed: proc.mem,
53
+ });
54
+ } else {
55
+ results.failed.push({
56
+ ...proc,
57
+ error: killResult.error,
58
+ });
59
+ appendLog({
60
+ action: 'kill-failed',
61
+ pid: proc.pid,
62
+ name: proc.name,
63
+ cmd: proc.cmd,
64
+ error: killResult.error,
65
+ });
66
+ }
67
+ }
68
+
69
+ // Log summary
70
+ appendLog({
71
+ action: 'cleanup-summary',
72
+ total: zombies.length,
73
+ killed: results.killed.length,
74
+ failed: results.failed.length,
75
+ skipped: results.skipped.length,
76
+ totalMemFreed: results.killed.reduce((sum, p) => sum + p.mem, 0),
77
+ });
78
+
79
+ return results;
80
+ }
81
+
82
+ /**
83
+ * Re-verify a process before killing it.
84
+ * Ensures we don't kill a recycled PID or wrong process.
85
+ */
86
+ function verifyProcess(proc) {
87
+ if (platform === 'win32') {
88
+ return verifyProcessWindows(proc);
89
+ }
90
+ return verifyProcessUnix(proc);
91
+ }
92
+
93
+ function verifyProcessUnix(proc) {
94
+ try {
95
+ // Check if PID still exists and get its command line
96
+ const cmd = execSync(`ps -o command= -p ${proc.pid}`, {
97
+ encoding: 'utf-8',
98
+ timeout: 5000,
99
+ }).trim();
100
+
101
+ if (!cmd) {
102
+ return { valid: false, reason: 'process-gone' };
103
+ }
104
+
105
+ // Verify command line matches (at least partially)
106
+ // Use first 50 chars to handle truncation
107
+ const scanPrefix = proc.cmd.substring(0, 50);
108
+ const currentPrefix = cmd.substring(0, 50);
109
+ if (scanPrefix !== currentPrefix) {
110
+ return { valid: false, reason: 'cmd-mismatch' };
111
+ }
112
+
113
+ // Verify start time if available
114
+ if (proc.startTime) {
115
+ try {
116
+ const lstart = execSync(`ps -o lstart= -p ${proc.pid}`, {
117
+ encoding: 'utf-8',
118
+ timeout: 5000,
119
+ }).trim();
120
+ const currentStart = new Date(lstart).toISOString();
121
+ if (currentStart !== proc.startTime) {
122
+ return { valid: false, reason: 'start-time-mismatch' };
123
+ }
124
+ } catch {
125
+ // Can't verify start time — proceed with caution
126
+ }
127
+ }
128
+
129
+ return { valid: true, reason: 'verified' };
130
+ } catch {
131
+ return { valid: false, reason: 'process-gone' };
132
+ }
133
+ }
134
+
135
+ function verifyProcessWindows(proc) {
136
+ try {
137
+ const output = execSync(
138
+ `wmic process where ProcessId=${proc.pid} get CommandLine /value`,
139
+ { encoding: 'utf-8', timeout: 5000 }
140
+ ).trim();
141
+
142
+ const match = output.match(/CommandLine=(.+)/);
143
+ if (!match) {
144
+ return { valid: false, reason: 'process-gone' };
145
+ }
146
+
147
+ const currentCmd = match[1].trim();
148
+ const scanPrefix = proc.cmd.substring(0, 50);
149
+ const currentPrefix = currentCmd.substring(0, 50);
150
+
151
+ if (scanPrefix !== currentPrefix) {
152
+ return { valid: false, reason: 'cmd-mismatch' };
153
+ }
154
+
155
+ return { valid: true, reason: 'verified' };
156
+ } catch {
157
+ return { valid: false, reason: 'process-gone' };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Kill a process with graceful shutdown sequence.
163
+ *
164
+ * macOS/Linux: SIGTERM → wait → SIGKILL
165
+ * Windows: taskkill → wait → taskkill /F
166
+ */
167
+ function killProcess(pid, timeoutMs) {
168
+ if (platform === 'win32') {
169
+ return killProcessWindows(pid, timeoutMs);
170
+ }
171
+ return killProcessUnix(pid, timeoutMs);
172
+ }
173
+
174
+ function killProcessUnix(pid, timeoutMs) {
175
+ try {
176
+ // Send SIGTERM
177
+ process.kill(pid, 'SIGTERM');
178
+ } catch (err) {
179
+ if (err.code === 'ESRCH') {
180
+ // Already dead
181
+ return { success: true, method: 'already-dead' };
182
+ }
183
+ return { success: false, error: `SIGTERM failed: ${err.message}` };
184
+ }
185
+
186
+ // Wait for graceful shutdown
187
+ const deadline = Date.now() + timeoutMs;
188
+ while (Date.now() < deadline) {
189
+ try {
190
+ // process.kill(pid, 0) throws if process doesn't exist
191
+ process.kill(pid, 0);
192
+ // Still alive — busy wait in small increments
193
+ const waitUntil = Date.now() + 500;
194
+ while (Date.now() < waitUntil) {
195
+ // Spin wait — we can't use setTimeout synchronously
196
+ }
197
+ } catch {
198
+ // Process is gone
199
+ return { success: true, method: 'sigterm' };
200
+ }
201
+ }
202
+
203
+ // Process survived SIGTERM — send SIGKILL
204
+ try {
205
+ process.kill(pid, 'SIGKILL');
206
+ return { success: true, method: 'sigkill' };
207
+ } catch (err) {
208
+ if (err.code === 'ESRCH') {
209
+ return { success: true, method: 'died-during-kill' };
210
+ }
211
+ return { success: false, error: `SIGKILL failed: ${err.message}` };
212
+ }
213
+ }
214
+
215
+ function killProcessWindows(pid, timeoutMs) {
216
+ try {
217
+ // Graceful kill
218
+ execSync(`taskkill /PID ${pid}`, { encoding: 'utf-8', timeout: 5000 });
219
+ } catch {
220
+ // Might fail — try force kill directly
221
+ }
222
+
223
+ // Wait
224
+ const deadline = Date.now() + timeoutMs;
225
+ while (Date.now() < deadline) {
226
+ try {
227
+ execSync(`wmic process where ProcessId=${pid} get ProcessId /value`, {
228
+ encoding: 'utf-8',
229
+ timeout: 3000,
230
+ });
231
+ // Still alive — wait
232
+ const waitUntil = Date.now() + 500;
233
+ while (Date.now() < waitUntil) { /* spin */ }
234
+ } catch {
235
+ return { success: true, method: 'taskkill' };
236
+ }
237
+ }
238
+
239
+ // Force kill
240
+ try {
241
+ execSync(`taskkill /F /PID ${pid}`, { encoding: 'utf-8', timeout: 5000 });
242
+ return { success: true, method: 'taskkill-force' };
243
+ } catch (err) {
244
+ return { success: false, error: `Force kill failed: ${err.message}` };
245
+ }
246
+ }
247
+
248
+ module.exports = { killZombies, verifyProcess, killProcess };