claude-code-discord-presence 1.0.0

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.
Files changed (51) hide show
  1. package/.env.example +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +164 -0
  4. package/assets/claude-code.png +0 -0
  5. package/assets/claude-liquid-dark.png +0 -0
  6. package/assets/claude-liquid-light.png +0 -0
  7. package/assets/claude-stats-dark.png +0 -0
  8. package/assets/claude-stats-light.png +0 -0
  9. package/assets/claude-usage-stats.png +0 -0
  10. package/assets/social-preview.jpg +0 -0
  11. package/assets/usage-stats.png +0 -0
  12. package/dist/appearance/theme-assets.js +13 -0
  13. package/dist/appearance/theme-watcher.js +188 -0
  14. package/dist/claude/app-liveness.js +82 -0
  15. package/dist/claude/cost.js +54 -0
  16. package/dist/claude/default-selection.js +268 -0
  17. package/dist/claude/desktop-focus.js +191 -0
  18. package/dist/claude/limits.js +90 -0
  19. package/dist/claude/monthly-usage.js +279 -0
  20. package/dist/claude/oauth-discovery.js +82 -0
  21. package/dist/claude/paths.js +13 -0
  22. package/dist/claude/plan-info.js +102 -0
  23. package/dist/claude/session-store.js +617 -0
  24. package/dist/claude/tool-labels.js +55 -0
  25. package/dist/claude/transcript.js +150 -0
  26. package/dist/claude/usage-poller.js +243 -0
  27. package/dist/cli.js +137 -0
  28. package/dist/config.js +96 -0
  29. package/dist/discord/presence-builder.js +295 -0
  30. package/dist/discord/rpc-client.js +224 -0
  31. package/dist/index.js +160 -0
  32. package/dist/remote/tunnel-manager.js +83 -0
  33. package/dist/server/http-server.js +89 -0
  34. package/dist/types.js +1 -0
  35. package/dist/util/autostart.js +73 -0
  36. package/dist/util/logger.js +74 -0
  37. package/dist/util/process-liveness.js +231 -0
  38. package/dist/util/process-scan-watcher.js +196 -0
  39. package/package.json +73 -0
  40. package/pricing.json +47 -0
  41. package/scripts/hook.mjs +32 -0
  42. package/scripts/install-autostart.mjs +50 -0
  43. package/scripts/install-remote.mjs +53 -0
  44. package/scripts/remove-autostart.mjs +35 -0
  45. package/scripts/remove-autostart.ps1 +7 -0
  46. package/scripts/run-hidden.vbs +7 -0
  47. package/scripts/run-service.ps1 +44 -0
  48. package/scripts/setup-autostart.ps1 +32 -0
  49. package/scripts/setup-claude.mjs +132 -0
  50. package/scripts/setup-remote.mjs +99 -0
  51. package/scripts/statusline.mjs +59 -0
