Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,10 +47,11 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS } = {}) {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false } = {}) {
this.projectDir = projectDir;
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
this.ws = null;
this.connected = false;
this._events = new Set(); // callbacks
Expand DownExpand Up@@ -82,6 +83,32 @@ class DaemonClient {
}

async startDaemon() {
// Phase 4(rant #12 §4):打包模式直接 spawn 捆绑 emrgd 可执行文件(脚本内部
// exec python -m emrg.server);源码模式保持 python -m emrg.server。
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
stdio: "ignore",
detached: true,
};
// R36/R66:Windows .cmd 需 shell:true(非 PE),windowsHide 防黑窗闪烁;
// Node shell 模式自动给含空格的用户名加引号(R92b)。
if (process.platform === "win32") {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
const deadline = Date.now() + SPAWN_WAIT_MS;
while (Date.now() < deadline) {
if (await this.isRunning(500)) return child;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("emrgd failed to start within timeout");
}
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
Expand All@@ -102,6 +129,14 @@ class DaemonClient {
throw new Error("emrgd failed to start within timeout");
}

_findDaemonExecutable() {
// Phase 4(rant #12 §4 R7):打包模式定位捆绑 emrgd。
// Windows: ~/.emrg/install/bin/emrgd.cmd;POSIX: ~/.emrg/install/bin/emrgd。
const bin = path.join(os.homedir(), ".emrg", "install", "bin");
const name = process.platform === "win32" ? "emrgd.cmd" : "emrgd";
return path.join(bin, name);
}

_findPython() {
// G59/G61/G126:优先项目 .venv,其次 PATH python3/python
const root = path.resolve(__dirname, "..", "..");
Expand Down
80 changes: 79 additions & 1 deletion emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,9 +105,51 @@ function main() {

function configPath() { return path.join(os.homedir(), ".emrg", "config.toml"); }

// Phase 4(rant #12 §4 R108):打包模式无 .venv/PATH python → 内联模板直接生成
// (与 emrg/config.py ensure_config() 105-130 行同源);源码模式复用 ensure_config()。
const CONFIG_TEMPLATE = `[llm]
# OpenAI-compatible API endpoint
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-chat"
max_tokens = 8192
temperature = 0.7
context_window = 131072
auto_compact_threshold = 0.0
# vision: set to true if model supports OpenAI vision API (image_url content type)
vision = false

# Additional models for /model switching (optional — add or remove as needed)
# model: API model name (optional — defaults to name if not set)
[[llm.models]]
name = "deepseek-v3"
model = "deepseek-chat"
context_window = 131072
vision = false

[[llm.models]]
name = "deepseek-r1"
model = "deepseek-reasoner"
context_window = 65536
vision = false
`;

function ensureConfigTemplate() {
// G116:优先复用 ensure_config() 生成官方模板(含 [[llm.models]] 预置)
return new Promise((resolve) => {
if (app.isPackaged) {
if (!fs.existsSync(configPath())) {
try {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), CONFIG_TEMPLATE, { mode: 0o600 });
} catch (e) {
resolve("");
return;
}
}
resolve(fs.readFileSync(configPath(), "utf8"));
return;
}
const python = client?._findPython() || "python3";
const child = spawn(python, ["-c", "from emrg.config import ensure_config; ensure_config()"], {
cwd: projectDir,
Expand DownExpand Up@@ -335,7 +377,9 @@ function main() {

async function ensureConnected() {
if (!client) {
client = new DaemonClient({ projectDir, logger });
// Phase 4(rant #12 §4 R7):打包模式传 app.isPackaged → daemon_client 走
// 捆绑 emrgd 分支(_findDaemonExecutable)。
client = new DaemonClient({ projectDir, logger, isPackaged: app.isPackaged });
// G122:message_delta 16ms 批量推送
let deltaBuf = [];
let deltaTimer = null;
Expand DownExpand Up@@ -461,9 +505,43 @@ function main() {
}
});

// ── Phase 4 AppImage 自解压(rant #12 §5 R65/R88/R93)──────────
// AppImage 是自解压安装器:首次运行把 resourcesPath/runtime 复制到
// ~/.emrg/install/(稳定路径,非 AppImage 挂载),并建 ~/.local/bin/emrg 软链。
// 仅 Linux(process.env.APPIMAGE 存在);macOS/Windows 由安装器直接放置。
function ensureAppImageExtracted() {
if (process.platform !== "linux" || !process.env.APPIMAGE) return;
const home = os.homedir();
const installBin = path.join(home, ".emrg", "install", "bin");
if (fs.existsSync(installBin)) return; // 已解压
const runtimeSrc = path.join(process.resourcesPath, "runtime");
if (!fs.existsSync(runtimeSrc)) return;
try {
// R88:先 mkdir ~/.local/bin 否则 symlinkSync ENOENT
fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true });
// R93:250MB 复制期间显示"正在安装 EMRG..."提示防误判卡死
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: true } });
}
fs.cpSync(runtimeSrc, path.join(home, ".emrg", "install"), { recursive: true });
const link = path.join(home, ".local", "bin", "emrg");
if (!fs.existsSync(link)) {
fs.symlinkSync(path.join(home, ".emrg", "install", "bin", "emrg"), link, "file");
}
logger.info("[gui] AppImage runtime extracted to ~/.emrg/install/");
} catch (e) {
logger.error("[gui] AppImage extraction failed (retry on next launch)", e);
} finally {
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: false } });
}
}
}

app.whenReady().then(() => {
registerIpc();
createWindow();
ensureAppImageExtracted(); // R65:自解压归 main.js(electron-builder AppRun 无官方自定义)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Expand Down
52 changes: 52 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,58 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
const expected = path.join(os.homedir(), ".emrg", "install", "bin", process.platform === "win32" ? "emrgd.cmd" : "emrgd");
assert.strictEqual(exe, expected);
assert.ok(exe.includes(path.join(".emrg", "install", "bin")), `exe=${exe}`);
});

test("Phase4: isPackaged startDaemon 走捆绑 emrgd 分支(非 python -m)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
// _findPython 只存在于源码分支;打包分支调用 _findDaemonExecutable。
const exe = client._findDaemonExecutable();
assert.ok(exe.endsWith("emrgd") || exe.endsWith("emrgd.cmd"), `exe=${exe}`);
// 打包分支绝不能触达 _findPython:置为抛错,若被调用测试即失败。
client._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
// _findDaemonExecutable 返回真实路径;真实 spawn 会执行该文件——用一个
// 必然超时失败但能证明走了打包分支的方式:spawn 的 emrgd 不存在时 error 事件。
// 更稳:stub isRunning 恒 false + 缩短等待,捕获 startDaemon 抛的超时错,
// 说明它没有走 _findPython(否则直接抛上面的错)。
client.isRunning = async () => false;
const realSpawn = require("child_process").spawn;
const childProcess = require("child_process");
// daemon_client 顶层解构 { spawn }——只能替换 require.cache 中的模块导出。
// 简化:monkey-patch childProcess.spawn 无法生效,改用 spy 模块缓存。
// 这里直接验证:打包分支下 spawn 的 cmd 是 emrgd(通过临时替换模块再 require)。
const cacheKey = require.resolve("../daemon_client.js");
const originalModule = require.cache[cacheKey];
delete require.cache[cacheKey];
const origSpawn = require("child_process").spawn;
let captured = null;
require("child_process").spawn = (cmd, args, opts) => {
captured = { cmd, args, opts };
return { unref() {}, pid: 1234 };
};
let DaemonClientPackaged;
try {
DaemonClientPackaged = require("../daemon_client.js").DaemonClient;
const c2 = new DaemonClientPackaged({ projectDir: tmpHome, isPackaged: true });
c2._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
c2.isRunning = async () => true;
const child = await c2.startDaemon();
assert.strictEqual(child.pid, 1234);
assert.ok(captured, "spawn should be called");
assert.ok(String(captured.cmd).endsWith("emrgd") || String(captured.cmd).endsWith("emrgd.cmd"), `cmd=${captured.cmd}`);
assert.deepStrictEqual(captured.args, []);
assert.strictEqual(captured.opts.detached, true);
} finally {
require("child_process").spawn = origSpawn;
require.cache[cacheKey] = originalModule;
}
});

