@lark-codem/codem-cli 0.1.234-dev-browser-use-review.100102657797.0.g7a31979 → 0.1.234
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/package.json +11 -8
- package/profile/config-lock.js +453 -0
- package/profile/credential-errors.js +27 -0
- package/profile/credential-metadata.js +29 -0
- package/profile/debug.js +104 -0
- package/profile/device-identity.js +95 -0
- package/profile/diagnostic-id.js +47 -0
- package/profile/index.d.ts +61 -0
- package/profile/index.js +17 -0
- package/profile/profile-io.js +70 -0
- package/profile/profile-migration.js +1001 -0
- package/profile/profile-snapshot.js +120 -0
- package/profile/profile-writers.js +296 -0
- package/profile/runtime-logs.js +374 -0
- package/profile/slardar-pc-transport.js +38 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { open, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { readCredentialMetadata } from "./credential-metadata.js";
|
|
5
|
+
import { CredentialStorageError } from "./credential-errors.js";
|
|
6
|
+
import { withCodeMConfigLock } from "./profile-io.js";
|
|
7
|
+
|
|
8
|
+
const string = value => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
9
|
+
const record = value => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
10
|
+
const first = (...values) => values.map(string).find(Boolean) ?? null;
|
|
11
|
+
|
|
12
|
+
export function resolveProfileUser(status, value) {
|
|
13
|
+
const config = record(value);
|
|
14
|
+
const oauth = record(record(config?.auth)?.oauth);
|
|
15
|
+
const info = record(oauth?.userInfo ?? oauth?.user_info);
|
|
16
|
+
const stored = record(config?.user);
|
|
17
|
+
return {
|
|
18
|
+
userId: first(status.userId, oauth?.userId, oauth?.user_id, stored?.id, oauth?.codemUserId, oauth?.codem_user_id),
|
|
19
|
+
tenantId: first(status.tenantId, oauth?.tenantId, oauth?.tenant_id),
|
|
20
|
+
displayName: first(status.displayName, oauth?.displayName, oauth?.display_name, info?.displayName, info?.display_name, info?.name, stored?.displayName, stored?.display_name),
|
|
21
|
+
avatarUrl: avatar(first(info?.avatarUrl, info?.avatar_url, stored?.avatarUrl, stored?.avatar_url))
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function avatar(value) {
|
|
26
|
+
if (!value) return null;
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(value);
|
|
29
|
+
const allowed = ["feishucdn.com", "larksuitecdn.com", "feishu-boe.cn"].some(root => url.hostname === root || url.hostname.endsWith(`.${root}`));
|
|
30
|
+
return url.protocol === "https:" && allowed && !url.username && !url.password ? url.href : null;
|
|
31
|
+
} catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function readProfileSnapshot(home) {
|
|
35
|
+
if (!isAbsolute(home)) throw new Error("CodeM profile home must be absolute");
|
|
36
|
+
try {
|
|
37
|
+
if (!(await stat(home)).isDirectory()) throw new Error("CodeM profile home is not a directory");
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== "ENOENT") throw error;
|
|
40
|
+
return { schemaVersion: 1, serverUrl: null,
|
|
41
|
+
user: { userId: null, tenantId: null, displayName: null, avatarUrl: null },
|
|
42
|
+
account: null, device: null, identityRevision: null };
|
|
43
|
+
}
|
|
44
|
+
return withCodeMConfigLock(join(home, "config.json"), () => readLockedProfileSnapshot(home));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readLockedProfileSnapshot(home) {
|
|
48
|
+
const configPath = join(home, "config.json");
|
|
49
|
+
const config = await readProfileObject(configPath);
|
|
50
|
+
let metadata;
|
|
51
|
+
try {
|
|
52
|
+
metadata = await readCredentialMetadata(join(home, "credential-storage.json"), config?.credentialStorageRef, "oauth");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!(error instanceof CredentialStorageError)) throw error;
|
|
55
|
+
const kind = ["oauth", "router-ak"].includes(error.kind) ? error.kind : null;
|
|
56
|
+
throw new CredentialStorageError(error.code.replace(/^credential_storage_/, ""), kind);
|
|
57
|
+
}
|
|
58
|
+
if (!config && metadata && !metadata.loggedOut) throw new CredentialStorageError("config_missing", "oauth");
|
|
59
|
+
const device = record(config?.device);
|
|
60
|
+
const last = record(config?.lastLoginUser ?? config?.last_login_user);
|
|
61
|
+
const user = resolveProfileUser({}, config);
|
|
62
|
+
const userId = user.userId ?? first(last?.userId, last?.user_id);
|
|
63
|
+
const tenantId = user.tenantId ?? first(last?.tenantId, last?.tenant_id);
|
|
64
|
+
const historicalMatches = (!first(last?.userId, last?.user_id) || userId === first(last?.userId, last?.user_id))
|
|
65
|
+
&& (!first(last?.tenantId, last?.tenant_id) || tenantId === first(last?.tenantId, last?.tenant_id));
|
|
66
|
+
if (!historicalMatches) throw new Error("CodeM profile identity is inconsistent");
|
|
67
|
+
let serverUrl = null;
|
|
68
|
+
if (config?.serverUrl) {
|
|
69
|
+
let url;
|
|
70
|
+
try { url = new URL(config.serverUrl); }
|
|
71
|
+
catch { throw new Error("CodeM profile server origin is invalid"); }
|
|
72
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
73
|
+
throw new Error("CodeM profile server origin is invalid");
|
|
74
|
+
}
|
|
75
|
+
serverUrl = url.origin;
|
|
76
|
+
}
|
|
77
|
+
const deviceId = first(device?.deviceId, device?.device_id);
|
|
78
|
+
if (deviceId && deviceId.length > 128) throw new Error("CodeM profile device identity is invalid");
|
|
79
|
+
const bound = Boolean(deviceId && first(device?.deviceSecret, device?.device_secret) && !metadata?.loggedOut);
|
|
80
|
+
const account = bound && userId && tenantId && serverUrl ? {
|
|
81
|
+
userId, tenantId,
|
|
82
|
+
displayName: user.displayName ?? first(last?.displayName, last?.display_name),
|
|
83
|
+
avatarUrl: user.avatarUrl
|
|
84
|
+
} : null;
|
|
85
|
+
// Full binding login replaces both fields; OAuth generation may instead be
|
|
86
|
+
// backfilled by scope-upgrade or a delayed mirror without any new login.
|
|
87
|
+
const generation = [string(config?.clientId), string(config?.createdAt)];
|
|
88
|
+
return {
|
|
89
|
+
schemaVersion: 1,
|
|
90
|
+
serverUrl,
|
|
91
|
+
user: metadata?.loggedOut
|
|
92
|
+
? { userId: null, tenantId: null, displayName: null, avatarUrl: null }
|
|
93
|
+
: { userId, tenantId, displayName: user.displayName ?? first(last?.displayName, last?.display_name), avatarUrl: user.avatarUrl },
|
|
94
|
+
account,
|
|
95
|
+
device: deviceId ? { id: deviceId, name: first(device?.deviceName, device?.device_name), binding: bound ? "ready" : "missing" } : null,
|
|
96
|
+
identityRevision: account ? createHash("sha256").update(JSON.stringify([serverUrl, userId, tenantId, deviceId, first(device?.deviceSecret, device?.device_secret), generation])).digest("hex") : null
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function readProfileObject(path) {
|
|
101
|
+
let file;
|
|
102
|
+
try { file = await open(path, "r"); }
|
|
103
|
+
catch (error) { if (error.code === "ENOENT") return null; throw new Error("Cannot read CodeM profile metadata", { cause: error }); }
|
|
104
|
+
try {
|
|
105
|
+
const info = await file.stat();
|
|
106
|
+
if (!info.isFile() || info.size > 1024 * 1024) throw new Error("Invalid profile metadata size");
|
|
107
|
+
const buffer = Buffer.alloc(info.size + 1);
|
|
108
|
+
let count = 0;
|
|
109
|
+
while (count < buffer.length) {
|
|
110
|
+
const part = await file.read(buffer, count, buffer.length - count, count);
|
|
111
|
+
if (!part.bytesRead) break;
|
|
112
|
+
count += part.bytesRead;
|
|
113
|
+
}
|
|
114
|
+
if (count > info.size) throw new Error("Profile metadata changed during read");
|
|
115
|
+
let parsed;
|
|
116
|
+
try { parsed = record(JSON.parse(buffer.toString("utf8", 0, count))); } catch { /* Do not surface credential bytes. */ }
|
|
117
|
+
if (!parsed) throw new Error("Invalid CodeM profile metadata");
|
|
118
|
+
return parsed;
|
|
119
|
+
} finally { await file.close(); }
|
|
120
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { lstat, readFile, readdir, readlink, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { isProcessExit, requireProcessSuccess, runProcess, runProcessResult } from './profile-io.js';
|
|
5
|
+
const isCodeMBoeTtEnv = (value)=>value === 'boe' || /^boe_[A-Za-z0-9_-]+$/u.test(value ?? '');
|
|
6
|
+
export const CODEM_PROFILE_SESSION_TREES = [
|
|
7
|
+
'sessions',
|
|
8
|
+
'sessions.legacy-account-merge-v1',
|
|
9
|
+
'sessions.account-profile-staging-v1'
|
|
10
|
+
];
|
|
11
|
+
export class CodeMProfileWriterBusyError extends Error {
|
|
12
|
+
code = 'CODEM_PROFILE_BUSY';
|
|
13
|
+
home;
|
|
14
|
+
blockers;
|
|
15
|
+
constructor(home, blockers){
|
|
16
|
+
super(`资料仍被命令行或旧 App 使用,请完成任务并关闭对应窗口后重试:${home}`);
|
|
17
|
+
this.name = 'CodeMProfileWriterBusyError';
|
|
18
|
+
this.home = home;
|
|
19
|
+
this.blockers = blockers;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class CodeMProfileWriterInspectionError extends Error {
|
|
23
|
+
code = 'CODEM_PROFILE_WRITER_INSPECTION_FAILED';
|
|
24
|
+
home;
|
|
25
|
+
constructor(home, cause){
|
|
26
|
+
const code = errorCode(cause);
|
|
27
|
+
super(`无法确认资料是否仍被命令行或旧 App 使用${code ? `(${code})` : ''},请结束任务并关闭相关进程后重试:${home}`, {
|
|
28
|
+
cause
|
|
29
|
+
});
|
|
30
|
+
this.name = 'CodeMProfileWriterInspectionError';
|
|
31
|
+
this.home = home;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function requireIdleProfileSessions(home) {
|
|
35
|
+
const roots = [];
|
|
36
|
+
for (const name of CODEM_PROFILE_SESSION_TREES){
|
|
37
|
+
const path = join(home, name);
|
|
38
|
+
try {
|
|
39
|
+
if ((await lstat(path)).isDirectory()) roots.push(path);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (!missing(error)) throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let blockers;
|
|
45
|
+
try {
|
|
46
|
+
if (process.platform === 'linux') {
|
|
47
|
+
try {
|
|
48
|
+
blockers = await linuxProfileWriters(home, roots);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'EACCES')) throw error;
|
|
51
|
+
await delay(25);
|
|
52
|
+
blockers = await linuxProfileWriters(home, roots);
|
|
53
|
+
}
|
|
54
|
+
} else if (process.platform === 'darwin') blockers = await macProfileWriters(home, roots);
|
|
55
|
+
else if (process.platform === 'win32') blockers = await windowsCodeMProcesses();
|
|
56
|
+
else throw new Error('Unsupported process inspection');
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new CodeMProfileWriterInspectionError(home, error);
|
|
59
|
+
}
|
|
60
|
+
if (blockers.length > 0) {
|
|
61
|
+
throw new CodeMProfileWriterBusyError(home, deduplicateBlockers(blockers));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function linuxProfileWriters(home, roots) {
|
|
65
|
+
if (!process.getuid) throw new Error('Process owner is unavailable');
|
|
66
|
+
const uid = process.getuid();
|
|
67
|
+
for (const pid of (await readdir('/proc'))){
|
|
68
|
+
if (!/^\d+$/u.test(pid)) continue;
|
|
69
|
+
const directory = join('/proc', pid);
|
|
70
|
+
let authenticationSupervisor = false;
|
|
71
|
+
let processName = '';
|
|
72
|
+
try {
|
|
73
|
+
if ((await lstat(directory)).uid !== uid) continue;
|
|
74
|
+
const name = (await readFile(join(directory, 'comm'), 'utf8')).trim();
|
|
75
|
+
processName = name;
|
|
76
|
+
authenticationSupervisor = name === 'sshd' || name === '(sd-pam)';
|
|
77
|
+
const command = (await readFile(join(directory, 'cmdline'), 'utf8')).split('\0');
|
|
78
|
+
if (command.some((argument)=>argument.startsWith('--type='))) continue;
|
|
79
|
+
const kind = codeMProcessKind(`${name} ${command.join(' ')}`);
|
|
80
|
+
if (Number(pid) !== process.pid && kind !== 'other') {
|
|
81
|
+
if (kind === 'app') {
|
|
82
|
+
return [
|
|
83
|
+
processBlocker(kind, Number(pid), name)
|
|
84
|
+
];
|
|
85
|
+
} else {
|
|
86
|
+
const environment = (await readFile(join(directory, 'environ'), 'utf8')).split('\0');
|
|
87
|
+
if (await profileMatchesEnvironment(home, environment)) {
|
|
88
|
+
return [
|
|
89
|
+
processBlocker(kind, Number(pid), name)
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const descriptor of (await readdir(join(directory, 'fd')))){
|
|
95
|
+
try {
|
|
96
|
+
const path = await readlink(join(directory, 'fd', descriptor));
|
|
97
|
+
if (roots.some((root)=>path === root || path.startsWith(`${root}${sep}`))) {
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
kind: 'session-handle',
|
|
101
|
+
processId: Number(pid),
|
|
102
|
+
processName: processName || null
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (!missing(error)) throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (authenticationSupervisor && error instanceof Error && 'code' in error && error.code === 'EACCES') continue;
|
|
112
|
+
if (error instanceof Error && 'code' in error && error.code === 'EACCES') {
|
|
113
|
+
const status = await readFile(join(directory, 'status'), 'utf8').catch((cause)=>{
|
|
114
|
+
if (missing(cause)) return null;
|
|
115
|
+
throw cause;
|
|
116
|
+
});
|
|
117
|
+
if (status === null || /^State:\s+[ZX]\b/mu.test(status)) continue;
|
|
118
|
+
}
|
|
119
|
+
if (!missing(error)) {
|
|
120
|
+
console.warn('Cannot inspect profile writer', {
|
|
121
|
+
processId: Number(pid),
|
|
122
|
+
processName,
|
|
123
|
+
code: error instanceof Error && 'code' in error ? error.code : null
|
|
124
|
+
});
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
async function macProfileWriters(home, roots) {
|
|
132
|
+
if (!process.getuid) throw new Error('Process owner is unavailable');
|
|
133
|
+
const { stdout: processes } = await runProcess({
|
|
134
|
+
executable: '/bin/ps',
|
|
135
|
+
arguments: [
|
|
136
|
+
'eww',
|
|
137
|
+
'-U',
|
|
138
|
+
String(process.getuid()),
|
|
139
|
+
'-o',
|
|
140
|
+
'pid=,comm=,command='
|
|
141
|
+
],
|
|
142
|
+
timeoutMilliseconds: 10_000,
|
|
143
|
+
maximumOutputBytes: 16 * 1024 * 1024
|
|
144
|
+
});
|
|
145
|
+
for (const line of processes.split('\n')){
|
|
146
|
+
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
147
|
+
if (!match || Number(match[1]) === process.pid || /(?:^|\s)--type=/u.test(match[2])) continue;
|
|
148
|
+
const kind = codeMProcessKind(match[2].split(/\s+[A-Za-z_][A-Za-z0-9_]*=/u, 1)[0]);
|
|
149
|
+
if (kind === 'app') {
|
|
150
|
+
return [
|
|
151
|
+
processBlocker(kind, Number(match[1]), 'CodeM')
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
if (kind === 'cli') {
|
|
155
|
+
const environment = [
|
|
156
|
+
...match[2].matchAll(/(?:^|\s)(CODEM_STATE_HOME|LINCO_HOME|CODEM_HOME|CODEM_X_TT_ENV|HOME)=([^\n]*?)(?=\s+[A-Za-z_][A-Za-z0-9_]*=|$)/gu)
|
|
157
|
+
].map((entry)=>`${entry[1]}=${entry[2]}`);
|
|
158
|
+
if (await profileMatchesEnvironment(home, environment)) {
|
|
159
|
+
return [
|
|
160
|
+
processBlocker(kind, Number(match[1]), 'codem-cli')
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const root of roots){
|
|
166
|
+
const outcome = await runProcessResult({
|
|
167
|
+
executable: '/usr/sbin/lsof',
|
|
168
|
+
arguments: [
|
|
169
|
+
'-t',
|
|
170
|
+
'+D',
|
|
171
|
+
root
|
|
172
|
+
],
|
|
173
|
+
timeoutMilliseconds: 10_000,
|
|
174
|
+
maximumOutputBytes: 1024 * 1024
|
|
175
|
+
});
|
|
176
|
+
if (isProcessExit(outcome, 0) && outcome.stdout.trim()) {
|
|
177
|
+
const pid = outcome.stdout.trim().split(/\s+/u)[0];
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
kind: 'session-handle',
|
|
181
|
+
processId: /^\d+$/u.test(pid) ? Number(pid) : null,
|
|
182
|
+
processName: null
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
if (!isProcessExit(outcome, 1) || outcome.stdout !== '' || outcome.stderr !== '') {
|
|
187
|
+
requireProcessSuccess(outcome);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
function codeMProcessKind(command) {
|
|
193
|
+
if (/codem-bridge\/.*src\/index\.js(?:\s|$)/u.test(command) || /node_modules\/(?:@lark-codem|@byted-meego)\/codem-cli\//u.test(command)) return 'cli';
|
|
194
|
+
if (/\/Contents\/MacOS\/(?:飞书 )?CodeM(?:\s|$)/u.test(command) || /^CodeM(?:\s|$)/u.test(command)) return 'app';
|
|
195
|
+
if (/(?:^|[\s/])(?:codem|codem-cli|codem-core|linco)(?:\s|$)/iu.test(command)) return 'cli';
|
|
196
|
+
return 'other';
|
|
197
|
+
}
|
|
198
|
+
async function profileMatchesEnvironment(home, environment) {
|
|
199
|
+
const values = new Map(environment.map((entry)=>{
|
|
200
|
+
const separator = entry.indexOf('=');
|
|
201
|
+
return [
|
|
202
|
+
entry.slice(0, separator),
|
|
203
|
+
entry.slice(separator + 1)
|
|
204
|
+
];
|
|
205
|
+
}));
|
|
206
|
+
const configured = [
|
|
207
|
+
'CODEM_STATE_HOME',
|
|
208
|
+
'LINCO_HOME',
|
|
209
|
+
'CODEM_HOME'
|
|
210
|
+
].flatMap((key)=>{
|
|
211
|
+
const value = values.get(key)?.trim();
|
|
212
|
+
return value ? [
|
|
213
|
+
value
|
|
214
|
+
] : [];
|
|
215
|
+
});
|
|
216
|
+
const base = values.get('HOME');
|
|
217
|
+
const homes = configured.length ? configured : base ? [
|
|
218
|
+
join(base, '.codem', ...isCodeMBoeTtEnv(values.get('CODEM_X_TT_ENV')) ? [
|
|
219
|
+
'.boe'
|
|
220
|
+
] : [])
|
|
221
|
+
] : [];
|
|
222
|
+
if (!homes.length) return true;
|
|
223
|
+
for (const candidate of homes){
|
|
224
|
+
if (!isAbsolute(candidate)) return true;
|
|
225
|
+
let canonical;
|
|
226
|
+
try {
|
|
227
|
+
canonical = await realpath(candidate);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (!missing(error)) return true;
|
|
230
|
+
canonical = resolve(candidate);
|
|
231
|
+
}
|
|
232
|
+
if (canonical === resolve(home)) return true;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
async function windowsCodeMProcesses() {
|
|
237
|
+
const script = `$ErrorActionPreference='Stop'; Get-CimInstance Win32_Process | Where-Object {
|
|
238
|
+
$_.ProcessId -ne ${process.pid} -and $_.CommandLine -notmatch '--type=' -and (
|
|
239
|
+
$_.Name -match '^(codem|codem-cli|codem-core|linco)(\\.exe)?$' -or
|
|
240
|
+
($_.Name -match '^node(\\.exe)?$' -and $_.CommandLine -match '(codem-cli[\\\\/]|codem-bridge[\\\\/].*src[\\\\/]index\\.js)')
|
|
241
|
+
)
|
|
242
|
+
} | ForEach-Object { "$($_.ProcessId)|$($_.Name)" }`;
|
|
243
|
+
const { stdout } = await runProcess({
|
|
244
|
+
executable: 'powershell.exe',
|
|
245
|
+
arguments: [
|
|
246
|
+
'-NoProfile',
|
|
247
|
+
'-NonInteractive',
|
|
248
|
+
'-Command',
|
|
249
|
+
script
|
|
250
|
+
],
|
|
251
|
+
windowsHide: true,
|
|
252
|
+
timeoutMilliseconds: 10_000,
|
|
253
|
+
maximumOutputBytes: 1024 * 1024
|
|
254
|
+
});
|
|
255
|
+
return parseWindowsCodeMProcesses(stdout);
|
|
256
|
+
}
|
|
257
|
+
export function parseWindowsCodeMProcesses(stdout) {
|
|
258
|
+
return deduplicateBlockers(stdout.split(/\r?\n/u).flatMap((line)=>{
|
|
259
|
+
if (!line.trim()) return [];
|
|
260
|
+
const match = /^\s*(\d+)\|((?:codem|codem-cli|codem-core|linco|node)(?:\.exe)?)\s*$/iu.exec(line);
|
|
261
|
+
const processId = match ? Number(match[1]) : NaN;
|
|
262
|
+
if (!match || !Number.isSafeInteger(processId) || processId <= 0) {
|
|
263
|
+
throw new Error('Unexpected Windows process inspection output');
|
|
264
|
+
}
|
|
265
|
+
const processName = match[2];
|
|
266
|
+
const cli = /^(?:codem-cli|codem-core|linco|node)(?:\.exe)?$/iu.test(processName);
|
|
267
|
+
return [
|
|
268
|
+
{
|
|
269
|
+
kind: cli ? 'cli-process' : 'codem-process',
|
|
270
|
+
processId,
|
|
271
|
+
processName
|
|
272
|
+
}
|
|
273
|
+
];
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
function processBlocker(kind, processId, processName) {
|
|
277
|
+
return {
|
|
278
|
+
kind: kind === 'app' ? 'app-process' : 'cli-process',
|
|
279
|
+
processId,
|
|
280
|
+
processName
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function deduplicateBlockers(blockers) {
|
|
284
|
+
return [
|
|
285
|
+
...new Map(blockers.map((blocker)=>[
|
|
286
|
+
`${blocker.kind}:${blocker.processId ?? 'none'}:${blocker.processName ?? 'none'}`,
|
|
287
|
+
blocker
|
|
288
|
+
])).values()
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
function errorCode(error) {
|
|
292
|
+
return error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' && /^[A-Z_]+$/u.test(error.code) ? error.code : null;
|
|
293
|
+
}
|
|
294
|
+
function missing(error) {
|
|
295
|
+
return error instanceof Error && 'code' in error && (error.code === 'ENOENT' || error.code === 'ESRCH');
|
|
296
|
+
}
|