@@ -0,0 +1,83 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createLogger } from "../util/logger.js";
3
+ const log = createLogger("remote-tunnel");
4
+ const BACKOFFS_MS = [5_000, 15_000, 60_000];
5
+ const SSH_ALIAS = /^[a-z0-9](?:[a-z0-9._-]{0,252}[a-z0-9])?$/i;
6
+ export function isSafeSshAlias(value) {
7
+ return SSH_ALIAS.test(value);
8
+ }
9
+ export function remoteForwardArguments(host, localPort, remotePort) {
10
+ if (!isSafeSshAlias(host))
11
+ throw new Error(`Unsafe SSH alias: ${JSON.stringify(host)}`);
12
+ return [
13
+ "-N",
14
+ "-o", "BatchMode=yes",
15
+ "-o", "ExitOnForwardFailure=yes",
16
+ "-o", "ConnectTimeout=10",
17
+ "-o", "ServerAliveInterval=30",
18
+ "-o", "ServerAliveCountMax=3",
19
+ "-R", `127.0.0.1:${remotePort}:127.0.0.1:${localPort}`,
20
+ host,
21
+ ];
22
+ }
23
+ export class RemoteTunnelManager {
24
+ hosts;
25
+ localPort;
26
+ remotePort;
27
+ stopped = false;
28
+ children = new Map();
29
+ timers = new Set();
30
+ constructor(hosts, localPort, remotePort) {
31
+ this.hosts = hosts;
32
+ this.localPort = localPort;
33
+ this.remotePort = remotePort;
34
+ }
35
+ start() {
36
+ this.stopped = false;
37
+ for (const host of this.hosts)
38
+ this.connect(host, 0);
39
+ }
40
+ stop() {
41
+ this.stopped = true;
42
+ for (const timer of this.timers)
43
+ clearTimeout(timer);
44
+ this.timers.clear();
45
+ for (const child of this.children.values())
46
+ child.kill();
47
+ this.children.clear();
48
+ }
49
+ connect(host, attempt) {
50
+ if (this.stopped)
51
+ return;
52
+ const child = spawn("ssh", remoteForwardArguments(host, this.localPort, this.remotePort), {
53
+ shell: false,
54
+ windowsHide: true,
55
+ stdio: ["ignore", "ignore", "pipe"],
56
+ });
57
+ this.children.set(host, child);
58
+ let stderr = "";
59
+ child.stderr.setEncoding("utf8");
60
+ child.stderr.on("data", (chunk) => {
61
+ stderr = (stderr + chunk).slice(-2_048);
62
+ });
63
+ let settled = false;
64
+ const reconnect = (reason) => {
65
+ if (settled)
66
+ return;
67
+ settled = true;
68
+ this.children.delete(host);
69
+ if (this.stopped)
70
+ return;
71
+ if (attempt === 0)
72
+ log.warn(`SSH tunnel to ${host} stopped (${reason}); reconnecting`);
73
+ const delay = BACKOFFS_MS[Math.min(attempt, BACKOFFS_MS.length - 1)];
74
+ const timer = setTimeout(() => {
75
+ this.timers.delete(timer);
76
+ this.connect(host, attempt + 1);
77
+ }, delay);
78
+ this.timers.add(timer);
79
+ };
80
+ child.once("error", (err) => reconnect(err.message));
81
+ child.once("exit", (code) => reconnect(stderr.trim() || `exit ${code ?? "unknown"}`));
82
+ }
83
+ }
@@ -0,0 +1,89 @@
1
+ import { createServer } from "node:http";
2
+ import { createLogger } from "../util/logger.js";
3
+ const log = createLogger("server");
4
+ const MAX_BODY_BYTES = 512 * 1024;
5
+ function respond(response, status, body = "") {
6
+ response.writeHead(status, {
7
+ "content-type": "text/plain; charset=utf-8",
8
+ "cache-control": "no-store",
9
+ "x-content-type-options": "nosniff",
10
+ });
11
+ response.end(body);
12
+ }
13
+ async function readJson(request) {
14
+ const contentLength = Number(request.headers["content-length"] ?? 0);
15
+ if (Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) {
16
+ request.resume();
17
+ throw new RangeError("request body is too large");
18
+ }
19
+ const chunks = [];
20
+ let length = 0;
21
+ for await (const chunk of request) {
22
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
23
+ length += buffer.length;
24
+ if (length > MAX_BODY_BYTES)
25
+ throw new RangeError("request body is too large");
26
+ chunks.push(buffer);
27
+ }
28
+ try {
29
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
30
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
31
+ ? parsed
32
+ : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ async function handle(request, response, handlers) {
39
+ const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
40
+ if (path === "/health") {
41
+ if (request.method !== "GET")
42
+ return respond(response, 405, "method not allowed");
43
+ return respond(response, 200, "ok");
44
+ }
45
+ if (path !== "/hook" && path !== "/statusline")
46
+ return respond(response, 404, "not found");
47
+ if (request.method !== "POST")
48
+ return respond(response, 405, "method not allowed");
49
+ try {
50
+ const payload = await readJson(request);
51
+ if (!payload)
52
+ return respond(response, 400, "invalid JSON object");
53
+ try {
54
+ if (path === "/hook")
55
+ handlers.onHook(payload);
56
+ else
57
+ handlers.onStatusline(payload);
58
+ }
59
+ catch (err) {
60
+ log.error(`${path.slice(1)} handler failed: ${err.message}`);
61
+ return respond(response, 500, "handler failed");
62
+ }
63
+ return respond(response, 204);
64
+ }
65
+ catch (err) {
66
+ if (err instanceof RangeError)
67
+ return respond(response, 413, "payload too large");
68
+ log.warn(`request failed: ${err.message}`);
69
+ return respond(response, 400, "bad request");
70
+ }
71
+ }
72
+ export function startServer(port, handlers) {
73
+ const server = createServer((request, response) => {
74
+ void handle(request, response, handlers).catch((err) => {
75
+ log.error(`request crashed: ${err.message}`);
76
+ if (!response.headersSent)
77
+ respond(response, 500, "internal error");
78
+ else
79
+ response.destroy();
80
+ });
81
+ });
82
+ server.requestTimeout = 5_000;
83
+ server.headersTimeout = 5_000;
84
+ server.maxHeadersCount = 32;
85
+ server.listen(port, "127.0.0.1");
86
+ server.on("listening", () => log.info(`listening on http://127.0.0.1:${port}`));
87
+ server.on("error", (err) => log.error(`server error: ${err.message}`));
88
+ return server;
89
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ function xml(value) {
2
+ return value
3
+ .replaceAll("&", "&")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&apos;");
8
+ }
9
+ function systemdQuote(value) {
10
+ return `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
11
+ }
12
+ export function createLaunchAgentPlist(definition) {
13
+ const args = [
14
+ definition.nodePath,
15
+ "--disable-warning=ExperimentalWarning",
16
+ ...(definition.envFile ? [`--env-file=${definition.envFile}`] : []),
17
+ "--enable-source-maps",
18
+ definition.entryPoint,
19
+ ];
20
+ const renderedArgs = args.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
21
+ return `<?xml version="1.0" encoding="UTF-8"?>
22
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
23
+ <plist version="1.0">
24
+ <dict>
25
+ <key>Label</key>
26
+ <string>${xml(definition.label)}</string>
27
+ <key>ProgramArguments</key>
28
+ <array>
29
+ ${renderedArgs}
30
+ </array>
31
+ <key>WorkingDirectory</key>
32
+ <string>${xml(definition.projectDir)}</string>
33
+ <key>RunAtLoad</key>
34
+ <true/>
35
+ <key>KeepAlive</key>
36
+ <true/>
37
+ <key>ThrottleInterval</key>
38
+ <integer>10</integer>
39
+ <key>StandardOutPath</key>
40
+ <string>/dev/null</string>
41
+ <key>StandardErrorPath</key>
42
+ <string>/dev/null</string>
43
+ </dict>
44
+ </plist>
45
+ `;
46
+ }
47
+ export function createSystemdUserUnit(definition) {
48
+ const command = [
49
+ definition.nodePath,
50
+ "--disable-warning=ExperimentalWarning",
51
+ ...(definition.envFile ? [`--env-file=${definition.envFile}`] : []),
52
+ "--enable-source-maps",
53
+ definition.entryPoint,
54
+ ]
55
+ .map(systemdQuote)
56
+ .join(" ");
57
+ return `[Unit]
58
+ Description=${definition.description}
59
+ After=graphical-session.target
60
+
61
+ [Service]
62
+ Type=simple
63
+ WorkingDirectory=${systemdQuote(definition.projectDir)}
64
+ ExecStart=${command}
65
+ Restart=on-failure
66
+ RestartSec=10
67
+ StandardOutput=null
68
+ StandardError=null
69
+
70
+ [Install]
71
+ WantedBy=default.target
72
+ `;
73
+ }
@@ -0,0 +1,74 @@
1
+ import { appendFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ const PRIORITY = {
4
+ debug: 10,
5
+ info: 20,
6
+ warn: 30,
7
+ error: 40,
8
+ silent: 50,
9
+ };
10
+ const DEFAULT_MAX_LOG_BYTES = 1024 * 1024;
11
+ export function resolveLogLevel(env = process.env) {
12
+ const configured = env.RPC_LOG_LEVEL?.trim().toLowerCase();
13
+ if (configured && configured in PRIORITY)
14
+ return configured;
15
+ return env.RPC_DEBUG === "1" || env.RPC_DEBUG?.toLowerCase() === "true" ? "debug" : "warn";
16
+ }
17
+ function resolveMaxLogBytes() {
18
+ const parsed = Number.parseInt(process.env.RPC_LOG_MAX_BYTES ?? "", 10);
19
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_LOG_BYTES;
20
+ }
21
+ const LOG_LEVEL = resolveLogLevel();
22
+ const MAX_LOG_BYTES = resolveMaxLogBytes();
23
+ let logFile;
24
+ export function rotateLogFile(path, maxBytes, incomingBytes = 0) {
25
+ try {
26
+ mkdirSync(dirname(path), { recursive: true });
27
+ if (!existsSync(path) || statSync(path).size + incomingBytes <= maxBytes)
28
+ return;
29
+ const archive = `${path}.1`;
30
+ rmSync(archive, { force: true });
31
+ renameSync(path, archive);
32
+ }
33
+ catch {
34
+ try {
35
+ writeFileSync(path, "");
36
+ }
37
+ catch {
38
+ /* ignore */
39
+ }
40
+ }
41
+ }
42
+ export function configureLogger(path) {
43
+ logFile = path;
44
+ rotateLogFile(path, MAX_LOG_BYTES);
45
+ }
46
+ export function getLogFile() {
47
+ return logFile;
48
+ }
49
+ function emit(level, scope, message, extra) {
50
+ if (PRIORITY[level] < PRIORITY[LOG_LEVEL])
51
+ return;
52
+ const line = `${new Date().toISOString()} [${level}] [${scope}] ${message}`;
53
+ const rendered = extra === undefined ? line : `${line} ${String(extra)}`;
54
+ const stream = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
55
+ stream(rendered);
56
+ if (!logFile)
57
+ return;
58
+ try {
59
+ const output = `${rendered}\n`;
60
+ rotateLogFile(logFile, MAX_LOG_BYTES, Buffer.byteLength(output));
61
+ appendFileSync(logFile, output);
62
+ }
63
+ catch {
64
+ /* ignore */
65
+ }
66
+ }
67
+ export function createLogger(scope) {
68
+ return {
69
+ debug: (message, extra) => emit("debug", scope, message, extra),
70
+ info: (message, extra) => emit("info", scope, message, extra),
71
+ warn: (message, extra) => emit("warn", scope, message, extra),
72
+ error: (message, extra) => emit("error", scope, message, extra),
73
+ };
74
+ }
@@ -0,0 +1,231 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createLogger } from "./logger.js";
3
+ const log = createLogger("proc-liveness");
4
+ const POLL_MS = 20_000;
5
+ const SCAN_TIMEOUT_MS = 10_000;
6
+ const DEAD_CONFIRMATIONS = 2;
7
+ export const CLAUDE_WINDOWS_PROCESS_RULES = [
8
+ { name: /^claude$/i, path: /\\WindowsApps\\Claude_/i, hasMainWindow: true },
9
+ { name: /^claude$/i, path: /^(?!.*\\WindowsApps\\Claude_).+$/i },
10
+ ];
11
+ export const CODEX_WINDOWS_PROCESS_RULES = [
12
+ { name: /^ChatGPT$/i, path: /\\WindowsApps\\OpenAI\.Codex_/i, hasMainWindow: true },
13
+ { name: /^codex$/i, commandLine: /^(?!.*\bapp-server\b).+$/i },
14
+ ];
15
+ function regexMatches(pattern, value) {
16
+ pattern.lastIndex = 0;
17
+ return pattern.test(value);
18
+ }
19
+ export function parseElapsedTimeSeconds(value) {
20
+ const trimmed = value.trim();
21
+ if (/^\d+$/.test(trimmed))
22
+ return Number(trimmed);
23
+ const dayParts = trimmed.split("-");
24
+ if (dayParts.length > 2)
25
+ return undefined;
26
+ const days = dayParts.length === 2 ? Number(dayParts[0]) : 0;
27
+ const clock = dayParts.at(-1)?.split(":") ?? [];
28
+ if (clock.length < 2 || clock.length > 3 || clock.some((part) => !/^\d+$/.test(part))) {
29
+ return undefined;
30
+ }
31
+ const numbers = clock.map(Number);
32
+ const seconds = numbers.at(-1);
33
+ const minutes = numbers.at(-2);
34
+ const hours = numbers.length === 3 ? numbers[0] : 0;
35
+ if (seconds >= 60 || minutes >= 60 || (dayParts.length === 2 && hours >= 24))
36
+ return undefined;
37
+ return days * 86_400 + hours * 3_600 + minutes * 60 + seconds;
38
+ }
39
+ export function processCommandName(command) {
40
+ return command.trim().split(/[\\/]/).at(-1)?.replace(/\.exe$/i, "") ?? "";
41
+ }
42
+ export function parsePosixProcessList(raw, pattern, now = Date.now()) {
43
+ let alive = false;
44
+ let earliestStartedAt;
45
+ let pid;
46
+ for (const line of raw.split("\n")) {
47
+ const match = line.trim().match(/^(\d+)\s+(\S+)\s+(.+)$/);
48
+ if (!match || !regexMatches(pattern, processCommandName(match[3])))
49
+ continue;
50
+ const elapsedSeconds = parseElapsedTimeSeconds(match[2]);
51
+ if (elapsedSeconds === undefined)
52
+ continue;
53
+ alive = true;
54
+ const startedAt = now - elapsedSeconds * 1000;
55
+ if (earliestStartedAt === undefined || startedAt < earliestStartedAt) {
56
+ earliestStartedAt = startedAt;
57
+ pid = Number(match[1]);
58
+ }
59
+ }
60
+ return { alive, earliestStartedAt, pid };
61
+ }
62
+ export function matchesWindowsProcess(info, rule) {
63
+ if (!regexMatches(rule.name, info.name))
64
+ return false;
65
+ if (rule.path && !regexMatches(rule.path, info.path))
66
+ return false;
67
+ if (rule.commandLine && !regexMatches(rule.commandLine, info.commandLine))
68
+ return false;
69
+ if (rule.hasMainWindow !== undefined && rule.hasMainWindow !== info.hasMainWindow)
70
+ return false;
71
+ return true;
72
+ }
73
+ function parseWindowsProcesses(raw) {
74
+ try {
75
+ const parsed = JSON.parse(raw);
76
+ if (!Array.isArray(parsed))
77
+ return undefined;
78
+ const result = [];
79
+ for (const value of parsed) {
80
+ if (!value || typeof value !== "object")
81
+ return undefined;
82
+ const item = value;
83
+ if (typeof item.name !== "string" ||
84
+ typeof item.pid !== "number" ||
85
+ !Number.isInteger(item.pid) ||
86
+ item.pid <= 0 ||
87
+ typeof item.path !== "string" ||
88
+ typeof item.commandLine !== "string" ||
89
+ typeof item.hasMainWindow !== "boolean" ||
90
+ typeof item.startedAt !== "number" ||
91
+ !Number.isFinite(item.startedAt)) {
92
+ return undefined;
93
+ }
94
+ result.push({
95
+ pid: item.pid,
96
+ name: item.name,
97
+ path: item.path,
98
+ commandLine: item.commandLine,
99
+ hasMainWindow: item.hasMainWindow,
100
+ startedAt: item.startedAt,
101
+ });
102
+ }
103
+ return result;
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ export class ProcessLiveness {
110
+ pattern;
111
+ onUpdate;
112
+ options;
113
+ timer;
114
+ scanning = false;
115
+ stopped = false;
116
+ lastAlive;
117
+ deadStreak = 0;
118
+ constructor(pattern, onUpdate, options = {}) {
119
+ this.pattern = pattern;
120
+ this.onUpdate = onUpdate;
121
+ this.options = options;
122
+ }
123
+ start() {
124
+ void this.scan();
125
+ this.timer = setInterval(() => void this.scan(), this.options.pollIntervalMs ?? POLL_MS);
126
+ }
127
+ stop() {
128
+ this.stopped = true;
129
+ if (this.timer)
130
+ clearInterval(this.timer);
131
+ }
132
+ async run(cmd) {
133
+ const [executable, ...args] = cmd;
134
+ if (!executable)
135
+ return undefined;
136
+ try {
137
+ return await new Promise((resolve) => {
138
+ execFile(executable, args, {
139
+ encoding: "utf8",
140
+ maxBuffer: 4 * 1024 * 1024,
141
+ windowsHide: true,
142
+ timeout: SCAN_TIMEOUT_MS,
143
+ killSignal: "SIGKILL",
144
+ }, (error, stdout) => resolve(error ? undefined : stdout));
145
+ });
146
+ }
147
+ catch (err) {
148
+ log.debug(`process list failed: ${err.message}`);
149
+ return undefined;
150
+ }
151
+ }
152
+ async matches() {
153
+ if (process.platform === "win32") {
154
+ const rules = this.options.windowsRules ?? [{ name: this.pattern }];
155
+ const namePredicate = rules
156
+ .map((rule) => `$processName -match '${rule.name.source.replace(/'/g, "''")}'`)
157
+ .join(" -or ");
158
+ const text = await this.run([
159
+ "powershell",
160
+ "-NoProfile",
161
+ "-NonInteractive",
162
+ "-Command",
163
+ `$items = @(Get-CimInstance Win32_Process | ForEach-Object { $processName = [System.IO.Path]::GetFileNameWithoutExtension([string]$_.Name); if (${namePredicate}) { $hasMainWindow = $false; try { $hasMainWindow = (Get-Process -Id $_.ProcessId -ErrorAction Stop).MainWindowHandle -ne 0 } catch {}; $startedAt = 0; try { $startedAt = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() } catch {}; [PSCustomObject]@{ pid = [int]$_.ProcessId; name = $processName; path = [string]$_.ExecutablePath; commandLine = [string]$_.CommandLine; hasMainWindow = $hasMainWindow; startedAt = $startedAt } } }); ConvertTo-Json -InputObject @($items) -Compress`,
164
+ ]);
165
+ if (text === undefined)
166
+ return undefined;
167
+ const processes = parseWindowsProcesses(text);
168
+ if (!processes) {
169
+ log.debug("process list returned invalid JSON");
170
+ return undefined;
171
+ }
172
+ let alive = false;
173
+ let earliest;
174
+ let pid;
175
+ for (const info of processes) {
176
+ if (!rules.some((rule) => matchesWindowsProcess(info, rule)))
177
+ continue;
178
+ alive = true;
179
+ const ms = info.startedAt;
180
+ if (Number.isFinite(ms) && ms > 0 && (earliest === undefined || ms < earliest)) {
181
+ earliest = ms;
182
+ pid = info.pid;
183
+ }
184
+ else if (pid === undefined) {
185
+ pid = info.pid;
186
+ }
187
+ }
188
+ return { alive, earliestStartedAt: earliest, pid };
189
+ }
190
+ const elapsedColumn = process.platform === "darwin" ? "etime=" : "etimes=";
191
+ const withStart = await this.run(["ps", "-A", "-o", `pid=,${elapsedColumn},comm=`]);
192
+ if (withStart !== undefined) {
193
+ return parsePosixProcessList(withStart, this.pattern);
194
+ }
195
+ const namesOnly = await this.run(["ps", "-A", "-o", "comm="]);
196
+ if (namesOnly === undefined)
197
+ return undefined;
198
+ const alive = namesOnly
199
+ .split("\n")
200
+ .some((line) => regexMatches(this.pattern, processCommandName(line)));
201
+ return { alive };
202
+ }
203
+ async scan() {
204
+ if (this.stopped || this.scanning)
205
+ return;
206
+ this.scanning = true;
207
+ try {
208
+ const result = await this.matches();
209
+ if (result === undefined)
210
+ return;
211
+ if (result.alive) {
212
+ this.deadStreak = 0;
213
+ }
214
+ else {
215
+ this.deadStreak++;
216
+ if (this.lastAlive === true && this.deadStreak < DEAD_CONFIRMATIONS)
217
+ return;
218
+ }
219
+ if (result.alive !== this.lastAlive) {
220
+ this.lastAlive = result.alive;
221
+ log.debug(`processes matching ${this.pattern} alive=${result.alive}` +
222
+ (result.earliestStartedAt !== undefined ? ` earliestStartedAt=${result.earliestStartedAt}` : "") +
223
+ (result.pid !== undefined ? ` pid=${result.pid}` : ""));
224
+ }
225
+ this.onUpdate(result.alive, result.earliestStartedAt, result.pid);
226
+ }
227
+ finally {
228
+ this.scanning = false;
229
+ }
230
+ }
231
+ }