test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件重拉", async () => {
const client = new DaemonClient({ projectDir: tmpHome });
let respawned = false;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,10 +47,11 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS } = {}) {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false } = {}) {
this.projectDir = projectDir;
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
this.ws = null;
this.connected = false;
this._events = new Set(); // callbacks
Expand DownExpand Up@@ -82,6 +83,32 @@ class DaemonClient {
}

async startDaemon() {
// Phase 4(rant #12 §4):打包模式直接 spawn 捆绑 emrgd 可执行文件(脚本内部
// exec python -m emrg.server);源码模式保持 python -m emrg.server。
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
stdio: "ignore",
detached: true,
};
// R36/R66:Windows .cmd 需 shell:true(非 PE),windowsHide 防黑窗闪烁;
// Node shell 模式自动给含空格的用户名加引号(R92b)。
if (process.platform === "win32") {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
const deadline = Date.now() + SPAWN_WAIT_MS;
while (Date.now() < deadline) {
if (await this.isRunning(500)) return child;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("emrgd failed to start within timeout");
}
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
Expand All@@ -102,6 +129,14 @@ class DaemonClient {
throw new Error("emrgd failed to start within timeout");
}

_findDaemonExecutable() {
// Phase 4(rant #12 §4 R7):打包模式定位捆绑 emrgd。
// Windows: ~/.emrg/install/bin/emrgd.cmd;POSIX: ~/.emrg/install/bin/emrgd。
const bin = path.join(os.homedir(), ".emrg", "install", "bin");
const name = process.platform === "win32" ? "emrgd.cmd" : "emrgd";
return path.join(bin, name);
}

_findPython() {
// G59/G61/G126:优先项目 .venv,其次 PATH python3/python
const root = path.resolve(__dirname, "..", "..");
Expand Down
80 changes: 79 additions & 1 deletion emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,9 +105,51 @@ function main() {

function configPath() { return path.join(os.homedir(), ".emrg", "config.toml"); }

// Phase 4(rant #12 §4 R108):打包模式无 .venv/PATH python → 内联模板直接生成
// (与 emrg/config.py ensure_config() 105-130 行同源);源码模式复用 ensure_config()。
const CONFIG_TEMPLATE = `[llm]
# OpenAI-compatible API endpoint
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-chat"
max_tokens = 8192
temperature = 0.7
context_window = 131072
auto_compact_threshold = 0.0
# vision: set to true if model supports OpenAI vision API (image_url content type)
vision = false

# Additional models for /model switching (optional — add or remove as needed)
# model: API model name (optional — defaults to name if not set)
[[llm.models]]
name = "deepseek-v3"
model = "deepseek-chat"
context_window = 131072
vision = false

[[llm.models]]
name = "deepseek-r1"
model = "deepseek-reasoner"
context_window = 65536
vision = false
`;

function ensureConfigTemplate() {
// G116:优先复用 ensure_config() 生成官方模板(含 [[llm.models]] 预置)
return new Promise((resolve) => {
if (app.isPackaged) {
if (!fs.existsSync(configPath())) {
try {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), CONFIG_TEMPLATE, { mode: 0o600 });
} catch (e) {
resolve("");
return;
}
}
resolve(fs.readFileSync(configPath(), "utf8"));
return;
}
const python = client?._findPython() || "python3";
const child = spawn(python, ["-c", "from emrg.config import ensure_config; ensure_config()"], {
cwd: projectDir,
Expand DownExpand Up@@ -335,7 +377,9 @@ function main() {

async function ensureConnected() {
if (!client) {
client = new DaemonClient({ projectDir, logger });
// Phase 4(rant #12 §4 R7):打包模式传 app.isPackaged → daemon_client 走
// 捆绑 emrgd 分支(_findDaemonExecutable)。
client = new DaemonClient({ projectDir, logger, isPackaged: app.isPackaged });
// G122:message_delta 16ms 批量推送
let deltaBuf = [];
let deltaTimer = null;
Expand DownExpand Up@@ -461,9 +505,43 @@ function main() {
}
});

// ── Phase 4 AppImage 自解压(rant #12 §5 R65/R88/R93)──────────
// AppImage 是自解压安装器:首次运行把 resourcesPath/runtime 复制到
// ~/.emrg/install/(稳定路径,非 AppImage 挂载),并建 ~/.local/bin/emrg 软链。
// 仅 Linux(process.env.APPIMAGE 存在);macOS/Windows 由安装器直接放置。
function ensureAppImageExtracted() {
if (process.platform !== "linux" || !process.env.APPIMAGE) return;
const home = os.homedir();
const installBin = path.join(home, ".emrg", "install", "bin");
if (fs.existsSync(installBin)) return; // 已解压
const runtimeSrc = path.join(process.resourcesPath, "runtime");
if (!fs.existsSync(runtimeSrc)) return;
try {
// R88:先 mkdir ~/.local/bin 否则 symlinkSync ENOENT
fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true });
// R93:250MB 复制期间显示"正在安装 EMRG..."提示防误判卡死
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: true } });
}
fs.cpSync(runtimeSrc, path.join(home, ".emrg", "install"), { recursive: true });
const link = path.join(home, ".local", "bin", "emrg");
if (!fs.existsSync(link)) {
fs.symlinkSync(path.join(home, ".emrg", "install", "bin", "emrg"), link, "file");
}
logger.info("[gui] AppImage runtime extracted to ~/.emrg/install/");
} catch (e) {
logger.error("[gui] AppImage extraction failed (retry on next launch)", e);
} finally {
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: false } });
}
}
}

app.whenReady().then(() => {
registerIpc();
createWindow();
ensureAppImageExtracted(); // R65:自解压归 main.js(electron-builder AppRun 无官方自定义)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Expand Down
52 changes: 52 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,58 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
const expected = path.join(os.homedir(), ".emrg", "install", "bin", process.platform === "win32" ? "emrgd.cmd" : "emrgd");
assert.strictEqual(exe, expected);
assert.ok(exe.includes(path.join(".emrg", "install", "bin")), `exe=${exe}`);
});

test("Phase4: isPackaged startDaemon 走捆绑 emrgd 分支(非 python -m)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
// _findPython 只存在于源码分支;打包分支调用 _findDaemonExecutable。
const exe = client._findDaemonExecutable();
assert.ok(exe.endsWith("emrgd") || exe.endsWith("emrgd.cmd"), `exe=${exe}`);
// 打包分支绝不能触达 _findPython:置为抛错,若被调用测试即失败。
client._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
// _findDaemonExecutable 返回真实路径;真实 spawn 会执行该文件——用一个
// 必然超时失败但能证明走了打包分支的方式:spawn 的 emrgd 不存在时 error 事件。
// 更稳:stub isRunning 恒 false + 缩短等待,捕获 startDaemon 抛的超时错,
// 说明它没有走 _findPython(否则直接抛上面的错)。
client.isRunning = async () => false;
const realSpawn = require("child_process").spawn;
const childProcess = require("child_process");
// daemon_client 顶层解构 { spawn }——只能替换 require.cache 中的模块导出。
// 简化:monkey-patch childProcess.spawn 无法生效,改用 spy 模块缓存。
// 这里直接验证:打包分支下 spawn 的 cmd 是 emrgd(通过临时替换模块再 require)。
const cacheKey = require.resolve("../daemon_client.js");
const originalModule = require.cache[cacheKey];
delete require.cache[cacheKey];
const origSpawn = require("child_process").spawn;
let captured = null;
require("child_process").spawn = (cmd, args, opts) => {
captured = { cmd, args, opts };
return { unref() {}, pid: 1234 };
};
let DaemonClientPackaged;
try {
DaemonClientPackaged = require("../daemon_client.js").DaemonClient;
const c2 = new DaemonClientPackaged({ projectDir: tmpHome, isPackaged: true });
c2._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
c2.isRunning = async () => true;
const child = await c2.startDaemon();
assert.strictEqual(child.pid, 1234);
assert.ok(captured, "spawn should be called");
assert.ok(String(captured.cmd).endsWith("emrgd") || String(captured.cmd).endsWith("emrgd.cmd"), `cmd=${captured.cmd}`);
assert.deepStrictEqual(captured.args, []);
assert.strictEqual(captured.opts.detached, true);
} finally {
require("child_process").spawn = origSpawn;
require.cache[cacheKey] = originalModule;
}
});

