Files

739 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
const fs = require("fs");
const os = require("os");
const path = require("path");
const { execFile, execFileSync } = require("child_process");
const EXE_NAMES = process.platform === "win32"
? ["ChatGPT.exe", "Codex.exe", "OpenAI ChatGPT.exe", "ChatGPT Desktop.exe", "Codex Desktop.exe"]
: ["ChatGPT", "Codex"];
const WIN_PROCESS_NAMES = EXE_NAMES.filter((n) => /\.exe$/i.test(n));
let lastDiscovery = null;
const GLOBAL_DICTATION_HIDE_SCRIPT = `
set hiddenCount to 0
tell application "System Events"
set targetProcesses to every process whose name is "ChatGPT" or name is "Codex"
repeat with p in targetProcesses
tell p
repeat with w in windows
try
set titleText to title of w as text
set roleText to role of w as text
set subroleText to subrole of w as text
set windowSize to size of w
set widthValue to item 1 of windowSize as integer
set heightValue to item 2 of windowSize as integer
if titleText is "Codex" and roleText is "AXWindow" and subroleText is "AXDialog" and widthValue >= 500 and widthValue <= 900 and heightValue >= 60 and heightValue <= 120 then
set position of w to {-10000, -10000}
set hiddenCount to hiddenCount + 1
end if
end try
end repeat
end tell
end repeat
end tell
return hiddenCount
`;
function existsFile(p) {
try {
return !!p && fs.statSync(p).isFile();
} catch {
return false;
}
}
function isWindowsPackagedPath(p) {
return process.platform === "win32" && /[\\/]WindowsApps[\\/]|[\\/]Program Files[\\/]WindowsApps[\\/]/i.test(String(p || ""));
}
function isOwnWindowsAppCandidate(name, appId) {
const hay = [name, appId].filter(Boolean).join(" ");
return /tokendun|codexmarket|codex-market|td工具箱|td toolbox|插件市场|技能市场/i.test(hay);
}
function isOpenAiDesktopCandidate(name, appId) {
if (isOwnWindowsAppCandidate(name, appId)) return false;
const n = String(name || "").trim();
const hay = [name, appId].filter(Boolean).join(" ");
return /openai|chatgpt/i.test(hay) || /(^|[^a-z])codex([^a-z]|$)/i.test(n);
}
function addCandidate(out, seen, label, p, opts = {}) {
if (!p) return;
if (opts.launchType === "appId") {
const appId = String(opts.appId || p || "").trim();
if (!appId || isOwnWindowsAppCandidate(opts.name || "", appId)) return;
const key = "appid:" + appId.toLowerCase();
if (seen.has(key)) return;
seen.add(key);
out.push({ label, path: appId, appId, name: opts.name || "", launchType: "appId", packaged: true });
return;
}
let abs = "";
try {
abs = path.resolve(p);
} catch {
return;
}
const key = process.platform === "win32" ? abs.toLowerCase() : abs;
if (seen.has(key) || !existsFile(abs)) return;
seen.add(key);
out.push({
label,
path: abs,
launchType: opts.launchType || "exe",
packaged: !!opts.packaged || isWindowsPackagedPath(abs),
});
}
function addChecked(report, p) {
if (!report || !p) return;
try {
report.checkedPaths.push(path.resolve(p));
} catch {}
}
function shouldScanDir(name, depth) {
if (!name) return false;
if (/^(programs)$/i.test(name)) return true;
if (/codex|chatgpt|openai/i.test(name)) return true;
return depth === 0 && process.platform !== "win32";
}
function scanDirForExecutables(base, out, seen, opts = {}) {
const maxDepth = opts.maxDepth == null ? 4 : opts.maxDepth;
const maxEntries = opts.maxEntries == null ? 400 : opts.maxEntries;
const maxHits = opts.maxHits == null ? 12 : opts.maxHits;
const report = opts.report || null;
const targetNames = new Set(EXE_NAMES.map((n) => process.platform === "win32" ? n.toLowerCase() : n));
let visited = 0;
if (report && base) report.scannedRoots.push(base);
function walk(dir, depth) {
if (!dir || depth > maxDepth || out.length >= maxHits || visited >= maxEntries) return;
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
visited += entries.length;
for (const ent of entries) {
if (out.length >= maxHits) return;
const full = path.join(dir, ent.name);
const entName = process.platform === "win32" ? ent.name.toLowerCase() : ent.name;
if (ent.isFile() && targetNames.has(entName)) {
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(扫描)", full);
continue;
}
if (ent.isDirectory() && depth < maxDepth && shouldScanDir(ent.name, depth)) {
walk(full, depth + 1);
}
}
}
walk(base, 0);
}
function psJson(script, timeout = 3500) {
if (process.platform !== "win32") return null;
const shells = ["powershell.exe", "pwsh.exe"];
for (const shell of shells) {
try {
const out = execFileSync(shell, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
encoding: "utf-8",
windowsHide: true,
timeout,
}).trim();
if (!out) return null;
return JSON.parse(out);
} catch {}
}
return null;
}
function resolveWindowsShortcut(lnkPath) {
if (process.platform !== "win32" || !lnkPath) return null;
const script = [
"$p=$env:TD_LNK_PATH",
"$s=(New-Object -ComObject WScript.Shell).CreateShortcut($p)",
"$o=[pscustomobject]@{TargetPath=$s.TargetPath;Arguments=$s.Arguments;WorkingDirectory=$s.WorkingDirectory}",
"$o|ConvertTo-Json -Compress",
].join(";");
const shells = ["powershell.exe", "pwsh.exe"];
for (const shell of shells) {
try {
const out = execFileSync(shell, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
encoding: "utf-8",
env: { ...process.env, TD_LNK_PATH: lnkPath },
windowsHide: true,
timeout: 2500,
}).trim();
if (!out) return null;
return JSON.parse(out);
} catch {}
}
return null;
}
function collectWindowsShortcuts(base, out, opts = {}) {
if (process.platform !== "win32" || !base) return;
const maxDepth = opts.maxDepth == null ? 5 : opts.maxDepth;
const maxEntries = opts.maxEntries == null ? 900 : opts.maxEntries;
const maxHits = opts.maxHits == null ? 24 : opts.maxHits;
let visited = 0;
function walk(dir, depth) {
if (!dir || depth > maxDepth || out.length >= maxHits || visited >= maxEntries) return;
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
visited += entries.length;
for (const ent of entries) {
if (out.length >= maxHits) return;
const full = path.join(dir, ent.name);
if (ent.isFile() && /\.lnk$/i.test(ent.name) && /codex|chatgpt|openai/i.test(ent.name)) {
out.push(full);
continue;
}
if (ent.isDirectory() && depth < maxDepth && shouldScanDir(ent.name, depth)) {
walk(full, depth + 1);
}
}
}
walk(base, 0);
}
function addSquirrelShortcutTargets(out, seen, report, shortcut, targetPath, args) {
if (!targetPath || !/update\.exe$/i.test(path.basename(targetPath || ""))) return;
const m = String(args || "").match(/--processStart\s+(?:"([^"]+)"|([^\s]+))/i);
const exeName = (m && (m[1] || m[2])) || "";
if (!exeName || !/\.exe$/i.test(exeName)) return;
const root = path.dirname(targetPath);
let entries = [];
try {
entries = fs.readdirSync(root, { withFileTypes: true });
} catch {
return;
}
const appDirs = entries
.filter((ent) => ent.isDirectory() && /^app-/i.test(ent.name))
.map((ent) => path.join(root, ent.name))
.sort()
.reverse();
for (const dir of appDirs) {
const exe = path.join(dir, exeName);
addChecked(report, exe);
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(开始菜单快捷方式)", exe);
}
if (report) report.shortcutTargets.push({ shortcut, targetPath, args: args || "", inferred: exeName });
}
function addWindowsShortcutCandidates(out, seen, report, roots) {
const shortcuts = [];
for (const root of roots.filter(Boolean)) collectWindowsShortcuts(root, shortcuts);
for (const lnk of shortcuts) {
if (report) report.shortcuts.push(lnk);
const resolved = resolveWindowsShortcut(lnk);
if (!resolved) continue;
const targetPath = resolved.TargetPath || resolved.targetPath || "";
const args = resolved.Arguments || resolved.arguments || "";
if (report) report.shortcutTargets.push({ shortcut: lnk, targetPath, args });
if (targetPath && /\.exe$/i.test(targetPath) && !/update\.exe$/i.test(path.basename(targetPath))) {
addChecked(report, targetPath);
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(开始菜单快捷方式)", targetPath);
continue;
}
addSquirrelShortcutTargets(out, seen, report, lnk, targetPath, args);
}
}
function addWindowsAppxCandidates(out, seen, report) {
const script = [
"$startApps=Get-StartApps | Where-Object { $_.Name -match 'ChatGPT|Codex|OpenAI' -or $_.AppID -match 'ChatGPT|Codex|OpenAI' }",
"$startRows=@()",
"foreach($a in $startApps){ $startRows += [pscustomobject]@{Name=$a.Name;AppID=$a.AppID} }",
"$pkgs=Get-AppxPackage | Where-Object { $_.Name -match 'ChatGPT|Codex|OpenAI' -or $_.PackageFullName -match 'ChatGPT|Codex|OpenAI' }",
"$rows=@()",
"$appIds=@()",
"foreach($p in $pkgs){",
" $loc=$p.InstallLocation",
" if($loc){",
" $manifestPath=Join-Path $loc 'AppxManifest.xml'",
" if(Test-Path -LiteralPath $manifestPath){",
" try{",
" [xml]$manifest=Get-Content -LiteralPath $manifestPath -ErrorAction Stop",
" foreach($app in @($manifest.Package.Applications.Application)){",
" $id=[string]$app.Id",
" if($id){ $appIds += [pscustomobject]@{Package=$p.Name;Family=$p.PackageFamilyName;AppID=($p.PackageFamilyName + '!' + $id);InstallLocation=$loc} }",
" }",
" }catch{}",
" }",
" $exes=Get-ChildItem -LiteralPath $loc -Recurse -Filter *.exe -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'ChatGPT|Codex|OpenAI' } | Select-Object -First 8",
" foreach($e in $exes){ $rows += [pscustomobject]@{Package=$p.Name;Path=$e.FullName} }",
" }",
"}",
"[pscustomobject]@{StartApps=$startRows;Packages=$rows;AppIds=$appIds}|ConvertTo-Json -Depth 5 -Compress",
].join(";");
const data = psJson(script, 6500);
const startApps = data && data.StartApps ? (Array.isArray(data.StartApps) ? data.StartApps : [data.StartApps]) : [];
const arr = data && data.Packages ? (Array.isArray(data.Packages) ? data.Packages : [data.Packages]) : [];
const appIds = data && data.AppIds ? (Array.isArray(data.AppIds) ? data.AppIds : [data.AppIds]) : [];
for (const app of startApps) {
const appId = app && app.AppID ? String(app.AppID) : "";
const name = app && app.Name ? String(app.Name) : "";
if (!appId || !isOpenAiDesktopCandidate(name, appId)) continue;
if (report) report.appx.push({ package: "StartApps", name: app.Name || "", appId });
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(开始菜单 AppID)", appId, {
launchType: "appId",
appId,
name,
packaged: true,
});
}
for (const app of appIds) {
const appId = app && app.AppID ? String(app.AppID) : "";
const name = app && app.Package ? String(app.Package) : "";
if (!appId || !isOpenAiDesktopCandidate(name, appId)) continue;
if (report) report.appx.push({ package: name, family: app.Family || "", appId, manifest: true });
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(MSIX AppID)", appId, {
launchType: "appId",
appId,
name,
packaged: true,
});
}
for (const row of arr) {
const p = row && row.Path ? String(row.Path) : "";
if (report && row) report.appx.push({ package: row.Package || "", path: p });
addChecked(report, p);
// Program Files\WindowsApps 里的包内 exe 通常不能被普通进程直接启动;
// 只记录诊断,不当作启动候选,避免弹系统错误框。
if (p && !isWindowsPackagedPath(p)) {
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(Microsoft Store/MSIX)", p);
}
}
}
function desktopExecutableCandidates() {
const home = os.homedir();
const out = [];
const seen = new Set();
const report = {
platform: process.platform,
checkedPaths: [],
scannedRoots: [],
shortcuts: [],
shortcutTargets: [],
appx: [],
candidates: [],
};
if (process.platform === "darwin") {
[
path.join(home, "Applications/ChatGPT.app/Contents/MacOS/ChatGPT"),
"/Applications/ChatGPT.app/Contents/MacOS/ChatGPT",
path.join(home, "Applications/Codex.app/Contents/MacOS/Codex"),
"/Applications/Codex.app/Contents/MacOS/Codex",
].forEach((p) => {
addChecked(report, p);
addCandidate(out, seen, "ChatGPT/Codex 桌面程序", p);
});
const sorted = sortCandidates(out);
lastDiscovery = { ...report, candidates: sorted };
return sorted;
}
if (process.platform === "win32") {
const local = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming");
const programData = process.env.ProgramData || "C:\\ProgramData";
const pf = process.env.ProgramFiles || "C:\\Program Files";
const pf86 = process.env["ProgramFiles(x86)"] || "";
const windowsApps = path.join(local, "Microsoft", "WindowsApps");
const userStartMenu = path.join(appdata, "Microsoft", "Windows", "Start Menu", "Programs");
const systemStartMenu = path.join(programData, "Microsoft", "Windows", "Start Menu", "Programs");
const known = [
[local, "Programs", "ChatGPT", "ChatGPT.exe"],
[local, "Programs", "OpenAI", "ChatGPT", "ChatGPT.exe"],
[local, "Programs", "OpenAI ChatGPT", "ChatGPT.exe"],
[local, "Programs", "ChatGPT Desktop", "ChatGPT.exe"],
[local, "ChatGPT", "ChatGPT.exe"],
[local, "OpenAI", "ChatGPT", "ChatGPT.exe"],
[local, "OpenAI", "ChatGPT", "bin", "ChatGPT.exe"],
[pf, "ChatGPT", "ChatGPT.exe"],
[pf, "OpenAI", "ChatGPT", "ChatGPT.exe"],
[pf, "OpenAI ChatGPT", "ChatGPT.exe"],
pf86 && [pf86, "ChatGPT", "ChatGPT.exe"],
pf86 && [pf86, "OpenAI", "ChatGPT", "ChatGPT.exe"],
[local, "OpenAI", "Codex", "bin", "Codex.exe"],
[local, "Programs", "Codex", "Codex.exe"],
[local, "Programs", "codex", "Codex.exe"],
[local, "Programs", "OpenAI Codex", "Codex.exe"],
[local, "Programs", "Codex Desktop", "Codex.exe"],
[local, "Codex", "Codex.exe"],
[pf, "Codex", "Codex.exe"],
[pf, "OpenAI Codex", "Codex.exe"],
pf86 && [pf86, "Codex", "Codex.exe"],
].filter(Boolean);
for (const parts of known) {
const p = path.join(...parts);
addChecked(report, p);
addCandidate(out, seen, "ChatGPT/Codex 桌面程序", p);
}
for (const p of [path.join(windowsApps, "ChatGPT.exe"), path.join(windowsApps, "Codex.exe")]) {
addChecked(report, p);
if (existsFile(p)) {
report.appx.push({ package: "WindowsApps alias", path: path.resolve(p) });
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(WindowsApps 别名)", p, {
launchType: "alias",
packaged: true,
});
}
}
addWindowsShortcutCandidates(out, seen, report, [userStartMenu, systemStartMenu]);
addWindowsAppxCandidates(out, seen, report);
for (const base of [path.join(local, "Programs"), local, pf, pf86, windowsApps].filter(Boolean)) {
scanDirForExecutables(base, out, seen, { maxDepth: 5, maxEntries: 1600, maxHits: 24, report });
}
const sorted = sortCandidates(out);
lastDiscovery = { ...report, candidates: sorted };
return sorted;
}
lastDiscovery = { ...report, candidates: out };
return out;
}
function sortCandidates(items) {
return [...items].sort((a, b) => scoreCandidate(b.path) - scoreCandidate(a.path));
}
function scoreCandidate(p) {
let score = 0;
if (/chatgpt/i.test(path.basename(p))) score += 100;
if (/codex/i.test(path.basename(p))) score += 50;
if (isWindowsPackagedPath(p)) score -= 1000;
if (/localappdata|AppData[\\/]Local|Applications/i.test(p)) score += 10;
if (/programs/i.test(p)) score += 5;
return score;
}
function codexExecutable() {
const first = desktopExecutableCandidates().find((item) => !item.packaged);
return first ? first.path : "";
}
function discoveryReport() {
return lastDiscovery || {
platform: process.platform,
checkedPaths: [],
scannedRoots: [],
shortcuts: [],
shortcutTargets: [],
appx: [],
candidates: [],
};
}
function discoverySummary() {
const r = discoveryReport();
const parts = [];
if (r.checkedPaths && r.checkedPaths.length) parts.push(`固定路径 ${r.checkedPaths.length} 个`);
if (r.scannedRoots && r.scannedRoots.length) parts.push(`目录扫描 ${r.scannedRoots.length} 处`);
if (r.shortcuts && r.shortcuts.length) parts.push(`快捷方式 ${r.shortcuts.length} 个`);
if (r.appx && r.appx.length) parts.push(`Store/MSIX 线索 ${r.appx.length} 个`);
if (r.candidates && r.candidates.length) parts.push(`命中 ${r.candidates.length} 个`);
return parts.join(",");
}
function codexRunning() {
try {
if (process.platform === "darwin") {
return execFileSync("pgrep", ["-f", "(Codex.app/Contents/MacOS/Codex|ChatGPT.app/Contents/MacOS/ChatGPT)"], { encoding: "utf-8" }).trim().length > 0;
}
if (process.platform === "win32") {
return windowsProcessRows().length > 0;
}
} catch {
return false;
}
return false;
}
function execFileText(file, args, options = {}) {
return new Promise((resolve, reject) => {
execFile(file, args, {
encoding: "utf-8",
windowsHide: true,
timeout: options.timeout === undefined
? 2200
: Math.max(1, Number(options.timeout) || 1),
signal: options.signal,
env: options.env || process.env,
}, (error, stdout) => {
if (error) {
error.stdout = stdout || error.stdout || "";
reject(error);
return;
}
resolve(String(stdout || ""));
});
});
}
function isCodexProcessCommand(command) {
const text = String(command || "").replace(/\s+/g, " ").trim();
if (!text || isOwnWindowsAppCandidate(text, "")) return false;
return /(?:^|[\\/])(?:Codex|ChatGPT)\.app[\\/]/i.test(text) ||
/(?:^|[\\/\s])(?:Codex|ChatGPT)(?:\.exe)?(?:\s|$)/i.test(text) ||
/WindowsApps[\\/]OpenAI\.(?:Codex|CodexBeta|ChatGPT-Desktop)_/i.test(text);
}
function hideGlobalDictationWindows(options = {}) {
if (process.platform !== "darwin") {
return Promise.resolve({
ok: false,
status: "unsupported-platform",
hidden: 0,
error: "native global dictation window cleanup is only available on macOS",
});
}
const run = options.execFile || execFile;
return new Promise((resolve) => {
run("osascript", ["-e", GLOBAL_DICTATION_HIDE_SCRIPT], {
encoding: "utf-8",
windowsHide: true,
timeout: Math.max(500, Number(options.timeoutMs) || 5000),
signal: options.signal,
}, (error, stdout, stderr) => {
const output = String(stdout || "").trim();
const hidden = /^\d+$/.test(output) ? Number(output) : 0;
if (error) {
resolve({
ok: false,
status: "native-command-failed",
hidden,
error: String((error && error.message) || stderr || error),
});
return;
}
resolve({
ok: true,
status: hidden ? "hidden" : "not-found",
hidden,
error: "",
});
});
});
}
async function debugPortOwner(port, options = {}) {
const value = Number(port);
if (!Number.isInteger(value) || value < 1 || value > 65535) {
return { verified: false, owned: false, reason: "invalid-port", processes: [] };
}
const signal = options.signal;
const totalTimeoutMs = Math.max(1, Number(options.timeoutMs) || 5000);
const deadline = Date.now() + totalTimeoutMs;
const commandOptions = (maxMs) => {
if (signal && signal.aborted) {
throw signal.reason instanceof Error ? signal.reason : new Error("debug port owner check aborted");
}
const remaining = deadline - Date.now();
if (remaining <= 0) {
const error = new Error("debug port owner check timed out");
error.code = "ETIMEDOUT";
throw error;
}
return {
timeout: Math.max(1, Math.min(maxMs, remaining)),
signal,
};
};
if (process.platform === "darwin") {
try {
const output = await execFileText("lsof", [
"-nP",
"-a",
`-iTCP:${value}`,
"-sTCP:LISTEN",
"-Fp",
], commandOptions(2200));
const pids = [...new Set(
output.split(/\r?\n/).filter((line) => /^p\d+$/.test(line)).map((line) => line.slice(1)),
)];
const processes = [];
for (const pid of pids) {
try {
const command = (await execFileText(
"ps",
["-p", pid, "-o", "command="],
commandOptions(2200),
)).trim();
processes.push({ pid: Number(pid), command });
} catch {}
}
return {
verified: true,
owned: processes.some((item) => isCodexProcessCommand(item.command)),
reason: processes.length ? "process-inspected" : "listener-not-found",
processes,
};
} catch (e) {
return {
verified: false,
owned: false,
reason: String((e && e.message) || e),
processes: [],
};
}
}
if (process.platform === "win32") {
const script = [
"$port=[int]$env:TD_CODEX_DEBUG_PORT",
"$rows=@()",
"Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue | ForEach-Object {",
" $p=Get-CimInstance Win32_Process -Filter ('ProcessId=' + $_.OwningProcess) -ErrorAction SilentlyContinue",
" if($p){ $rows += [pscustomobject]@{ProcessId=$p.ProcessId;Name=$p.Name;ExecutablePath=$p.ExecutablePath;CommandLine=$p.CommandLine} }",
"}",
"$rows|ConvertTo-Json -Depth 4 -Compress",
].join(";");
for (const shell of ["powershell.exe", "pwsh.exe"]) {
try {
const output = (await execFileText(shell, [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
script,
], {
...commandOptions(3500),
env: { ...process.env, TD_CODEX_DEBUG_PORT: String(value) },
})).trim();
const parsed = output ? JSON.parse(output) : [];
const rows = Array.isArray(parsed) ? parsed : [parsed];
const processes = rows.map((row) => ({
pid: Number(row.ProcessId) || 0,
command: [row.Name, row.ExecutablePath, row.CommandLine].filter(Boolean).join(" "),
}));
return {
verified: true,
owned: processes.some((item) => isCodexProcessCommand(item.command)),
reason: processes.length ? "process-inspected" : "listener-not-found",
processes,
};
} catch {}
}
return { verified: false, owned: false, reason: "process-inspection-failed", processes: [] };
}
return { verified: false, owned: false, reason: "unsupported-platform", processes: [] };
}
function windowsProcessRows() {
if (process.platform !== "win32") return [];
const escapedNames = WIN_PROCESS_NAMES.map((name) => "'" + name.replace(/'/g, "''") + "'").join(",");
const script = [
`$names=@(${escapedNames})`,
"$pkg='WindowsApps\\\\OpenAI\\.(Codex|CodexBeta|ChatGPT-Desktop)_'",
"$rows=Get-CimInstance Win32_Process | Where-Object { $names -contains $_.Name -or ($_.ExecutablePath -match $pkg) -or ($_.CommandLine -match $pkg) } | ForEach-Object {",
" [pscustomobject]@{Name=$_.Name;ProcessId=$_.ProcessId;ExecutablePath=$_.ExecutablePath;CommandLine=$_.CommandLine}",
"}",
"$rows|ConvertTo-Json -Depth 4 -Compress",
].join(";");
const data = psJson(script, 4500);
if (!data) return [];
return Array.isArray(data) ? data : [data];
}
function windowsProcessDiagnostics(port) {
const rows = windowsProcessRows();
const portRe = new RegExp("--remote-debugging-port(?:=|\\s+)" + String(port || ""), "i");
const hasDebugArg = rows.some((row) => portRe.test(String((row && row.CommandLine) || "")));
const summary = rows.map((row) => {
const cmd = String((row && row.CommandLine) || "").replace(/\s+/g, " ").slice(0, 260);
const name = (row && row.Name) || "process";
const pid = (row && row.ProcessId) || "?";
return `${name}#${pid}${cmd ? " " + cmd : ""}`;
}).join(" | ");
return { rows, hasDebugArg, summary };
}
function waitForCodexExit(maxMs = 9000) {
return new Promise((resolve) => {
const t0 = Date.now();
const tick = () => {
if (!codexRunning()) {
resolve(true);
return;
}
if (Date.now() - t0 >= maxMs) {
resolve(false);
return;
}
setTimeout(tick, 300);
};
tick();
});
}
function killCodex() {
return new Promise((resolve) => {
if (process.platform === "darwin") {
execFile("osascript", ["-e", 'tell application "Codex" to quit', "-e", 'tell application "ChatGPT" to quit'], () => {
execFile("pkill", ["-f", "(Codex.app/Contents/MacOS/Codex|ChatGPT.app/Contents/MacOS/ChatGPT)"], () => {
waitForCodexExit(12000).then((exited) => resolve({ exited }));
});
});
return;
}
if (process.platform === "win32") {
let pending = WIN_PROCESS_NAMES.length;
const done = () => {
pending -= 1;
if (pending <= 0) {
waitForCodexExit(9000).then((ok) => {
if (!ok) {
try {
execFileSync("powershell.exe", [
"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
"$pkg='WindowsApps\\\\OpenAI\\.(Codex|CodexBeta|ChatGPT-Desktop)_'; Get-CimInstance Win32_Process | Where-Object { @('ChatGPT.exe','Codex.exe','OpenAI ChatGPT.exe','ChatGPT Desktop.exe','Codex Desktop.exe') -contains $_.Name -or ($_.ExecutablePath -match $pkg) -or ($_.CommandLine -match $pkg) } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
], { windowsHide: true, timeout: 3500 });
} catch {}
}
waitForCodexExit(5000).then((exited) => resolve({ exited }));
});
}
};
for (const name of WIN_PROCESS_NAMES) execFile("taskkill", ["/IM", name, "/T", "/F"], done);
return;
}
resolve({ exited: true });
});
}
module.exports = {
desktopExecutableCandidates,
codexExecutable,
isWindowsPackagedPath,
discoveryReport,
discoverySummary,
codexRunning,
debugPortOwner,
hideGlobalDictationWindows,
windowsProcessDiagnostics,
killCodex,
};