@ai-devkit/agent-manager 0.26.1 → 0.26.3
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/AgentManager.d.ts +5 -2
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +21 -4
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +184 -0
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +13 -4
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/CodexAdapter.test.js +13 -4
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/CopilotAdapter.test.js +7 -2
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js +18 -5
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/GrokCliAdapter.test.js +7 -2
- package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +13 -4
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/PiAdapter.test.js +32 -4
- package/dist/__tests__/adapters/PiAdapter.test.js.map +1 -1
- package/dist/__tests__/utils/AgentRegistry.test.js +68 -46
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
- package/dist/__tests__/utils/process.test.js +90 -2
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +9 -3
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +5 -4
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +11 -7
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +5 -4
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +11 -7
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/CopilotAdapter.d.ts +5 -4
- package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
- package/dist/adapters/CopilotAdapter.js +11 -7
- package/dist/adapters/CopilotAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +6 -5
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +12 -8
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/adapters/GrokCliAdapter.d.ts +3 -2
- package/dist/adapters/GrokCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GrokCliAdapter.js +10 -6
- package/dist/adapters/GrokCliAdapter.js.map +1 -1
- package/dist/adapters/OpenCodeAdapter.d.ts +5 -4
- package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/OpenCodeAdapter.js +11 -7
- package/dist/adapters/OpenCodeAdapter.js.map +1 -1
- package/dist/adapters/PiAdapter.d.ts +3 -2
- package/dist/adapters/PiAdapter.d.ts.map +1 -1
- package/dist/adapters/PiAdapter.js +12 -9
- package/dist/adapters/PiAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/database/connection.d.ts +22 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +58 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/database/index.d.ts +4 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +4 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/migrations/001_initial.sql +12 -0
- package/dist/database/schema.d.ts +4 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/database/schema.js +47 -0
- package/dist/database/schema.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts +7 -3
- package/dist/utils/AgentRegistry.d.ts.map +1 -1
- package/dist/utils/AgentRegistry.js +108 -57
- package/dist/utils/AgentRegistry.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/process.d.ts +9 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +155 -10
- package/dist/utils/process.js.map +1 -1
- package/package.json +2 -2
- package/src/AgentManager.ts +28 -4
- package/src/__tests__/AgentManager.test.ts +126 -0
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +16 -5
- package/src/__tests__/adapters/CodexAdapter.test.ts +16 -5
- package/src/__tests__/adapters/CopilotAdapter.test.ts +8 -1
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +20 -4
- package/src/__tests__/adapters/GrokCliAdapter.test.ts +8 -1
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +16 -5
- package/src/__tests__/adapters/PiAdapter.test.ts +28 -5
- package/src/__tests__/utils/AgentRegistry.test.ts +48 -40
- package/src/__tests__/utils/process.test.ts +72 -1
- package/src/adapters/AgentAdapter.ts +11 -3
- package/src/adapters/ClaudeCodeAdapter.ts +10 -7
- package/src/adapters/CodexAdapter.ts +10 -7
- package/src/adapters/CopilotAdapter.ts +16 -7
- package/src/adapters/GeminiCliAdapter.ts +17 -8
- package/src/adapters/GrokCliAdapter.ts +9 -6
- package/src/adapters/OpenCodeAdapter.ts +10 -7
- package/src/adapters/PiAdapter.ts +10 -9
- package/src/adapters/index.ts +1 -1
- package/src/database/connection.ts +74 -0
- package/src/database/index.ts +7 -0
- package/src/database/migrations/001_initial.sql +12 -0
- package/src/database/schema.ts +62 -0
- package/src/index.ts +2 -0
- package/src/utils/AgentRegistry.ts +109 -49
- package/src/utils/index.ts +1 -1
- package/src/utils/process.ts +160 -10
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import os from 'os';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import type { AgentType } from '../adapters/AgentAdapter.js';
|
|
4
|
+
import {
|
|
5
|
+
DatabaseConnection,
|
|
6
|
+
resolveAgentRegistryDbPath,
|
|
7
|
+
} from '../database/index.js';
|
|
5
8
|
|
|
6
9
|
export class RenameNotFoundError extends Error {
|
|
7
10
|
constructor(public agentName: string) {
|
|
@@ -28,8 +31,16 @@ export interface RegistryEntry {
|
|
|
28
31
|
sessionFilePath: string;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
interface
|
|
32
|
-
|
|
34
|
+
interface RegistryRow {
|
|
35
|
+
name: string;
|
|
36
|
+
type: AgentType;
|
|
37
|
+
pid: number;
|
|
38
|
+
tmux_session: string;
|
|
39
|
+
cwd: string;
|
|
40
|
+
started_at: string;
|
|
41
|
+
session_id: string;
|
|
42
|
+
session_file_path: string;
|
|
43
|
+
updated_at: string;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
const DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');
|
|
@@ -37,10 +48,10 @@ const DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json
|
|
|
37
48
|
let defaultInstance: AgentRegistry | null = null;
|
|
38
49
|
|
|
39
50
|
export class AgentRegistry {
|
|
40
|
-
private
|
|
51
|
+
private db: DatabaseConnection;
|
|
41
52
|
|
|
42
53
|
constructor(filePath: string = DEFAULT_REGISTRY_PATH) {
|
|
43
|
-
this.
|
|
54
|
+
this.db = new DatabaseConnection({ dbPath: resolveAgentRegistryDbPath(filePath) });
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
static default(): AgentRegistry {
|
|
@@ -50,32 +61,79 @@ export class AgentRegistry {
|
|
|
50
61
|
return defaultInstance;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const dir = path.dirname(this.filePath);
|
|
65
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
-
const tmp = `${this.filePath}.tmp`;
|
|
67
|
-
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
68
|
-
fs.renameSync(tmp, this.filePath);
|
|
64
|
+
private rowToEntry(row: RegistryRow): RegistryEntry {
|
|
65
|
+
return {
|
|
66
|
+
name: row.name,
|
|
67
|
+
type: row.type,
|
|
68
|
+
pid: row.pid,
|
|
69
|
+
tmuxSession: row.tmux_session,
|
|
70
|
+
cwd: row.cwd,
|
|
71
|
+
startedAt: row.started_at,
|
|
72
|
+
sessionId: row.session_id,
|
|
73
|
+
sessionFilePath: row.session_file_path,
|
|
74
|
+
};
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
private mergeEntry(incoming: RegistryEntry, existing: RegistryEntry | undefined): RegistryEntry {
|
|
72
78
|
if (!existing) return incoming;
|
|
79
|
+
const incomingIsManaged = Boolean(incoming.tmuxSession);
|
|
73
80
|
return {
|
|
74
|
-
...
|
|
81
|
+
...existing,
|
|
82
|
+
name: incomingIsManaged ? incoming.name : existing.name,
|
|
75
83
|
tmuxSession: incoming.tmuxSession || existing.tmuxSession,
|
|
84
|
+
cwd: incoming.cwd || existing.cwd,
|
|
85
|
+
startedAt: existing.startedAt || incoming.startedAt,
|
|
86
|
+
sessionId: incoming.sessionId || existing.sessionId,
|
|
87
|
+
sessionFilePath: incoming.sessionFilePath || existing.sessionFilePath,
|
|
76
88
|
};
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
private findByIdentity(type: AgentType, pid: number): RegistryEntry | undefined {
|
|
92
|
+
const row = this.db.queryOne<RegistryRow>(
|
|
93
|
+
'SELECT * FROM agents WHERE type = ? AND pid = ?',
|
|
94
|
+
[type, pid],
|
|
95
|
+
);
|
|
96
|
+
return row ? this.rowToEntry(row) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private findByName(name: string): RegistryEntry | undefined {
|
|
100
|
+
const row = this.db.queryOne<RegistryRow>('SELECT * FROM agents WHERE name = ?', [name]);
|
|
101
|
+
return row ? this.rowToEntry(row) : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private deleteNameConflict(name: string, type: AgentType, pid: number): void {
|
|
105
|
+
const conflict = this.findByName(name);
|
|
106
|
+
if (!conflict) return;
|
|
107
|
+
if (conflict.type === type && conflict.pid === pid) return;
|
|
108
|
+
if (!this.isAlive(conflict)) {
|
|
109
|
+
this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [conflict.type, conflict.pid]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private insertOrUpdate(entry: RegistryEntry): void {
|
|
114
|
+
this.db.instance.prepare(`
|
|
115
|
+
INSERT INTO agents (
|
|
116
|
+
type, pid, name, tmux_session, cwd, started_at, session_id, session_file_path, updated_at
|
|
117
|
+
)
|
|
118
|
+
VALUES (
|
|
119
|
+
@type, @pid, @name, @tmuxSession, @cwd, @startedAt, @sessionId, @sessionFilePath, @updatedAt
|
|
120
|
+
)
|
|
121
|
+
ON CONFLICT(type, pid) DO UPDATE SET
|
|
122
|
+
name = excluded.name,
|
|
123
|
+
tmux_session = excluded.tmux_session,
|
|
124
|
+
cwd = excluded.cwd,
|
|
125
|
+
started_at = agents.started_at,
|
|
126
|
+
session_id = excluded.session_id,
|
|
127
|
+
session_file_path = excluded.session_file_path,
|
|
128
|
+
updated_at = excluded.updated_at
|
|
129
|
+
`).run({ ...entry, updatedAt: new Date().toISOString() });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private save(entry: RegistryEntry): void {
|
|
133
|
+
this.deleteNameConflict(entry.name, entry.type, entry.pid);
|
|
134
|
+
this.insertOrUpdate(entry);
|
|
135
|
+
}
|
|
136
|
+
|
|
79
137
|
isAlive(entry: RegistryEntry): boolean {
|
|
80
138
|
try {
|
|
81
139
|
process.kill(entry.pid, 0);
|
|
@@ -86,11 +144,13 @@ export class AgentRegistry {
|
|
|
86
144
|
}
|
|
87
145
|
|
|
88
146
|
prune(): void {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
147
|
+
const entries = this.list();
|
|
148
|
+
const stale = entries.filter((e) => !this.isAlive(e));
|
|
149
|
+
this.db.transaction(() => {
|
|
150
|
+
for (const entry of stale) {
|
|
151
|
+
this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [entry.type, entry.pid]);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
94
154
|
}
|
|
95
155
|
|
|
96
156
|
register(entry: RegistryEntry): void {
|
|
@@ -99,41 +159,41 @@ export class AgentRegistry {
|
|
|
99
159
|
|
|
100
160
|
registerBatch(entries: RegistryEntry[]): void {
|
|
101
161
|
if (entries.length === 0) return;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
data.entries[idx] = this.mergeEntry(incoming, data.entries[idx]);
|
|
107
|
-
} else {
|
|
108
|
-
data.entries.push(incoming);
|
|
162
|
+
this.db.transaction(() => {
|
|
163
|
+
for (const incoming of entries) {
|
|
164
|
+
const existing = this.findByIdentity(incoming.type, incoming.pid);
|
|
165
|
+
this.save(this.mergeEntry(incoming, existing));
|
|
109
166
|
}
|
|
110
|
-
}
|
|
111
|
-
this.writeFile(data);
|
|
167
|
+
});
|
|
112
168
|
}
|
|
113
169
|
|
|
114
170
|
rename(currentName: string, newName: string): void {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
if (idx < 0) {
|
|
171
|
+
const existing = this.findByName(currentName);
|
|
172
|
+
if (!existing) {
|
|
118
173
|
throw new RenameNotFoundError(currentName);
|
|
119
174
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
if (conflict) {
|
|
175
|
+
const conflict = this.findByName(newName);
|
|
176
|
+
if (conflict && this.isAlive(conflict)) {
|
|
123
177
|
throw new RenameConflictError(newName);
|
|
124
178
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
179
|
+
|
|
180
|
+
this.db.transaction(() => {
|
|
181
|
+
if (conflict) {
|
|
182
|
+
this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [conflict.type, conflict.pid]);
|
|
183
|
+
}
|
|
184
|
+
this.db.execute(
|
|
185
|
+
'UPDATE agents SET name = ?, updated_at = ? WHERE type = ? AND pid = ?',
|
|
186
|
+
[newName, new Date().toISOString(), existing.type, existing.pid],
|
|
187
|
+
);
|
|
188
|
+
});
|
|
129
189
|
}
|
|
130
190
|
|
|
131
191
|
lookup(name: string): RegistryEntry | null {
|
|
132
|
-
|
|
133
|
-
return data.entries.find((e) => e.name === name) ?? null;
|
|
192
|
+
return this.findByName(name) ?? null;
|
|
134
193
|
}
|
|
135
194
|
|
|
136
195
|
list(): RegistryEntry[] {
|
|
137
|
-
|
|
196
|
+
const rows = this.db.query<RegistryRow>('SELECT * FROM agents ORDER BY started_at ASC, name ASC');
|
|
197
|
+
return rows.map((row) => this.rowToEntry(row));
|
|
138
198
|
}
|
|
139
199
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { listAgentProcesses, batchGetProcessCwds, batchGetProcessStartTimes, enrichProcesses } from './process.js';
|
|
1
|
+
export { listAgentProcesses, batchGetProcessCwds, batchGetProcessStartTimes, enrichProcesses, captureProcessSnapshot } from './process.js';
|
|
2
2
|
export { getProcessTty } from './process.js';
|
|
3
3
|
export { batchGetSessionFileBirthtimes } from './session.js';
|
|
4
4
|
export type { SessionFile } from './session.js';
|
package/src/utils/process.ts
CHANGED
|
@@ -2,13 +2,42 @@
|
|
|
2
2
|
* Process Detection Utilities
|
|
3
3
|
*
|
|
4
4
|
* Shared shell command wrappers for detecting and inspecting running processes.
|
|
5
|
-
*
|
|
5
|
+
* Built-in refresh discovery uses captureProcessSnapshot(); synchronous helpers
|
|
6
|
+
* remain exported for compatibility with existing consumers.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import * as path from 'path';
|
|
9
|
-
import { execFileSync } from 'child_process';
|
|
10
|
+
import { execFile, execFileSync } from 'child_process';
|
|
10
11
|
import type { ProcessInfo } from '../adapters/AgentAdapter.js';
|
|
11
12
|
|
|
13
|
+
const PROCESS_EXEC_MAX_BUFFER = 10 * 1024 * 1024;
|
|
14
|
+
const VALID_EXECUTABLE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
15
|
+
|
|
16
|
+
export function executableBasename(command: string): string {
|
|
17
|
+
const executable = command.trim().split(/\s+/)[0] || '';
|
|
18
|
+
return path.basename(executable.replace(/\\/g, '/')).toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeExecutableName(name: string): string {
|
|
22
|
+
const lower = name.toLowerCase();
|
|
23
|
+
return lower.endsWith('.exe') ? lower.slice(0, -4) : lower;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizedProcessNames(namePatterns: readonly string[]): Set<string> {
|
|
27
|
+
return new Set(namePatterns.filter(Boolean).map(normalizeExecutableName));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function filterByProcessNames(
|
|
31
|
+
processes: readonly ProcessInfo[],
|
|
32
|
+
namePatterns: readonly string[],
|
|
33
|
+
): ProcessInfo[] {
|
|
34
|
+
const names = normalizedProcessNames(namePatterns);
|
|
35
|
+
if (names.size === 0) return [];
|
|
36
|
+
return processes.filter((process) => (
|
|
37
|
+
names.has(normalizeExecutableName(executableBasename(process.command)))
|
|
38
|
+
));
|
|
39
|
+
}
|
|
40
|
+
|
|
12
41
|
/**
|
|
13
42
|
* List running processes matching an agent executable name.
|
|
14
43
|
*
|
|
@@ -20,14 +49,14 @@ import type { ProcessInfo } from '../adapters/AgentAdapter.js';
|
|
|
20
49
|
*/
|
|
21
50
|
export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
22
51
|
// Validate pattern contains only safe characters (alphanumeric, dash, underscore)
|
|
23
|
-
if (!namePattern ||
|
|
52
|
+
if (!namePattern || !VALID_EXECUTABLE_NAME.test(namePattern)) {
|
|
24
53
|
return [];
|
|
25
54
|
}
|
|
26
55
|
|
|
27
56
|
try {
|
|
28
57
|
const output = execFileSync('ps', ['-axo', 'pid=,ppid=,tty=,command='], { encoding: 'utf-8' });
|
|
29
58
|
|
|
30
|
-
const
|
|
59
|
+
const names = normalizedProcessNames([namePattern]);
|
|
31
60
|
const processes: ProcessInfo[] = [];
|
|
32
61
|
|
|
33
62
|
for (const line of output.trim().split('\n')) {
|
|
@@ -43,12 +72,7 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
|
43
72
|
const tty = match[3];
|
|
44
73
|
const command = match[4];
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
const executable = command.trim().split(/\s+/)[0] || '';
|
|
48
|
-
const base = path.basename(executable).toLowerCase();
|
|
49
|
-
if (base !== lowerPattern && base !== `${lowerPattern}.exe`) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
75
|
+
if (!names.has(normalizeExecutableName(executableBasename(command)))) continue;
|
|
52
76
|
|
|
53
77
|
const ttyShort = tty.startsWith('/dev/') ? tty.slice(5) : tty;
|
|
54
78
|
|
|
@@ -67,6 +91,132 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
|
67
91
|
}
|
|
68
92
|
}
|
|
69
93
|
|
|
94
|
+
function execFileText(
|
|
95
|
+
file: string,
|
|
96
|
+
args: readonly string[],
|
|
97
|
+
): Promise<string> {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
execFile(file, args, { encoding: 'utf-8', maxBuffer: PROCESS_EXEC_MAX_BUFFER }, (error, stdout) => {
|
|
100
|
+
if (error) {
|
|
101
|
+
reject(error);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
resolve(stdout);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseProcessList(output: string, namePatterns: ReadonlySet<string>): ProcessInfo[] {
|
|
110
|
+
const processes: ProcessInfo[] = [];
|
|
111
|
+
|
|
112
|
+
for (const line of output.trim().split('\n')) {
|
|
113
|
+
if (!line.trim()) continue;
|
|
114
|
+
|
|
115
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
|
|
116
|
+
if (!match) continue;
|
|
117
|
+
|
|
118
|
+
const pid = parseInt(match[1], 10);
|
|
119
|
+
const ppid = parseInt(match[2], 10);
|
|
120
|
+
if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;
|
|
121
|
+
|
|
122
|
+
const tty = match[3];
|
|
123
|
+
const command = match[4];
|
|
124
|
+
const base = executableBasename(command);
|
|
125
|
+
const normalizedBase = normalizeExecutableName(base);
|
|
126
|
+
if (!namePatterns.has(normalizedBase)) continue;
|
|
127
|
+
|
|
128
|
+
processes.push({
|
|
129
|
+
pid,
|
|
130
|
+
ppid,
|
|
131
|
+
command,
|
|
132
|
+
cwd: '',
|
|
133
|
+
tty: tty.startsWith('/dev/') ? tty.slice(5) : tty,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return processes;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function batchGetProcessCwdsAsync(pids: number[]): Promise<Map<number, string>> {
|
|
141
|
+
const result = new Map<number, string>();
|
|
142
|
+
if (pids.length === 0) return result;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const output = await execFileText('lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.join(',')]);
|
|
146
|
+
let currentPid: number | null = null;
|
|
147
|
+
for (const line of output.trim().split('\n')) {
|
|
148
|
+
if (line.startsWith('p')) {
|
|
149
|
+
currentPid = parseInt(line.slice(1), 10);
|
|
150
|
+
} else if (line.startsWith('n') && currentPid !== null) {
|
|
151
|
+
result.set(currentPid, line.slice(1));
|
|
152
|
+
currentPid = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
} catch {
|
|
157
|
+
const entries = await Promise.all(pids.map(async (pid) => {
|
|
158
|
+
try {
|
|
159
|
+
const output = await execFileText('pwdx', [String(pid)]);
|
|
160
|
+
const match = output.match(/^\d+:\s*(.+)$/);
|
|
161
|
+
return match ? [pid, match[1].trim()] as const : null;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}));
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (entry) result.set(entry[0], entry[1]);
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function batchGetProcessStartTimesAsync(pids: number[]): Promise<Map<number, Date>> {
|
|
174
|
+
const result = new Map<number, Date>();
|
|
175
|
+
if (pids.length === 0) return result;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const output = await execFileText('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')]);
|
|
179
|
+
for (const rawLine of output.split('\n')) {
|
|
180
|
+
const match = rawLine.trim().match(/^(\d+)\s+(.+)$/);
|
|
181
|
+
if (!match) continue;
|
|
182
|
+
const pid = parseInt(match[1], 10);
|
|
183
|
+
const date = new Date(match[2].trim());
|
|
184
|
+
if (Number.isFinite(pid) && !Number.isNaN(date.getTime())) result.set(pid, date);
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// Return partial/empty data, matching the synchronous helper.
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Capture and enrich relevant processes without blocking the event loop.
|
|
194
|
+
* One base process listing is shared by every requested executable name.
|
|
195
|
+
*/
|
|
196
|
+
export async function captureProcessSnapshot(namePatterns: readonly string[]): Promise<ProcessInfo[]> {
|
|
197
|
+
const names = normalizedProcessNames(
|
|
198
|
+
namePatterns.filter((name) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name)),
|
|
199
|
+
);
|
|
200
|
+
if (names.size === 0) return [];
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const output = await execFileText('ps', ['-axo', 'pid=,ppid=,tty=,command=']);
|
|
204
|
+
const processes = parseProcessList(output, names);
|
|
205
|
+
const pids = processes.map((process) => process.pid);
|
|
206
|
+
const [cwdMap, startTimeMap] = await Promise.all([
|
|
207
|
+
batchGetProcessCwdsAsync(pids),
|
|
208
|
+
batchGetProcessStartTimesAsync(pids),
|
|
209
|
+
]);
|
|
210
|
+
return processes.map((process) => ({
|
|
211
|
+
...process,
|
|
212
|
+
cwd: cwdMap.get(process.pid) || '',
|
|
213
|
+
startTime: startTimeMap.get(process.pid),
|
|
214
|
+
}));
|
|
215
|
+
} catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
70
220
|
/**
|
|
71
221
|
* Batch-get current working directories for multiple PIDs.
|
|
72
222
|
*
|