test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件重拉", async () => {
const client = new DaemonClient({ projectDir: tmpHome });
let respawned = false;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,10 +47,11 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS } = {}) {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false } = {}) {
this.projectDir = projectDir;
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
this.ws = null;
this.connected = false;
this._events = new Set(); // callbacks
Expand DownExpand Up@@ -82,6 +83,32 @@ class DaemonClient {
}

async startDaemon() {
// Phase 4(rant #12 §4):打包模式直接 spawn 捆绑 emrgd 可执行文件(脚本内部
// exec python -m emrg.server);源码模式保持 python -m emrg.server。
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
stdio: "ignore",
detached: true,
};
// R36/R66:Windows .cmd 需 shell:true(非 PE),windowsHide 防黑窗闪烁;
// Node shell 模式自动给含空格的用户名加引号(R92b)。
if (process.platform === "win32") {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
const deadline = Date.now() + SPAWN_WAIT_MS;
while (Date.now() < deadline) {
if (await this.isRunning(500)) return child;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("emrgd failed to start within timeout");
}
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
Expand All@@ -102,6 +129,14 @@ class DaemonClient {
throw new Error("emrgd failed to start within timeout");
}

_findDaemonExecutable() {
// Phase 4(rant #12 §4 R7):打包模式定位捆绑 emrgd。
// Windows: ~/.emrg/install/bin/emrgd.cmd;POSIX: ~/.emrg/install/bin/emrgd。
const bin = path.join(os.homedir(), ".emrg", "install", "bin");
const name = process.platform === "win32" ? "emrgd.cmd" : "emrgd";
return path.join(bin, name);
}

_findPython() {
// G59/G61/G126:优先项目 .venv,其次 PATH python3/python
const root = path.resolve(__dirname, "..", "..");
Expand Down
80 changes: 79 additions & 1 deletion emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,9 +105,51 @@ function main() {

function configPath() { return path.join(os.homedir(), ".emrg", "config.toml"); }

// Phase 4(rant #12 §4 R108):打包模式无 .venv/PATH python → 内联模板直接生成
// (与 emrg/config.py ensure_config() 105-130 行同源);源码模式复用 ensure_config()。
const CONFIG_TEMPLATE = `[llm]
# OpenAI-compatible API endpoint
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-chat"
max_tokens = 8192
temperature = 0.7
context_window = 131072
auto_compact_threshold = 0.0
# vision: set to true if model supports OpenAI vision API (image_url content type)
vision = false

# Additional models for /model switching (optional — add or remove as needed)
# model: API model name (optional — defaults to name if not set)
[[llm.models]]
name = "deepseek-v3"
model = "deepseek-chat"
context_window = 131072
vision = false

[[llm.models]]
name = "deepseek-r1"
model = "deepseek-reasoner"
context_window = 65536
vision = false
`;

function ensureConfigTemplate() {
// G116:优先复用 ensure_config() 生成官方模板(含 [[llm.models]] 预置)
return new Promise((resolve) => {
if (app.isPackaged) {
if (!fs.existsSync(configPath())) {
try {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), CONFIG_TEMPLATE, { mode: 0o600 });
} catch (e) {
resolve("");
return;
}
}
resolve(fs.readFileSync(configPath(), "utf8"));
return;
}
const python = client?._findPython() || "python3";
const child = spawn(python, ["-c", "from emrg.config import ensure_config; ensure_config()"], {
cwd: projectDir,
Expand DownExpand Up@@ -335,7 +377,9 @@ function main() {

async function ensureConnected() {
if (!client) {
client = new DaemonClient({ projectDir, logger });
// Phase 4(rant #12 §4 R7):打包模式传 app.isPackaged → daemon_client 走
// 捆绑 emrgd 分支(_findDaemonExecutable)。
client = new DaemonClient({ projectDir, logger, isPackaged: app.isPackaged });
// G122:message_delta 16ms 批量推送
let deltaBuf = [];
let deltaTimer = null;
Expand DownExpand Up@@ -461,9 +505,43 @@ function main() {
}
});

// ── Phase 4 AppImage 自解压(rant #12 §5 R65/R88/R93)──────────
// AppImage 是自解压安装器:首次运行把 resourcesPath/runtime 复制到
// ~/.emrg/install/(稳定路径,非 AppImage 挂载),并建 ~/.local/bin/emrg 软链。
// 仅 Linux(process.env.APPIMAGE 存在);macOS/Windows 由安装器直接放置。
function ensureAppImageExtracted() {
if (process.platform !== "linux" || !process.env.APPIMAGE) return;
const home = os.homedir();
const installBin = path.join(home, ".emrg", "install", "bin");
if (fs.existsSync(installBin)) return; // 已解压
const runtimeSrc = path.join(process.resourcesPath, "runtime");
if (!fs.existsSync(runtimeSrc)) return;
try {
// R88:先 mkdir ~/.local/bin 否则 symlinkSync ENOENT
fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true });
// R93:250MB 复制期间显示"正在安装 EMRG..."提示防误判卡死
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: true } });
}
fs.cpSync(runtimeSrc, path.join(home, ".emrg", "install"), { recursive: true });
const link = path.join(home, ".local", "bin", "emrg");
if (!fs.existsSync(link)) {
fs.symlinkSync(path.join(home, ".emrg", "install", "bin", "emrg"), link, "file");
}
logger.info("[gui] AppImage runtime extracted to ~/.emrg/install/");
} catch (e) {
logger.error("[gui] AppImage extraction failed (retry on next launch)", e);
} finally {
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: false } });
}
}
}

app.whenReady().then(() => {
registerIpc();
createWindow();
ensureAppImageExtracted(); // R65:自解压归 main.js(electron-builder AppRun 无官方自定义)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Expand Down
52 changes: 52 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,58 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
const expected = path.join(os.homedir(), ".emrg", "install", "bin", process.platform === "win32" ? "emrgd.cmd" : "emrgd");
assert.strictEqual(exe, expected);
assert.ok(exe.includes(path.join(".emrg", "install", "bin")), `exe=${exe}`);
});

test("Phase4: isPackaged startDaemon 走捆绑 emrgd 分支(非 python -m)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
// _findPython 只存在于源码分支;打包分支调用 _findDaemonExecutable。
const exe = client._findDaemonExecutable();
assert.ok(exe.endsWith("emrgd") || exe.endsWith("emrgd.cmd"), `exe=${exe}`);
// 打包分支绝不能触达 _findPython:置为抛错,若被调用测试即失败。
client._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
// _findDaemonExecutable 返回真实路径;真实 spawn 会执行该文件——用一个
// 必然超时失败但能证明走了打包分支的方式:spawn 的 emrgd 不存在时 error 事件。
// 更稳:stub isRunning 恒 false + 缩短等待,捕获 startDaemon 抛的超时错,
// 说明它没有走 _findPython(否则直接抛上面的错)。
client.isRunning = async () => false;
const realSpawn = require("child_process").spawn;
const childProcess = require("child_process");
// daemon_client 顶层解构 { spawn }——只能替换 require.cache 中的模块导出。
// 简化:monkey-patch childProcess.spawn 无法生效,改用 spy 模块缓存。
// 这里直接验证:打包分支下 spawn 的 cmd 是 emrgd(通过临时替换模块再 require)。
const cacheKey = require.resolve("../daemon_client.js");
const originalModule = require.cache[cacheKey];
delete require.cache[cacheKey];
const origSpawn = require("child_process").spawn;
let captured = null;
require("child_process").spawn = (cmd, args, opts) => {
captured = { cmd, args, opts };
return { unref() {}, pid: 1234 };
};
let DaemonClientPackaged;
try {
DaemonClientPackaged = require("../daemon_client.js").DaemonClient;
const c2 = new DaemonClientPackaged({ projectDir: tmpHome, isPackaged: true });
c2._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
c2.isRunning = async () => true;
const child = await c2.startDaemon();
assert.strictEqual(child.pid, 1234);
assert.ok(captured, "spawn should be called");
assert.ok(String(captured.cmd).endsWith("emrgd") || String(captured.cmd).endsWith("emrgd.cmd"), `cmd=${captured.cmd}`);
assert.deepStrictEqual(captured.args, []);
assert.strictEqual(captured.opts.detached, true);
} finally {
require("child_process").spawn = origSpawn;
require.cache[cacheKey] = originalModule;
}
});

test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件重拉", async () => {
const client = new DaemonClient({ projectDir: tmpHome });
let respawned = false;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,10 +47,11 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS } = {}) {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false } = {}) {
this.projectDir = projectDir;
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
this.ws = null;
this.connected = false;
this._events = new Set(); // callbacks
Expand DownExpand Up@@ -82,6 +83,32 @@ class DaemonClient {
}

async startDaemon() {
// Phase 4(rant #12 §4):打包模式直接 spawn 捆绑 emrgd 可执行文件(脚本内部
// exec python -m emrg.server);源码模式保持 python -m emrg.server。
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
stdio: "ignore",
detached: true,
};
// R36/R66:Windows .cmd 需 shell:true(非 PE),windowsHide 防黑窗闪烁;
// Node shell 模式自动给含空格的用户名加引号(R92b)。
if (process.platform === "win32") {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
const deadline = Date.now() + SPAWN_WAIT_MS;
while (Date.now() < deadline) {
if (await this.isRunning(500)) return child;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("emrgd failed to start within timeout");
}
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
Expand All@@ -102,6 +129,14 @@ class DaemonClient {
throw new Error("emrgd failed to start within timeout");
}

_findDaemonExecutable() {
// Phase 4(rant #12 §4 R7):打包模式定位捆绑 emrgd。
// Windows: ~/.emrg/install/bin/emrgd.cmd;POSIX: ~/.emrg/install/bin/emrgd。
const bin = path.join(os.homedir(), ".emrg", "install", "bin");
const name = process.platform === "win32" ? "emrgd.cmd" : "emrgd";
return path.join(bin, name);
}

_findPython() {
// G59/G61/G126:优先项目 .venv,其次 PATH python3/python
const root = path.resolve(__dirname, "..", "..");
Expand Down
80 changes: 79 additions & 1 deletion emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,9 +105,51 @@ function main() {

function configPath() { return path.join(os.homedir(), ".emrg", "config.toml"); }

// Phase 4(rant #12 §4 R108):打包模式无 .venv/PATH python → 内联模板直接生成
// (与 emrg/config.py ensure_config() 105-130 行同源);源码模式复用 ensure_config()。
const CONFIG_TEMPLATE = `[llm]
# OpenAI-compatible API endpoint
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-chat"
max_tokens = 8192
temperature = 0.7
context_window = 131072
auto_compact_threshold = 0.0
# vision: set to true if model supports OpenAI vision API (image_url content type)
vision = false

# Additional models for /model switching (optional — add or remove as needed)
# model: API model name (optional — defaults to name if not set)
[[llm.models]]
name = "deepseek-v3"
model = "deepseek-chat"
context_window = 131072
vision = false

[[llm.models]]
name = "deepseek-r1"
model = "deepseek-reasoner"
context_window = 65536
vision = false
`;

function ensureConfigTemplate() {
// G116:优先复用 ensure_config() 生成官方模板(含 [[llm.models]] 预置)
return new Promise((resolve) => {
if (app.isPackaged) {
if (!fs.existsSync(configPath())) {
try {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), CONFIG_TEMPLATE, { mode: 0o600 });
} catch (e) {
resolve("");
return;
}
}
resolve(fs.readFileSync(configPath(), "utf8"));
return;
}
const python = client?._findPython() || "python3";
const child = spawn(python, ["-c", "from emrg.config import ensure_config; ensure_config()"], {
cwd: projectDir,
Expand DownExpand Up@@ -335,7 +377,9 @@ function main() {

async function ensureConnected() {
if (!client) {
client = new DaemonClient({ projectDir, logger });
// Phase 4(rant #12 §4 R7):打包模式传 app.isPackaged → daemon_client 走
// 捆绑 emrgd 分支(_findDaemonExecutable)。
client = new DaemonClient({ projectDir, logger, isPackaged: app.isPackaged });
// G122:message_delta 16ms 批量推送
let deltaBuf = [];
let deltaTimer = null;
Expand DownExpand Up@@ -461,9 +505,43 @@ function main() {
}
});

// ── Phase 4 AppImage 自解压(rant #12 §5 R65/R88/R93)──────────
// AppImage 是自解压安装器:首次运行把 resourcesPath/runtime 复制到
// ~/.emrg/install/(稳定路径,非 AppImage 挂载),并建 ~/.local/bin/emrg 软链。
// 仅 Linux(process.env.APPIMAGE 存在);macOS/Windows 由安装器直接放置。
function ensureAppImageExtracted() {
if (process.platform !== "linux" || !process.env.APPIMAGE) return;
const home = os.homedir();
const installBin = path.join(home, ".emrg", "install", "bin");
if (fs.existsSync(installBin)) return; // 已解压
const runtimeSrc = path.join(process.resourcesPath, "runtime");
if (!fs.existsSync(runtimeSrc)) return;
try {
// R88:先 mkdir ~/.local/bin 否则 symlinkSync ENOENT
fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true });
// R93:250MB 复制期间显示"正在安装 EMRG..."提示防误判卡死
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: true } });
}
fs.cpSync(runtimeSrc, path.join(home, ".emrg", "install"), { recursive: true });
const link = path.join(home, ".local", "bin", "emrg");
if (!fs.existsSync(link)) {
fs.symlinkSync(path.join(home, ".emrg", "install", "bin", "emrg"), link, "file");
}
logger.info("[gui] AppImage runtime extracted to ~/.emrg/install/");
} catch (e) {
logger.error("[gui] AppImage extraction failed (retry on next launch)", e);
} finally {
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: false } });
}
}
}

app.whenReady().then(() => {
registerIpc();
createWindow();
ensureAppImageExtracted(); // R65:自解压归 main.js(electron-builder AppRun 无官方自定义)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Expand Down
52 changes: 52 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,58 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
const expected = path.join(os.homedir(), ".emrg", "install", "bin", process.platform === "win32" ? "emrgd.cmd" : "emrgd");
assert.strictEqual(exe, expected);
assert.ok(exe.includes(path.join(".emrg", "install", "bin")), `exe=${exe}`);
});

test("Phase4: isPackaged startDaemon 走捆绑 emrgd 分支(非 python -m)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
// _findPython 只存在于源码分支;打包分支调用 _findDaemonExecutable。
const exe = client._findDaemonExecutable();
assert.ok(exe.endsWith("emrgd") || exe.endsWith("emrgd.cmd"), `exe=${exe}`);
// 打包分支绝不能触达 _findPython:置为抛错,若被调用测试即失败。
client._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
// _findDaemonExecutable 返回真实路径;真实 spawn 会执行该文件——用一个
// 必然超时失败但能证明走了打包分支的方式:spawn 的 emrgd 不存在时 error 事件。
// 更稳:stub isRunning 恒 false + 缩短等待,捕获 startDaemon 抛的超时错,
// 说明它没有走 _findPython(否则直接抛上面的错)。
client.isRunning = async () => false;
const realSpawn = require("child_process").spawn;
const childProcess = require("child_process");
// daemon_client 顶层解构 { spawn }——只能替换 require.cache 中的模块导出。
// 简化:monkey-patch childProcess.spawn 无法生效,改用 spy 模块缓存。
// 这里直接验证:打包分支下 spawn 的 cmd 是 emrgd(通过临时替换模块再 require)。
const cacheKey = require.resolve("../daemon_client.js");
const originalModule = require.cache[cacheKey];
delete require.cache[cacheKey];
const origSpawn = require("child_process").spawn;
let captured = null;
require("child_process").spawn = (cmd, args, opts) => {
captured = { cmd, args, opts };
return { unref() {}, pid: 1234 };
};
let DaemonClientPackaged;
try {
DaemonClientPackaged = require("../daemon_client.js").DaemonClient;
const c2 = new DaemonClientPackaged({ projectDir: tmpHome, isPackaged: true });
c2._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
c2.isRunning = async () => true;
const child = await c2.startDaemon();
assert.strictEqual(child.pid, 1234);
assert.ok(captured, "spawn should be called");
assert.ok(String(captured.cmd).endsWith("emrgd") || String(captured.cmd).endsWith("emrgd.cmd"), `cmd=${captured.cmd}`);
assert.deepStrictEqual(captured.args, []);
assert.strictEqual(captured.opts.detached, true);
} finally {
require("child_process").spawn = origSpawn;
require.cache[cacheKey] = originalModule;
}
});

test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件重拉", async () => {
const client = new DaemonClient({ projectDir: tmpHome });
let respawned = false;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,10 +47,11 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS } = {}) {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false } = {}) {
this.projectDir = projectDir;
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
this.ws = null;
this.connected = false;
this._events = new Set(); // callbacks
Expand DownExpand Up@@ -82,6 +83,32 @@ class DaemonClient {
}

async startDaemon() {
// Phase 4(rant #12 §4):打包模式直接 spawn 捆绑 emrgd 可执行文件(脚本内部
// exec python -m emrg.server);源码模式保持 python -m emrg.server。
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
stdio: "ignore",
detached: true,
};
// R36/R66:Windows .cmd 需 shell:true(非 PE),windowsHide 防黑窗闪烁;
// Node shell 模式自动给含空格的用户名加引号(R92b)。
if (process.platform === "win32") {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
const deadline = Date.now() + SPAWN_WAIT_MS;
while (Date.now() < deadline) {
if (await this.isRunning(500)) return child;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("emrgd failed to start within timeout");
}
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
Expand All@@ -102,6 +129,14 @@ class DaemonClient {
throw new Error("emrgd failed to start within timeout");
}

_findDaemonExecutable() {
// Phase 4(rant #12 §4 R7):打包模式定位捆绑 emrgd。
// Windows: ~/.emrg/install/bin/emrgd.cmd;POSIX: ~/.emrg/install/bin/emrgd。
const bin = path.join(os.homedir(), ".emrg", "install", "bin");
const name = process.platform === "win32" ? "emrgd.cmd" : "emrgd";
return path.join(bin, name);
}

_findPython() {
// G59/G61/G126:优先项目 .venv,其次 PATH python3/python
const root = path.resolve(__dirname, "..", "..");
Expand Down
80 changes: 79 additions & 1 deletion emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,9 +105,51 @@ function main() {

function configPath() { return path.join(os.homedir(), ".emrg", "config.toml"); }

// Phase 4(rant #12 §4 R108):打包模式无 .venv/PATH python → 内联模板直接生成
// (与 emrg/config.py ensure_config() 105-130 行同源);源码模式复用 ensure_config()。
const CONFIG_TEMPLATE = `[llm]
# OpenAI-compatible API endpoint
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-chat"
max_tokens = 8192
temperature = 0.7
context_window = 131072
auto_compact_threshold = 0.0
# vision: set to true if model supports OpenAI vision API (image_url content type)
vision = false

# Additional models for /model switching (optional — add or remove as needed)
# model: API model name (optional — defaults to name if not set)
[[llm.models]]
name = "deepseek-v3"
model = "deepseek-chat"
context_window = 131072
vision = false

[[llm.models]]
name = "deepseek-r1"
model = "deepseek-reasoner"
context_window = 65536
vision = false
`;

function ensureConfigTemplate() {
// G116:优先复用 ensure_config() 生成官方模板(含 [[llm.models]] 预置)
return new Promise((resolve) => {
if (app.isPackaged) {
if (!fs.existsSync(configPath())) {
try {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), CONFIG_TEMPLATE, { mode: 0o600 });
} catch (e) {
resolve("");
return;
}
}
resolve(fs.readFileSync(configPath(), "utf8"));
return;
}
const python = client?._findPython() || "python3";
const child = spawn(python, ["-c", "from emrg.config import ensure_config; ensure_config()"], {
cwd: projectDir,
Expand DownExpand Up@@ -335,7 +377,9 @@ function main() {

async function ensureConnected() {
if (!client) {
client = new DaemonClient({ projectDir, logger });
// Phase 4(rant #12 §4 R7):打包模式传 app.isPackaged → daemon_client 走
// 捆绑 emrgd 分支(_findDaemonExecutable)。
client = new DaemonClient({ projectDir, logger, isPackaged: app.isPackaged });
// G122:message_delta 16ms 批量推送
let deltaBuf = [];
let deltaTimer = null;
Expand DownExpand Up@@ -461,9 +505,43 @@ function main() {
}
});

// ── Phase 4 AppImage 自解压(rant #12 §5 R65/R88/R93)──────────
// AppImage 是自解压安装器:首次运行把 resourcesPath/runtime 复制到
// ~/.emrg/install/(稳定路径,非 AppImage 挂载),并建 ~/.local/bin/emrg 软链。
// 仅 Linux(process.env.APPIMAGE 存在);macOS/Windows 由安装器直接放置。
function ensureAppImageExtracted() {
if (process.platform !== "linux" || !process.env.APPIMAGE) return;
const home = os.homedir();
const installBin = path.join(home, ".emrg", "install", "bin");
if (fs.existsSync(installBin)) return; // 已解压
const runtimeSrc = path.join(process.resourcesPath, "runtime");
if (!fs.existsSync(runtimeSrc)) return;
try {
// R88:先 mkdir ~/.local/bin 否则 symlinkSync ENOENT
fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true });
// R93:250MB 复制期间显示"正在安装 EMRG..."提示防误判卡死
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: true } });
}
fs.cpSync(runtimeSrc, path.join(home, ".emrg", "install"), { recursive: true });
const link = path.join(home, ".local", "bin", "emrg");
if (!fs.existsSync(link)) {
fs.symlinkSync(path.join(home, ".emrg", "install", "bin", "emrg"), link, "file");
}
logger.info("[gui] AppImage runtime extracted to ~/.emrg/install/");
} catch (e) {
logger.error("[gui] AppImage extraction failed (retry on next launch)", e);
} finally {
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: false } });
}
}
}

app.whenReady().then(() => {
registerIpc();
createWindow();
ensureAppImageExtracted(); // R65:自解压归 main.js(electron-builder AppRun 无官方自定义)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Expand Down
52 changes: 52 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,58 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
const expected = path.join(os.homedir(), ".emrg", "install", "bin", process.platform === "win32" ? "emrgd.cmd" : "emrgd");
assert.strictEqual(exe, expected);
assert.ok(exe.includes(path.join(".emrg", "install", "bin")), `exe=${exe}`);
});

test("Phase4: isPackaged startDaemon 走捆绑 emrgd 分支(非 python -m)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
// _findPython 只存在于源码分支;打包分支调用 _findDaemonExecutable。
const exe = client._findDaemonExecutable();
assert.ok(exe.endsWith("emrgd") || exe.endsWith("emrgd.cmd"), `exe=${exe}`);
// 打包分支绝不能触达 _findPython:置为抛错,若被调用测试即失败。
client._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
// _findDaemonExecutable 返回真实路径;真实 spawn 会执行该文件——用一个
// 必然超时失败但能证明走了打包分支的方式:spawn 的 emrgd 不存在时 error 事件。
// 更稳:stub isRunning 恒 false + 缩短等待,捕获 startDaemon 抛的超时错,
// 说明它没有走 _findPython(否则直接抛上面的错)。
client.isRunning = async () => false;
const realSpawn = require("child_process").spawn;
const childProcess = require("child_process");
// daemon_client 顶层解构 { spawn }——只能替换 require.cache 中的模块导出。
// 简化:monkey-patch childProcess.spawn 无法生效,改用 spy 模块缓存。
// 这里直接验证:打包分支下 spawn 的 cmd 是 emrgd(通过临时替换模块再 require)。
const cacheKey = require.resolve("../daemon_client.js");
const originalModule = require.cache[cacheKey];
delete require.cache[cacheKey];
const origSpawn = require("child_process").spawn;
let captured = null;
require("child_process").spawn = (cmd, args, opts) => {
captured = { cmd, args, opts };
return { unref() {}, pid: 1234 };
};
let DaemonClientPackaged;
try {
DaemonClientPackaged = require("../daemon_client.js").DaemonClient;
const c2 = new DaemonClientPackaged({ projectDir: tmpHome, isPackaged: true });
c2._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
c2.isRunning = async () => true;
const child = await c2.startDaemon();
assert.strictEqual(child.pid, 1234);
assert.ok(captured, "spawn should be called");
assert.ok(String(captured.cmd).endsWith("emrgd") || String(captured.cmd).endsWith("emrgd.cmd"), `cmd=${captured.cmd}`);
assert.deepStrictEqual(captured.args, []);
assert.strictEqual(captured.opts.detached, true);
} finally {
require("child_process").spawn = origSpawn;
require.cache[cacheKey] = originalModule;
}
});

test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件重拉", async () => {
const client = new DaemonClient({ projectDir: tmpHome });
let respawned = false;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,10 +47,11 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS } = {}) {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false } = {}) {
this.projectDir = projectDir;
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
this.ws = null;
this.connected = false;
this._events = new Set(); // callbacks
Expand DownExpand Up@@ -82,6 +83,32 @@ class DaemonClient {
}

async startDaemon() {
// Phase 4(rant #12 §4):打包模式直接 spawn 捆绑 emrgd 可执行文件(脚本内部
// exec python -m emrg.server);源码模式保持 python -m emrg.server。
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
stdio: "ignore",
detached: true,
};
// R36/R66:Windows .cmd 需 shell:true(非 PE),windowsHide 防黑窗闪烁;
// Node shell 模式自动给含空格的用户名加引号(R92b)。
if (process.platform === "win32") {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
const deadline = Date.now() + SPAWN_WAIT_MS;
while (Date.now() < deadline) {
if (await this.isRunning(500)) return child;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("emrgd failed to start within timeout");
}
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
Expand All@@ -102,6 +129,14 @@ class DaemonClient {
throw new Error("emrgd failed to start within timeout");
}

_findDaemonExecutable() {
// Phase 4(rant #12 §4 R7):打包模式定位捆绑 emrgd。
// Windows: ~/.emrg/install/bin/emrgd.cmd;POSIX: ~/.emrg/install/bin/emrgd。
const bin = path.join(os.homedir(), ".emrg", "install", "bin");
const name = process.platform === "win32" ? "emrgd.cmd" : "emrgd";
return path.join(bin, name);
}

_findPython() {
// G59/G61/G126:优先项目 .venv,其次 PATH python3/python
const root = path.resolve(__dirname, "..", "..");
Expand Down
80 changes: 79 additions & 1 deletion emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,9 +105,51 @@ function main() {

function configPath() { return path.join(os.homedir(), ".emrg", "config.toml"); }

// Phase 4(rant #12 §4 R108):打包模式无 .venv/PATH python → 内联模板直接生成
// (与 emrg/config.py ensure_config() 105-130 行同源);源码模式复用 ensure_config()。
const CONFIG_TEMPLATE = `[llm]
# OpenAI-compatible API endpoint
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-chat"
max_tokens = 8192
temperature = 0.7
context_window = 131072
auto_compact_threshold = 0.0
# vision: set to true if model supports OpenAI vision API (image_url content type)
vision = false

# Additional models for /model switching (optional — add or remove as needed)
# model: API model name (optional — defaults to name if not set)
[[llm.models]]
name = "deepseek-v3"
model = "deepseek-chat"
context_window = 131072
vision = false

[[llm.models]]
name = "deepseek-r1"
model = "deepseek-reasoner"
context_window = 65536
vision = false
`;

function ensureConfigTemplate() {
// G116:优先复用 ensure_config() 生成官方模板(含 [[llm.models]] 预置)
return new Promise((resolve) => {
if (app.isPackaged) {
if (!fs.existsSync(configPath())) {
try {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), CONFIG_TEMPLATE, { mode: 0o600 });
} catch (e) {
resolve("");
return;
}
}
resolve(fs.readFileSync(configPath(), "utf8"));
return;
}
const python = client?._findPython() || "python3";
const child = spawn(python, ["-c", "from emrg.config import ensure_config; ensure_config()"], {
cwd: projectDir,
Expand DownExpand Up@@ -335,7 +377,9 @@ function main() {

async function ensureConnected() {
if (!client) {
client = new DaemonClient({ projectDir, logger });
// Phase 4(rant #12 §4 R7):打包模式传 app.isPackaged → daemon_client 走
// 捆绑 emrgd 分支(_findDaemonExecutable)。
client = new DaemonClient({ projectDir, logger, isPackaged: app.isPackaged });
// G122:message_delta 16ms 批量推送
let deltaBuf = [];
let deltaTimer = null;
Expand DownExpand Up@@ -461,9 +505,43 @@ function main() {
}
});

// ── Phase 4 AppImage 自解压(rant #12 §5 R65/R88/R93)──────────
// AppImage 是自解压安装器:首次运行把 resourcesPath/runtime 复制到
// ~/.emrg/install/(稳定路径,非 AppImage 挂载),并建 ~/.local/bin/emrg 软链。
// 仅 Linux(process.env.APPIMAGE 存在);macOS/Windows 由安装器直接放置。
function ensureAppImageExtracted() {
if (process.platform !== "linux" || !process.env.APPIMAGE) return;
const home = os.homedir();
const installBin = path.join(home, ".emrg", "install", "bin");
if (fs.existsSync(installBin)) return; // 已解压
const runtimeSrc = path.join(process.resourcesPath, "runtime");
if (!fs.existsSync(runtimeSrc)) return;
try {
// R88:先 mkdir ~/.local/bin 否则 symlinkSync ENOENT
fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true });
// R93:250MB 复制期间显示"正在安装 EMRG..."提示防误判卡死
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: true } });
}
fs.cpSync(runtimeSrc, path.join(home, ".emrg", "install"), { recursive: true });
const link = path.join(home, ".local", "bin", "emrg");
if (!fs.existsSync(link)) {
fs.symlinkSync(path.join(home, ".emrg", "install", "bin", "emrg"), link, "file");
}
logger.info("[gui] AppImage runtime extracted to ~/.emrg/install/");
} catch (e) {
logger.error("[gui] AppImage extraction failed (retry on next launch)", e);
} finally {
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: false } });
}
}
}

app.whenReady().then(() => {
registerIpc();
createWindow();
ensureAppImageExtracted(); // R65:自解压归 main.js(electron-builder AppRun 无官方自定义)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Expand Down
52 changes: 52 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,58 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
const expected = path.join(os.homedir(), ".emrg", "install", "bin", process.platform === "win32" ? "emrgd.cmd" : "emrgd");
assert.strictEqual(exe, expected);
assert.ok(exe.includes(path.join(".emrg", "install", "bin")), `exe=${exe}`);
});

test("Phase4: isPackaged startDaemon 走捆绑 emrgd 分支(非 python -m)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
// _findPython 只存在于源码分支;打包分支调用 _findDaemonExecutable。
const exe = client._findDaemonExecutable();
assert.ok(exe.endsWith("emrgd") || exe.endsWith("emrgd.cmd"), `exe=${exe}`);
// 打包分支绝不能触达 _findPython:置为抛错,若被调用测试即失败。
client._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
// _findDaemonExecutable 返回真实路径;真实 spawn 会执行该文件——用一个
// 必然超时失败但能证明走了打包分支的方式:spawn 的 emrgd 不存在时 error 事件。
// 更稳:stub isRunning 恒 false + 缩短等待,捕获 startDaemon 抛的超时错,
// 说明它没有走 _findPython(否则直接抛上面的错)。
client.isRunning = async () => false;
const realSpawn = require("child_process").spawn;
const childProcess = require("child_process");
// daemon_client 顶层解构 { spawn }——只能替换 require.cache 中的模块导出。
// 简化:monkey-patch childProcess.spawn 无法生效,改用 spy 模块缓存。
// 这里直接验证:打包分支下 spawn 的 cmd 是 emrgd(通过临时替换模块再 require)。
const cacheKey = require.resolve("../daemon_client.js");
const originalModule = require.cache[cacheKey];
delete require.cache[cacheKey];
const origSpawn = require("child_process").spawn;
let captured = null;
require("child_process").spawn = (cmd, args, opts) => {
captured = { cmd, args, opts };
return { unref() {}, pid: 1234 };
};
let DaemonClientPackaged;
try {
DaemonClientPackaged = require("../daemon_client.js").DaemonClient;
const c2 = new DaemonClientPackaged({ projectDir: tmpHome, isPackaged: true });
c2._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
c2.isRunning = async () => true;
const child = await c2.startDaemon();
assert.strictEqual(child.pid, 1234);
assert.ok(captured, "spawn should be called");
assert.ok(String(captured.cmd).endsWith("emrgd") || String(captured.cmd).endsWith("emrgd.cmd"), `cmd=${captured.cmd}`);
assert.deepStrictEqual(captured.args, []);
assert.strictEqual(captured.opts.detached, true);
} finally {
require("child_process").spawn = origSpawn;
require.cache[cacheKey] = originalModule;
}
});

test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件重拉", async () => {
const client = new DaemonClient({ projectDir: tmpHome });
let respawned = false;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,10 +47,11 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS } = {}) {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false } = {}) {
this.projectDir = projectDir;
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
this.ws = null;
this.connected = false;
this._events = new Set(); // callbacks
Expand DownExpand Up@@ -82,6 +83,32 @@ class DaemonClient {
}

async startDaemon() {
// Phase 4(rant #12 §4):打包模式直接 spawn 捆绑 emrgd 可执行文件(脚本内部
// exec python -m emrg.server);源码模式保持 python -m emrg.server。
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
stdio: "ignore",
detached: true,
};
// R36/R66:Windows .cmd 需 shell:true(非 PE),windowsHide 防黑窗闪烁;
// Node shell 模式自动给含空格的用户名加引号(R92b)。
if (process.platform === "win32") {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
const deadline = Date.now() + SPAWN_WAIT_MS;
while (Date.now() < deadline) {
if (await this.isRunning(500)) return child;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("emrgd failed to start within timeout");
}
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
Expand All@@ -102,6 +129,14 @@ class DaemonClient {
throw new Error("emrgd failed to start within timeout");
}

_findDaemonExecutable() {
// Phase 4(rant #12 §4 R7):打包模式定位捆绑 emrgd。
// Windows: ~/.emrg/install/bin/emrgd.cmd;POSIX: ~/.emrg/install/bin/emrgd。
const bin = path.join(os.homedir(), ".emrg", "install", "bin");
const name = process.platform === "win32" ? "emrgd.cmd" : "emrgd";
return path.join(bin, name);
}

_findPython() {
// G59/G61/G126:优先项目 .venv,其次 PATH python3/python
const root = path.resolve(__dirname, "..", "..");
Expand Down
80 changes: 79 additions & 1 deletion emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,9 +105,51 @@ function main() {

function configPath() { return path.join(os.homedir(), ".emrg", "config.toml"); }

// Phase 4(rant #12 §4 R108):打包模式无 .venv/PATH python → 内联模板直接生成
// (与 emrg/config.py ensure_config() 105-130 行同源);源码模式复用 ensure_config()。
const CONFIG_TEMPLATE = `[llm]
# OpenAI-compatible API endpoint
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-chat"
max_tokens = 8192
temperature = 0.7
context_window = 131072
auto_compact_threshold = 0.0
# vision: set to true if model supports OpenAI vision API (image_url content type)
vision = false

# Additional models for /model switching (optional — add or remove as needed)
# model: API model name (optional — defaults to name if not set)
[[llm.models]]
name = "deepseek-v3"
model = "deepseek-chat"
context_window = 131072
vision = false

[[llm.models]]
name = "deepseek-r1"
model = "deepseek-reasoner"
context_window = 65536
vision = false
`;

function ensureConfigTemplate() {
// G116:优先复用 ensure_config() 生成官方模板(含 [[llm.models]] 预置)
return new Promise((resolve) => {
if (app.isPackaged) {
if (!fs.existsSync(configPath())) {
try {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), CONFIG_TEMPLATE, { mode: 0o600 });
} catch (e) {
resolve("");
return;
}
}
resolve(fs.readFileSync(configPath(), "utf8"));
return;
}
const python = client?._findPython() || "python3";
const child = spawn(python, ["-c", "from emrg.config import ensure_config; ensure_config()"], {
cwd: projectDir,
Expand DownExpand Up@@ -335,7 +377,9 @@ function main() {

async function ensureConnected() {
if (!client) {
client = new DaemonClient({ projectDir, logger });
// Phase 4(rant #12 §4 R7):打包模式传 app.isPackaged → daemon_client 走
// 捆绑 emrgd 分支(_findDaemonExecutable)。
client = new DaemonClient({ projectDir, logger, isPackaged: app.isPackaged });
// G122:message_delta 16ms 批量推送
let deltaBuf = [];
let deltaTimer = null;
Expand DownExpand Up@@ -461,9 +505,43 @@ function main() {
}
});

// ── Phase 4 AppImage 自解压(rant #12 §5 R65/R88/R93)──────────
// AppImage 是自解压安装器:首次运行把 resourcesPath/runtime 复制到
// ~/.emrg/install/(稳定路径,非 AppImage 挂载),并建 ~/.local/bin/emrg 软链。
// 仅 Linux(process.env.APPIMAGE 存在);macOS/Windows 由安装器直接放置。
function ensureAppImageExtracted() {
if (process.platform !== "linux" || !process.env.APPIMAGE) return;
const home = os.homedir();
const installBin = path.join(home, ".emrg", "install", "bin");
if (fs.existsSync(installBin)) return; // 已解压
const runtimeSrc = path.join(process.resourcesPath, "runtime");
if (!fs.existsSync(runtimeSrc)) return;
try {
// R88:先 mkdir ~/.local/bin 否则 symlinkSync ENOENT
fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true });
// R93:250MB 复制期间显示"正在安装 EMRG..."提示防误判卡死
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: true } });
}
fs.cpSync(runtimeSrc, path.join(home, ".emrg", "install"), { recursive: true });
const link = path.join(home, ".local", "bin", "emrg");
if (!fs.existsSync(link)) {
fs.symlinkSync(path.join(home, ".emrg", "install", "bin", "emrg"), link, "file");
}
logger.info("[gui] AppImage runtime extracted to ~/.emrg/install/");
} catch (e) {
logger.error("[gui] AppImage extraction failed (retry on next launch)", e);
} finally {
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: false } });
}
}
}

app.whenReady().then(() => {
registerIpc();
createWindow();
ensureAppImageExtracted(); // R65:自解压归 main.js(electron-builder AppRun 无官方自定义)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Expand Down
52 changes: 52 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,58 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
const expected = path.join(os.homedir(), ".emrg", "install", "bin", process.platform === "win32" ? "emrgd.cmd" : "emrgd");
assert.strictEqual(exe, expected);
assert.ok(exe.includes(path.join(".emrg", "install", "bin")), `exe=${exe}`);
});

test("Phase4: isPackaged startDaemon 走捆绑 emrgd 分支(非 python -m)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
// _findPython 只存在于源码分支;打包分支调用 _findDaemonExecutable。
const exe = client._findDaemonExecutable();
assert.ok(exe.endsWith("emrgd") || exe.endsWith("emrgd.cmd"), `exe=${exe}`);
// 打包分支绝不能触达 _findPython:置为抛错,若被调用测试即失败。
client._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
// _findDaemonExecutable 返回真实路径;真实 spawn 会执行该文件——用一个
// 必然超时失败但能证明走了打包分支的方式:spawn 的 emrgd 不存在时 error 事件。
// 更稳:stub isRunning 恒 false + 缩短等待,捕获 startDaemon 抛的超时错,
// 说明它没有走 _findPython(否则直接抛上面的错)。
client.isRunning = async () => false;
const realSpawn = require("child_process").spawn;
const childProcess = require("child_process");
// daemon_client 顶层解构 { spawn }——只能替换 require.cache 中的模块导出。
// 简化:monkey-patch childProcess.spawn 无法生效,改用 spy 模块缓存。
// 这里直接验证:打包分支下 spawn 的 cmd 是 emrgd(通过临时替换模块再 require)。
const cacheKey = require.resolve("../daemon_client.js");
const originalModule = require.cache[cacheKey];
delete require.cache[cacheKey];
const origSpawn = require("child_process").spawn;
let captured = null;
require("child_process").spawn = (cmd, args, opts) => {
captured = { cmd, args, opts };
return { unref() {}, pid: 1234 };
};
let DaemonClientPackaged;
try {
DaemonClientPackaged = require("../daemon_client.js").DaemonClient;
const c2 = new DaemonClientPackaged({ projectDir: tmpHome, isPackaged: true });
c2._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
c2.isRunning = async () => true;
const child = await c2.startDaemon();
assert.strictEqual(child.pid, 1234);
assert.ok(captured, "spawn should be called");
assert.ok(String(captured.cmd).endsWith("emrgd") || String(captured.cmd).endsWith("emrgd.cmd"), `cmd=${captured.cmd}`);
assert.deepStrictEqual(captured.args, []);
assert.strictEqual(captured.opts.detached, true);
} finally {
require("child_process").spawn = origSpawn;
require.cache[cacheKey] = originalModule;
}
});

test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件重拉", async () => {
const client = new DaemonClient({ projectDir: tmpHome });
let respawned = false;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,10 +47,11 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS } = {}) {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false } = {}) {
this.projectDir = projectDir;
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
this.ws = null;
this.connected = false;
this._events = new Set(); // callbacks
Expand DownExpand Up@@ -82,6 +83,32 @@ class DaemonClient {
}

async startDaemon() {
// Phase 4(rant #12 §4):打包模式直接 spawn 捆绑 emrgd 可执行文件(脚本内部
// exec python -m emrg.server);源码模式保持 python -m emrg.server。
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
stdio: "ignore",
detached: true,
};
// R36/R66:Windows .cmd 需 shell:true(非 PE),windowsHide 防黑窗闪烁;
// Node shell 模式自动给含空格的用户名加引号(R92b)。
if (process.platform === "win32") {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
const deadline = Date.now() + SPAWN_WAIT_MS;
while (Date.now() < deadline) {
if (await this.isRunning(500)) return child;
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("emrgd failed to start within timeout");
}
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
Expand All@@ -102,6 +129,14 @@ class DaemonClient {
throw new Error("emrgd failed to start within timeout");
}

_findDaemonExecutable() {
// Phase 4(rant #12 §4 R7):打包模式定位捆绑 emrgd。
// Windows: ~/.emrg/install/bin/emrgd.cmd;POSIX: ~/.emrg/install/bin/emrgd。
const bin = path.join(os.homedir(), ".emrg", "install", "bin");
const name = process.platform === "win32" ? "emrgd.cmd" : "emrgd";
return path.join(bin, name);
}

_findPython() {
// G59/G61/G126:优先项目 .venv,其次 PATH python3/python
const root = path.resolve(__dirname, "..", "..");
Expand Down
80 changes: 79 additions & 1 deletion emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,9 +105,51 @@ function main() {

function configPath() { return path.join(os.homedir(), ".emrg", "config.toml"); }

// Phase 4(rant #12 §4 R108):打包模式无 .venv/PATH python → 内联模板直接生成
// (与 emrg/config.py ensure_config() 105-130 行同源);源码模式复用 ensure_config()。
const CONFIG_TEMPLATE = `[llm]
# OpenAI-compatible API endpoint
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-chat"
max_tokens = 8192
temperature = 0.7
context_window = 131072
auto_compact_threshold = 0.0
# vision: set to true if model supports OpenAI vision API (image_url content type)
vision = false

# Additional models for /model switching (optional — add or remove as needed)
# model: API model name (optional — defaults to name if not set)
[[llm.models]]
name = "deepseek-v3"
model = "deepseek-chat"
context_window = 131072
vision = false

[[llm.models]]
name = "deepseek-r1"
model = "deepseek-reasoner"
context_window = 65536
vision = false
`;

function ensureConfigTemplate() {
// G116:优先复用 ensure_config() 生成官方模板(含 [[llm.models]] 预置)
return new Promise((resolve) => {
if (app.isPackaged) {
if (!fs.existsSync(configPath())) {
try {
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
fs.writeFileSync(configPath(), CONFIG_TEMPLATE, { mode: 0o600 });
} catch (e) {
resolve("");
return;
}
}
resolve(fs.readFileSync(configPath(), "utf8"));
return;
}
const python = client?._findPython() || "python3";
const child = spawn(python, ["-c", "from emrg.config import ensure_config; ensure_config()"], {
cwd: projectDir,
Expand DownExpand Up@@ -335,7 +377,9 @@ function main() {

async function ensureConnected() {
if (!client) {
client = new DaemonClient({ projectDir, logger });
// Phase 4(rant #12 §4 R7):打包模式传 app.isPackaged → daemon_client 走
// 捆绑 emrgd 分支(_findDaemonExecutable)。
client = new DaemonClient({ projectDir, logger, isPackaged: app.isPackaged });
// G122:message_delta 16ms 批量推送
let deltaBuf = [];
let deltaTimer = null;
Expand DownExpand Up@@ -461,9 +505,43 @@ function main() {
}
});

// ── Phase 4 AppImage 自解压(rant #12 §5 R65/R88/R93)──────────
// AppImage 是自解压安装器:首次运行把 resourcesPath/runtime 复制到
// ~/.emrg/install/(稳定路径,非 AppImage 挂载),并建 ~/.local/bin/emrg 软链。
// 仅 Linux(process.env.APPIMAGE 存在);macOS/Windows 由安装器直接放置。
function ensureAppImageExtracted() {
if (process.platform !== "linux" || !process.env.APPIMAGE) return;
const home = os.homedir();
const installBin = path.join(home, ".emrg", "install", "bin");
if (fs.existsSync(installBin)) return; // 已解压
const runtimeSrc = path.join(process.resourcesPath, "runtime");
if (!fs.existsSync(runtimeSrc)) return;
try {
// R88:先 mkdir ~/.local/bin 否则 symlinkSync ENOENT
fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true });
// R93:250MB 复制期间显示"正在安装 EMRG..."提示防误判卡死
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: true } });
}
fs.cpSync(runtimeSrc, path.join(home, ".emrg", "install"), { recursive: true });
const link = path.join(home, ".local", "bin", "emrg");
if (!fs.existsSync(link)) {
fs.symlinkSync(path.join(home, ".emrg", "install", "bin", "emrg"), link, "file");
}
logger.info("[gui] AppImage runtime extracted to ~/.emrg/install/");
} catch (e) {
logger.error("[gui] AppImage extraction failed (retry on next launch)", e);
} finally {
if (win && !win.isDestroyed()) {
win.webContents.send("emrg:event", { type: "status", data: { connected: false, installing: false } });
}
}
}

app.whenReady().then(() => {
registerIpc();
createWindow();
ensureAppImageExtracted(); // R65:自解压归 main.js(electron-builder AppRun 无官方自定义)
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Expand Down
52 changes: 52 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,58 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
const expected = path.join(os.homedir(), ".emrg", "install", "bin", process.platform === "win32" ? "emrgd.cmd" : "emrgd");
assert.strictEqual(exe, expected);
assert.ok(exe.includes(path.join(".emrg", "install", "bin")), `exe=${exe}`);
});

test("Phase4: isPackaged startDaemon 走捆绑 emrgd 分支(非 python -m)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
// _findPython 只存在于源码分支;打包分支调用 _findDaemonExecutable。
const exe = client._findDaemonExecutable();
assert.ok(exe.endsWith("emrgd") || exe.endsWith("emrgd.cmd"), `exe=${exe}`);
// 打包分支绝不能触达 _findPython:置为抛错,若被调用测试即失败。
client._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
// _findDaemonExecutable 返回真实路径;真实 spawn 会执行该文件——用一个
// 必然超时失败但能证明走了打包分支的方式:spawn 的 emrgd 不存在时 error 事件。
// 更稳:stub isRunning 恒 false + 缩短等待,捕获 startDaemon 抛的超时错,
// 说明它没有走 _findPython(否则直接抛上面的错)。
client.isRunning = async () => false;
const realSpawn = require("child_process").spawn;
const childProcess = require("child_process");
// daemon_client 顶层解构 { spawn }——只能替换 require.cache 中的模块导出。
// 简化:monkey-patch childProcess.spawn 无法生效,改用 spy 模块缓存。
// 这里直接验证:打包分支下 spawn 的 cmd 是 emrgd(通过临时替换模块再 require)。
const cacheKey = require.resolve("../daemon_client.js");
const originalModule = require.cache[cacheKey];
delete require.cache[cacheKey];
const origSpawn = require("child_process").spawn;
let captured = null;
require("child_process").spawn = (cmd, args, opts) => {
captured = { cmd, args, opts };
return { unref() {}, pid: 1234 };
};
let DaemonClientPackaged;
try {
DaemonClientPackaged = require("../daemon_client.js").DaemonClient;
const c2 = new DaemonClientPackaged({ projectDir: tmpHome, isPackaged: true });
c2._findPython = () => { throw new Error("_findPython must not be called in packaged mode"); };
c2.isRunning = async () => true;
const child = await c2.startDaemon();
assert.strictEqual(child.pid, 1234);
assert.ok(captured, "spawn should be called");
assert.ok(String(captured.cmd).endsWith("emrgd") || String(captured.cmd).endsWith("emrgd.cmd"), `cmd=${captured.cmd}`);
assert.deepStrictEqual(captured.args, []);
assert.strictEqual(captured.opts.detached, true);
} finally {
require("child_process").spawn = origSpawn;
require.cache[cacheKey] = originalModule;
}
});

test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件重拉", async () => {
const client = new DaemonClient({ projectDir: tmpHome });
let respawned = false;
Expand Down
Loading