From 4af3c7f672049d6ac7ecc25311e88de7a1bb1a21 Mon Sep 17 00:00:00 2001
From: imcodes-win
Date: Mon, 6 Apr 2026 04:19:17 +0800
Subject: [PATCH 01/57] fix(windows): use absolute cmd.exe path in upgrade
spawn and startup shortcut
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The daemon runs from a restricted environment (Startup shortcut / schtask)
where bare 'cmd.exe' / 'cmd' fails CreateProcess PATH resolution.
- command-handler.ts: upgrade batch spawn now uses COMSPEC absolute path
- windows-daemon.ts: tryStartStartupShortcut uses COMSPEC absolute path
- This was the root cause of empty upgrade logs — the batch never executed
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/daemon/command-handler.ts | 4 +++-
src/util/windows-daemon.ts | 3 ++-
test/util/windows-daemon.test.ts | 10 ++++------
3 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/daemon/command-handler.ts b/src/daemon/command-handler.ts
index 6870c494..cd2c5378 100644
--- a/src/daemon/command-handler.ts
+++ b/src/daemon/command-handler.ts
@@ -2132,7 +2132,9 @@ launchctl load -w "${plist}"`;
writeFileSync(batchPath, batch);
- const child = spawn('cmd.exe', ['/c', batchPath], {
+ // Use absolute path — bare 'cmd.exe' fails in restricted daemon environments
+ const cmdExe = process.env.COMSPEC || `${process.env.SystemRoot || 'C:\\Windows'}\\system32\\cmd.exe`;
+ const child = spawn(cmdExe, ['/c', batchPath], {
detached: true,
stdio: 'ignore',
windowsHide: true,
diff --git a/src/util/windows-daemon.ts b/src/util/windows-daemon.ts
index 257adf9f..7487998c 100644
--- a/src/util/windows-daemon.ts
+++ b/src/util/windows-daemon.ts
@@ -91,7 +91,8 @@ function tryStartStartupShortcut(): boolean {
'imcodes-daemon.cmd',
);
if (!existsSync(startupCmd)) return false;
- spawn('cmd', ['/c', startupCmd], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
+ const cmdExe = process.env.COMSPEC || `${process.env.SystemRoot || 'C:\\Windows'}\\system32\\cmd.exe`;
+ spawn(cmdExe, ['/c', startupCmd], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
return true;
}
diff --git a/test/util/windows-daemon.test.ts b/test/util/windows-daemon.test.ts
index 6e5c363a..77d14d13 100644
--- a/test/util/windows-daemon.test.ts
+++ b/test/util/windows-daemon.test.ts
@@ -126,12 +126,10 @@ describe('restartWindowsDaemon', () => {
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(true);
- expect(state.spawnCalls).toEqual([
- {
- cmd: 'cmd',
- args: ['/c', 'C:\\Users\\tester\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\imcodes-daemon.cmd'],
- },
- ]);
+ expect(state.spawnCalls).toHaveLength(1);
+ // cmd resolved via COMSPEC — match any path ending in cmd.exe
+ expect(state.spawnCalls[0].cmd).toMatch(/cmd(\.exe)?$/i);
+ expect(state.spawnCalls[0].args).toEqual(['/c', 'C:\\Users\\tester\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\imcodes-daemon.cmd']);
});
it('force-kills daemon if tree-kill misses it', async () => {
From 2cee873b242587d238bc37335e15c59e0d5865c4 Mon Sep 17 00:00:00 2001
From: imcodes-win
Date: Mon, 6 Apr 2026 09:57:24 +0800
Subject: [PATCH 02/57] fix(windows): prevent upgrade race condition and use
npm shim in watchdog
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Root cause: npm install -g deletes old package before writing new one.
The watchdog loop tried to restart the daemon during this window, hitting
MODULE_NOT_FOUND. After that the watchdog died and never recovered.
Fixes:
1. Upgrade batch now kills the entire watchdog tree BEFORE npm install,
preventing the race entirely
2. On install failure, abort paths restart the old daemon via VBS so
it's never left dead
3. After successful install, starts fresh watchdog via VBS directly
instead of calling `imcodes restart` (avoids version-coupling)
4. Watchdog.cmd now uses the npm global shim (`imcodes.cmd`) instead
of hard-coded node+script paths — always launches whatever version
is currently installed, surviving npm upgrades without repair-watchdog
5. command-handler upgrade spawn uses COMSPEC absolute path
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/daemon/command-handler.ts | 5 +--
src/util/windows-launch-artifacts.ts | 15 ++++++-
src/util/windows-upgrade-script.ts | 57 +++++++++++++++++++++---
test/util/windows-upgrade-script.test.ts | 54 +++++++++++++++++++---
4 files changed, 114 insertions(+), 17 deletions(-)
diff --git a/src/daemon/command-handler.ts b/src/daemon/command-handler.ts
index cd2c5378..4ad3d67c 100644
--- a/src/daemon/command-handler.ts
+++ b/src/daemon/command-handler.ts
@@ -2118,9 +2118,7 @@ launchctl load -w "${plist}"`;
const targetVer = targetVersion ?? 'latest';
const cleanupScript = buildWindowsCleanupScript(scriptDir);
writeFileSync(cleanupPath, cleanupScript);
- // Windows: install first while the daemon is still running. On success,
- // delegate restart to the CLI's shared watchdog logic (`imcodes restart`)
- // instead of reimplementing restart behavior in batch.
+ const vbsLauncherPath = join(homedir(), '.imcodes', 'daemon-launcher.vbs');
const batch = buildWindowsUpgradeBatch({
logFile,
scriptDir,
@@ -2128,6 +2126,7 @@ launchctl load -w "${plist}"`;
npmCmd,
pkgSpec,
targetVer,
+ vbsLauncherPath,
});
writeFileSync(batchPath, batch);
diff --git a/src/util/windows-launch-artifacts.ts b/src/util/windows-launch-artifacts.ts
index c037f122..956e2e85 100644
--- a/src/util/windows-launch-artifacts.ts
+++ b/src/util/windows-launch-artifacts.ts
@@ -1,4 +1,5 @@
import { writeFile, mkdir, stat, truncate } from 'fs/promises';
+import { existsSync } from 'fs';
import { execSync } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
@@ -29,10 +30,20 @@ export function resolveLaunchPaths(): LaunchPaths {
};
}
-/** Write the daemon-watchdog.cmd that loops and restarts the daemon. */
+/** Write the daemon-watchdog.cmd that loops and restarts the daemon.
+ * Uses the npm global shim (`imcodes.cmd`) instead of hard-coding
+ * node.exe + script paths — this way the watchdog always launches
+ * whatever version is currently installed, even after npm upgrades. */
export async function writeWatchdogCmd(paths: LaunchPaths): Promise {
await mkdir(dirname(paths.watchdogPath), { recursive: true });
- const watchdog = `@echo off\r\nchcp 65001 >nul 2>&1\r\n:loop\r\n"${paths.nodeExe}" "${paths.imcodesScript}" start --foreground >> "${paths.logPath}" 2>&1\r\ntimeout /t 5 /nobreak >nul\r\ngoto loop\r\n`;
+ // Resolve the npm global shim path (e.g. C:\Users\X\AppData\Roaming\npm\imcodes.cmd)
+ const npmGlobalBin = dirname(paths.imcodesScript).replace(/[/\\]node_modules[/\\]imcodes[/\\]dist[/\\]src$/i, '');
+ const shimPath = join(npmGlobalBin, 'imcodes.cmd');
+ // Prefer the shim if it exists; fall back to direct node+script for dev setups
+ const launchCmd = existsSync(shimPath)
+ ? `"${shimPath}" start --foreground`
+ : `"${paths.nodeExe}" "${paths.imcodesScript}" start --foreground`;
+ const watchdog = `@echo off\r\nchcp 65001 >nul 2>&1\r\n:loop\r\n${launchCmd} >> "${paths.logPath}" 2>&1\r\ntimeout /t 5 /nobreak >nul\r\ngoto loop\r\n`;
await writeFile(paths.watchdogPath, watchdog, 'utf8');
}
diff --git a/src/util/windows-upgrade-script.ts b/src/util/windows-upgrade-script.ts
index d7841912..fc391ca2 100644
--- a/src/util/windows-upgrade-script.ts
+++ b/src/util/windows-upgrade-script.ts
@@ -5,6 +5,8 @@ export interface WindowsUpgradeScriptInput {
npmCmd: string;
pkgSpec: string;
targetVer: string;
+ /** Absolute path to daemon-launcher.vbs for hidden restart */
+ vbsLauncherPath: string;
}
export function buildWindowsCleanupScript(scriptDir: string): string {
@@ -15,17 +17,48 @@ rmdir /s /q "${scriptDir}"\r
}
export function buildWindowsUpgradeBatch(input: WindowsUpgradeScriptInput): string {
- const { logFile, cleanupPath, npmCmd, pkgSpec, targetVer } = input;
+ const { logFile, cleanupPath, npmCmd, pkgSpec, targetVer, vbsLauncherPath } = input;
return `@echo off\r
setlocal EnableDelayedExpansion\r
echo === imcodes upgrade started at %date% %time% === >> "${logFile}"\r
timeout /t 2 /nobreak > nul\r
\r
+rem ── Stop watchdog + daemon BEFORE npm install ──────────────────────────\r
+rem npm install -g deletes the old package before writing the new one.\r
+rem If the watchdog loop restarts the daemon during that window, it hits\r
+rem MODULE_NOT_FOUND and dies. Kill the entire watchdog tree first.\r
+set "PIDFILE=%USERPROFILE%\\.imcodes\\daemon.pid"\r
+if exist "%PIDFILE%" (\r
+ set /p OLD_PID=<"%PIDFILE%"\r
+ echo Stopping daemon PID !OLD_PID! and watchdog tree... >> "${logFile}"\r
+ rem Find watchdog (parent of daemon) via wmic and tree-kill it\r
+ for /f "tokens=2 delims==" %%a in ('wmic process where "ProcessId=!OLD_PID!" get ParentProcessId /format:list 2^>nul ^| find "="') do (\r
+ set "WATCHDOG_PID=%%a"\r
+ )\r
+ if defined WATCHDOG_PID (\r
+ rem Strip trailing carriage return from wmic output\r
+ set "WATCHDOG_PID=!WATCHDOG_PID: =!"\r
+ for /f "delims=" %%x in ("!WATCHDOG_PID!") do set "WATCHDOG_PID=%%x"\r
+ taskkill /f /t /pid !WATCHDOG_PID! >nul 2>&1\r
+ echo Killed watchdog tree PID !WATCHDOG_PID! >> "${logFile}"\r
+ )\r
+ rem Belt-and-suspenders: ensure daemon is dead even if tree-kill missed it\r
+ taskkill /f /pid !OLD_PID! >nul 2>&1\r
+ rem Also kill any other stale watchdog loops\r
+ for /f "tokens=2 delims=," %%p in ('wmic process where "CommandLine like '%%daemon-watchdog%%' and Name='cmd.exe'" get ProcessId /format:csv 2^>nul ^| findstr /r "[0-9]"') do (\r
+ taskkill /f /t /pid %%p >nul 2>&1\r
+ )\r
+ del "%PIDFILE%" >nul 2>&1\r
+ timeout /t 2 /nobreak >nul\r
+)\r
+\r
echo Installing ${pkgSpec}... >> "${logFile}"\r
call "${npmCmd}" install -g ${pkgSpec} >> "${logFile}" 2>&1\r
if %errorlevel% neq 0 (\r
- echo Install FAILED — keeping current daemon running. >> "${logFile}"\r
+ echo Install FAILED — restarting current daemon. >> "${logFile}"\r
echo === upgrade aborted at %date% %time% === >> "${logFile}"\r
+ rem Restart the old version so the daemon isn't left dead\r
+ if exist "${vbsLauncherPath}" wscript "${vbsLauncherPath}"\r
start "" /min cmd /c "${cleanupPath}" >nul 2>&1\r
goto :done\r
)\r
@@ -35,6 +68,7 @@ for /f "usebackq delims=" %%p in (\`call "${npmCmd}" prefix -g 2^>nul\`) do if n
if not defined NPM_PREFIX (\r
echo Could not resolve npm global prefix after install. >> "${logFile}"\r
echo === upgrade aborted at %date% %time% === >> "${logFile}"\r
+ if exist "${vbsLauncherPath}" wscript "${vbsLauncherPath}"\r
start "" /min cmd /c "${cleanupPath}" >nul 2>&1\r
goto :done\r
)\r
@@ -43,6 +77,7 @@ set "CLI_SHIM=%NPM_PREFIX%\\imcodes.cmd"\r
if not exist "%CLI_SHIM%" (\r
echo imcodes shim missing after install: %CLI_SHIM% >> "${logFile}"\r
echo === upgrade aborted at %date% %time% === >> "${logFile}"\r
+ if exist "${vbsLauncherPath}" wscript "${vbsLauncherPath}"\r
start "" /min cmd /c "${cleanupPath}" >nul 2>&1\r
goto :done\r
)\r
@@ -51,8 +86,9 @@ set "INSTALLED_VER="\r
for /f "usebackq delims=" %%v in (\`call "%CLI_SHIM%" --version 2^>nul\`) do if not defined INSTALLED_VER set "INSTALLED_VER=%%v"\r
echo Install succeeded. Installed version: %INSTALLED_VER%, target: ${targetVer}, shim: %CLI_SHIM% >> "${logFile}"\r
if not "${targetVer}"=="latest" if /I not "%INSTALLED_VER%"=="${targetVer}" (\r
- echo Version mismatch after install — keeping current daemon running. >> "${logFile}"\r
+ echo Version mismatch after install — restarting current version. >> "${logFile}"\r
echo === upgrade aborted at %date% %time% === >> "${logFile}"\r
+ if exist "${vbsLauncherPath}" wscript "${vbsLauncherPath}"\r
start "" /min cmd /c "${cleanupPath}" >nul 2>&1\r
goto :done\r
)\r
@@ -66,11 +102,18 @@ call "%CLI_SHIM%" repair-watchdog >> "${logFile}" 2>&1\r
if %errorlevel% neq 0 (\r
echo WARNING: Launch chain regeneration failed >> "${logFile}"\r
)\r
-echo Restarting daemon via CLI watchdog path... >> "${logFile}"\r
-call "%CLI_SHIM%" restart >> "${logFile}" 2>&1\r
-if %errorlevel% neq 0 echo Restart command failed (exit %errorlevel%). >> "${logFile}"\r
+rem ── Start fresh hidden watchdog via VBS (not imcodes restart) ──────────\r
+rem Using VBS directly avoids depending on the new CLI's restart logic\r
+rem which may differ across versions. The watchdog loop inside the CMD\r
+rem will start the daemon automatically.\r
+echo Starting fresh watchdog via VBS launcher... >> "${logFile}"\r
+if exist "${vbsLauncherPath}" (\r
+ wscript "${vbsLauncherPath}"\r
+) else (\r
+ echo WARNING: VBS launcher not found, falling back to CLI restart >> "${logFile}"\r
+ call "%CLI_SHIM%" restart >> "${logFile}" 2>&1\r
+)\r
timeout /t 8 /nobreak >nul\r
-set "PIDFILE=%USERPROFILE%\\.imcodes\\daemon.pid"\r
if exist "%PIDFILE%" (\r
set /p DAEMON_PID=<"%PIDFILE%"\r
tasklist /fi "PID eq !DAEMON_PID!" /nh 2^>nul | find "!DAEMON_PID!" >nul\r
diff --git a/test/util/windows-upgrade-script.test.ts b/test/util/windows-upgrade-script.test.ts
index a1595949..5b8217e5 100644
--- a/test/util/windows-upgrade-script.test.ts
+++ b/test/util/windows-upgrade-script.test.ts
@@ -18,12 +18,41 @@ describe('buildWindowsUpgradeBatch', () => {
npmCmd: 'C:\\Program Files\\nodejs\\npm.cmd',
pkgSpec: 'imcodes@1.2.3',
targetVer: '1.2.3',
+ vbsLauncherPath: 'C:\\Users\\tester\\.imcodes\\daemon-launcher.vbs',
+ });
+
+ it('kills watchdog tree before npm install to prevent MODULE_NOT_FOUND race', () => {
+ const installIdx = batch.indexOf('call "C:\\Program Files\\nodejs\\npm.cmd" install');
+ const killIdx = batch.indexOf('taskkill /f /t /pid');
+ // Kill must come BEFORE the actual install command
+ expect(killIdx).toBeGreaterThan(-1);
+ expect(installIdx).toBeGreaterThan(-1);
+ expect(killIdx).toBeLessThan(installIdx);
+ // Should also read daemon PID and find parent via wmic
+ expect(batch).toContain('set /p OLD_PID=');
+ expect(batch).toContain('wmic process where');
+ expect(batch).toContain('ParentProcessId');
+ });
+
+ it('deletes stale PID file after killing watchdog', () => {
+ expect(batch).toContain('del "%PIDFILE%" >nul 2>&1');
});
it('installs the requested package with a quoted npm path', () => {
expect(batch).toContain('call "C:\\Program Files\\nodejs\\npm.cmd" install -g imcodes@1.2.3');
});
+ it('restarts old daemon on install failure (does not leave it dead)', () => {
+ // Every "Install FAILED" / abort path should restart via VBS before aborting
+ expect(batch).toContain('Install FAILED');
+ expect(batch).toContain('restarting current daemon');
+ // Count VBS restarts in abort paths (each abort block has wscript call)
+ const abortBlocks = batch.split('goto :done');
+ // At least 4 abort paths (install fail, no prefix, no shim, version mismatch)
+ const vbsRestarts = abortBlocks.filter(b => b.includes('wscript'));
+ expect(vbsRestarts.length).toBeGreaterThanOrEqual(4);
+ });
+
it('verifies the installed CLI shim and version before restart', () => {
expect(batch).toContain('set "NPM_PREFIX="');
expect(batch).toContain('prefix -g');
@@ -34,14 +63,29 @@ describe('buildWindowsUpgradeBatch', () => {
expect(batch).toContain('if /I not "%INSTALLED_VER%"=="1.2.3"');
});
- it('uses the shared CLI restart path through the global shim instead of startup cmd shortcuts', () => {
- expect(batch).toContain('call "%CLI_SHIM%" restart');
- expect(batch).not.toContain('imcodes-daemon.cmd');
- expect(batch).not.toContain('taskkill /f /pid');
+ it('starts fresh watchdog via VBS launcher instead of imcodes restart', () => {
+ // After successful install, should use VBS directly (not CLI restart)
+ expect(batch).toContain('wscript "C:\\Users\\tester\\.imcodes\\daemon-launcher.vbs"');
+ // Should NOT depend on `imcodes restart` for the normal path
+ // (only as fallback if VBS missing)
+ const afterRepair = batch.split('repair-watchdog')[1] ?? '';
+ expect(afterRepair).toContain('Starting fresh watchdog via VBS');
+ expect(afterRepair).toContain('wscript');
+ });
+
+ it('runs health check after restart', () => {
+ expect(batch).toContain('Health check PASSED');
+ expect(batch).toContain('Health check FAILED');
+ expect(batch).toContain('tasklist /fi "PID eq !DAEMON_PID!"');
});
- it('uses a standalone cleanup script instead of nested inline cleanup quoting', () => {
+ it('uses minimized cleanup windows', () => {
expect(batch).toContain('start "" /min cmd /c "C:\\Temp\\imcodes-upgrade-123\\cleanup.cmd" >nul 2>&1');
expect(batch).not.toContain('rmdir /s /q ""');
});
+
+ it('kills stale watchdog loops not just the active one', () => {
+ expect(batch).toContain('daemon-watchdog');
+ expect(batch).toContain("Name='cmd.exe'");
+ });
});
From 603a2dda71f6ebaa210317fa3d507049004264f9 Mon Sep 17 00:00:00 2001
From: imcodes-win
Date: Mon, 6 Apr 2026 10:05:30 +0800
Subject: [PATCH 03/57] fix(windows): upgrade lock file prevents watchdog race
during npm install
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Instead of relying on killing the watchdog tree (which doesn't work
when upgrading from an old version without tree-kill logic), use a
simple lock file (~/.imcodes/upgrade.lock):
1. Upgrade batch creates lock file BEFORE npm install
2. Kills old watchdog + daemon (old watchdog doesn't know about lock)
3. npm install runs safely — nothing tries to restart
4. repair-watchdog generates new watchdog.cmd with lock-checking loop
5. Starts new watchdog via VBS — it sees lock, waits
6. Deletes lock file — watchdog immediately starts the new daemon
7. On any abort path: delete lock + restart VBS so daemon is never left dead
Also:
- watchdog.cmd now uses npm global shim (imcodes.cmd) instead of
hard-coded node+script paths — survives npm upgrades without
repair-watchdog
- restartWindowsDaemon simplified: just kill daemon + launch VBS,
let watchdog handle the restart loop
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/daemon/command-handler.ts | 2 +
src/util/windows-daemon.ts | 47 +++-------------
src/util/windows-launch-artifacts.ts | 20 ++++++-
src/util/windows-upgrade-script.ts | 65 +++++++++++-----------
test/util/windows-daemon.test.ts | 40 ++------------
test/util/windows-upgrade-script.test.ts | 69 +++++++++++-------------
6 files changed, 99 insertions(+), 144 deletions(-)
diff --git a/src/daemon/command-handler.ts b/src/daemon/command-handler.ts
index 4ad3d67c..164c866f 100644
--- a/src/daemon/command-handler.ts
+++ b/src/daemon/command-handler.ts
@@ -36,6 +36,7 @@ import { getProvider } from '../agent/provider-registry.js';
import { copyFile } from 'node:fs/promises';
import { ensureImcDir, imcSubDir } from '../util/imc-dir.js';
import { buildWindowsCleanupScript, buildWindowsUpgradeBatch } from '../util/windows-upgrade-script.js';
+import { UPGRADE_LOCK_FILE } from '../util/windows-launch-artifacts.js';
import { registerTempFile, removeTrackedTempFile } from '../store/temp-file-store.js';
import { sanitizeProjectName } from '../../shared/sanitize-project-name.js';
import { P2P_TERMINAL_RUN_STATUSES } from '../../shared/p2p-status.js';
@@ -2127,6 +2128,7 @@ launchctl load -w "${plist}"`;
pkgSpec,
targetVer,
vbsLauncherPath,
+ upgradeLockFile: UPGRADE_LOCK_FILE,
});
writeFileSync(batchPath, batch);
diff --git a/src/util/windows-daemon.ts b/src/util/windows-daemon.ts
index 7487998c..693824e9 100644
--- a/src/util/windows-daemon.ts
+++ b/src/util/windows-daemon.ts
@@ -29,37 +29,6 @@ function sleepMs(ms: number): void {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
-// ── Kill the watchdog cmd.exe tree that parents the daemon ──────────────────
-
-/** Find the parent PID of a process via wmic. Returns null on failure. */
-function getParentPid(pid: number): number | null {
- try {
- const raw = execSync(`wmic process where "ProcessId=${pid}" get ParentProcessId /format:list`, {
- encoding: 'utf8',
- stdio: ['pipe', 'pipe', 'ignore'],
- });
- const m = raw.match(/ParentProcessId=(\d+)/);
- return m ? parseInt(m[1], 10) : null;
- } catch {
- return null;
- }
-}
-
-/** Kill the watchdog process tree that parents the daemon.
- * The tree is: wscript → cmd.exe (watchdog loop) → node.exe (daemon).
- * We kill the top-level process tree so no stale watchdog keeps respawning. */
-function killWatchdogTree(daemonPid: number): void {
- // Walk up to the watchdog (cmd.exe or wscript)
- const parentPid = getParentPid(daemonPid);
- if (!parentPid) return;
-
- // The parent of the daemon is the watchdog cmd.exe loop.
- // Kill the entire process tree from the watchdog down — this also kills the daemon.
- try {
- execSync(`taskkill /f /t /pid ${parentPid}`, { stdio: 'ignore' });
- } catch { /* already dead */ }
-}
-
// ── Launcher methods (all hidden — no visible windows) ──────────────────────
function tryStartVbsLauncher(): boolean {
@@ -111,18 +80,16 @@ function tryStartStartupShortcut(): boolean {
export function restartWindowsDaemon(currentPid?: number): boolean {
const previousPid = readDaemonPid(currentPid);
if (previousPid) {
- // Kill the entire watchdog tree, not just the daemon.
- // This prevents the old watchdog from racing with the new one.
- killWatchdogTree(previousPid);
- // Belt-and-suspenders: ensure the daemon itself is dead even if tree-kill missed it
- sleepMs(500);
- if (isPidAlive(previousPid)) {
- try { execSync(`taskkill /f /pid ${previousPid}`, { stdio: 'ignore' }); } catch { /* ignore */ }
- }
+ // Kill the daemon process. The watchdog loop will detect the exit and
+ // restart it automatically (within ~5 seconds).
+ try { execSync(`taskkill /f /pid ${previousPid}`, { stdio: 'ignore' }); } catch { /* not running */ }
}
- // Launch a fresh hidden watchdog.
+ // If no watchdog is running (e.g. first start after bind), launch one.
// Priority: VBS (always hidden) > scheduled task > startup shortcut.
+ // If a watchdog IS already running, it will restart the daemon on its own —
+ // but launching a second VBS is harmless (the daemon lock prevents duplicates,
+ // and the extra watchdog exits when it sees "already running").
let triggered = false;
if (tryStartVbsLauncher()) {
triggered = true;
diff --git a/src/util/windows-launch-artifacts.ts b/src/util/windows-launch-artifacts.ts
index 956e2e85..69aa9d9d 100644
--- a/src/util/windows-launch-artifacts.ts
+++ b/src/util/windows-launch-artifacts.ts
@@ -10,6 +10,10 @@ const __dirname = dirname(__filename);
const TASK_NAME = 'imcodes-daemon';
+/** Sentinel file that tells the watchdog loop to pause.
+ * Created by the upgrade batch before npm install, deleted after restart. */
+export const UPGRADE_LOCK_FILE = join(homedir(), '.imcodes', 'upgrade.lock');
+
export interface LaunchPaths {
nodeExe: string;
imcodesScript: string;
@@ -43,7 +47,21 @@ export async function writeWatchdogCmd(paths: LaunchPaths): Promise {
const launchCmd = existsSync(shimPath)
? `"${shimPath}" start --foreground`
: `"${paths.nodeExe}" "${paths.imcodesScript}" start --foreground`;
- const watchdog = `@echo off\r\nchcp 65001 >nul 2>&1\r\n:loop\r\n${launchCmd} >> "${paths.logPath}" 2>&1\r\ntimeout /t 5 /nobreak >nul\r\ngoto loop\r\n`;
+ const lockFile = UPGRADE_LOCK_FILE.replace(/\//g, '\\');
+ const watchdog = [
+ '@echo off',
+ 'chcp 65001 >nul 2>&1',
+ ':loop',
+ `if exist "${lockFile}" (`,
+ ` echo Upgrade in progress, waiting... >> "${paths.logPath}"`,
+ ' timeout /t 5 /nobreak >nul',
+ ' goto loop',
+ ')',
+ `${launchCmd} >> "${paths.logPath}" 2>&1`,
+ 'timeout /t 5 /nobreak >nul',
+ 'goto loop',
+ '',
+ ].join('\r\n');
await writeFile(paths.watchdogPath, watchdog, 'utf8');
}
diff --git a/src/util/windows-upgrade-script.ts b/src/util/windows-upgrade-script.ts
index fc391ca2..6070a6f3 100644
--- a/src/util/windows-upgrade-script.ts
+++ b/src/util/windows-upgrade-script.ts
@@ -7,6 +7,8 @@ export interface WindowsUpgradeScriptInput {
targetVer: string;
/** Absolute path to daemon-launcher.vbs for hidden restart */
vbsLauncherPath: string;
+ /** Sentinel file — while it exists the watchdog loop pauses */
+ upgradeLockFile: string;
}
export function buildWindowsCleanupScript(scriptDir: string): string {
@@ -17,47 +19,41 @@ rmdir /s /q "${scriptDir}"\r
}
export function buildWindowsUpgradeBatch(input: WindowsUpgradeScriptInput): string {
- const { logFile, cleanupPath, npmCmd, pkgSpec, targetVer, vbsLauncherPath } = input;
+ const { logFile, cleanupPath, npmCmd, pkgSpec, targetVer, vbsLauncherPath, upgradeLockFile } = input;
return `@echo off\r
setlocal EnableDelayedExpansion\r
echo === imcodes upgrade started at %date% %time% === >> "${logFile}"\r
timeout /t 2 /nobreak > nul\r
\r
-rem ── Stop watchdog + daemon BEFORE npm install ──────────────────────────\r
-rem npm install -g deletes the old package before writing the new one.\r
-rem If the watchdog loop restarts the daemon during that window, it hits\r
-rem MODULE_NOT_FOUND and dies. Kill the entire watchdog tree first.\r
+rem ── Create upgrade lock — watchdog will pause while this file exists ──\r
+echo upgrade > "${upgradeLockFile}"\r
+echo Upgrade lock created >> "${logFile}"\r
+\r
+rem ── Kill daemon + old watchdog so npm can overwrite files cleanly ─────\r
+rem Old watchdog versions don't know about the lock file, so we must kill\r
+rem them. After npm install, repair-watchdog generates a new watchdog\r
+rem that respects the lock.\r
set "PIDFILE=%USERPROFILE%\\.imcodes\\daemon.pid"\r
if exist "%PIDFILE%" (\r
set /p OLD_PID=<"%PIDFILE%"\r
- echo Stopping daemon PID !OLD_PID! and watchdog tree... >> "${logFile}"\r
- rem Find watchdog (parent of daemon) via wmic and tree-kill it\r
+ echo Stopping daemon PID !OLD_PID! and old watchdog... >> "${logFile}"\r
+ rem Find watchdog (parent of daemon) and tree-kill it\r
for /f "tokens=2 delims==" %%a in ('wmic process where "ProcessId=!OLD_PID!" get ParentProcessId /format:list 2^>nul ^| find "="') do (\r
- set "WATCHDOG_PID=%%a"\r
+ set "WD_PID=%%a"\r
)\r
- if defined WATCHDOG_PID (\r
- rem Strip trailing carriage return from wmic output\r
- set "WATCHDOG_PID=!WATCHDOG_PID: =!"\r
- for /f "delims=" %%x in ("!WATCHDOG_PID!") do set "WATCHDOG_PID=%%x"\r
- taskkill /f /t /pid !WATCHDOG_PID! >nul 2>&1\r
- echo Killed watchdog tree PID !WATCHDOG_PID! >> "${logFile}"\r
+ if defined WD_PID (\r
+ taskkill /f /t /pid !WD_PID! >nul 2>&1\r
)\r
- rem Belt-and-suspenders: ensure daemon is dead even if tree-kill missed it\r
taskkill /f /pid !OLD_PID! >nul 2>&1\r
- rem Also kill any other stale watchdog loops\r
- for /f "tokens=2 delims=," %%p in ('wmic process where "CommandLine like '%%daemon-watchdog%%' and Name='cmd.exe'" get ProcessId /format:csv 2^>nul ^| findstr /r "[0-9]"') do (\r
- taskkill /f /t /pid %%p >nul 2>&1\r
- )\r
- del "%PIDFILE%" >nul 2>&1\r
timeout /t 2 /nobreak >nul\r
)\r
\r
echo Installing ${pkgSpec}... >> "${logFile}"\r
call "${npmCmd}" install -g ${pkgSpec} >> "${logFile}" 2>&1\r
if %errorlevel% neq 0 (\r
- echo Install FAILED — restarting current daemon. >> "${logFile}"\r
+ echo Install FAILED — removing lock, watchdog will restart current version. >> "${logFile}"\r
echo === upgrade aborted at %date% %time% === >> "${logFile}"\r
- rem Restart the old version so the daemon isn't left dead\r
+ del "${upgradeLockFile}" >nul 2>&1\r
if exist "${vbsLauncherPath}" wscript "${vbsLauncherPath}"\r
start "" /min cmd /c "${cleanupPath}" >nul 2>&1\r
goto :done\r
@@ -68,6 +64,7 @@ for /f "usebackq delims=" %%p in (\`call "${npmCmd}" prefix -g 2^>nul\`) do if n
if not defined NPM_PREFIX (\r
echo Could not resolve npm global prefix after install. >> "${logFile}"\r
echo === upgrade aborted at %date% %time% === >> "${logFile}"\r
+ del "${upgradeLockFile}" >nul 2>&1\r
if exist "${vbsLauncherPath}" wscript "${vbsLauncherPath}"\r
start "" /min cmd /c "${cleanupPath}" >nul 2>&1\r
goto :done\r
@@ -77,6 +74,7 @@ set "CLI_SHIM=%NPM_PREFIX%\\imcodes.cmd"\r
if not exist "%CLI_SHIM%" (\r
echo imcodes shim missing after install: %CLI_SHIM% >> "${logFile}"\r
echo === upgrade aborted at %date% %time% === >> "${logFile}"\r
+ del "${upgradeLockFile}" >nul 2>&1\r
if exist "${vbsLauncherPath}" wscript "${vbsLauncherPath}"\r
start "" /min cmd /c "${cleanupPath}" >nul 2>&1\r
goto :done\r
@@ -86,8 +84,9 @@ set "INSTALLED_VER="\r
for /f "usebackq delims=" %%v in (\`call "%CLI_SHIM%" --version 2^>nul\`) do if not defined INSTALLED_VER set "INSTALLED_VER=%%v"\r
echo Install succeeded. Installed version: %INSTALLED_VER%, target: ${targetVer}, shim: %CLI_SHIM% >> "${logFile}"\r
if not "${targetVer}"=="latest" if /I not "%INSTALLED_VER%"=="${targetVer}" (\r
- echo Version mismatch after install — restarting current version. >> "${logFile}"\r
+ echo Version mismatch after install — removing lock, watchdog will restart. >> "${logFile}"\r
echo === upgrade aborted at %date% %time% === >> "${logFile}"\r
+ del "${upgradeLockFile}" >nul 2>&1\r
if exist "${vbsLauncherPath}" wscript "${vbsLauncherPath}"\r
start "" /min cmd /c "${cleanupPath}" >nul 2>&1\r
goto :done\r
@@ -102,18 +101,22 @@ call "%CLI_SHIM%" repair-watchdog >> "${logFile}" 2>&1\r
if %errorlevel% neq 0 (\r
echo WARNING: Launch chain regeneration failed >> "${logFile}"\r
)\r
-rem ── Start fresh hidden watchdog via VBS (not imcodes restart) ──────────\r
-rem Using VBS directly avoids depending on the new CLI's restart logic\r
-rem which may differ across versions. The watchdog loop inside the CMD\r
-rem will start the daemon automatically.\r
-echo Starting fresh watchdog via VBS launcher... >> "${logFile}"\r
+\r
+rem ── Start new watchdog (lock-aware), then remove lock ─────────────────\r
+rem The new watchdog (generated by repair-watchdog) checks the lock file.\r
+rem It will loop/wait while the lock exists, then start the daemon once\r
+rem we delete it below.\r
+echo Starting new watchdog via VBS... >> "${logFile}"\r
if exist "${vbsLauncherPath}" (\r
wscript "${vbsLauncherPath}"\r
) else (\r
- echo WARNING: VBS launcher not found, falling back to CLI restart >> "${logFile}"\r
- call "%CLI_SHIM%" restart >> "${logFile}" 2>&1\r
+ echo WARNING: VBS launcher not found at ${vbsLauncherPath} >> "${logFile}"\r
)\r
-timeout /t 8 /nobreak >nul\r
+echo Removing upgrade lock... >> "${logFile}"\r
+del "${upgradeLockFile}" >nul 2>&1\r
+\r
+rem Wait for new watchdog to start the daemon, then health-check\r
+timeout /t 10 /nobreak >nul\r
if exist "%PIDFILE%" (\r
set /p DAEMON_PID=<"%PIDFILE%"\r
tasklist /fi "PID eq !DAEMON_PID!" /nh 2^>nul | find "!DAEMON_PID!" >nul\r
diff --git a/test/util/windows-daemon.test.ts b/test/util/windows-daemon.test.ts
index 77d14d13..a149d49c 100644
--- a/test/util/windows-daemon.test.ts
+++ b/test/util/windows-daemon.test.ts
@@ -9,8 +9,6 @@ const state = vi.hoisted(() => ({
alivePids: new Set(),
execCalls: [] as string[],
spawnCalls: [] as Array<{ cmd: string; args: string[] }>,
- /** Simulated wmic ParentProcessId result for killWatchdogTree */
- wmicParentPid: null as number | null,
}));
vi.mock('node:os', () => ({
@@ -40,19 +38,8 @@ vi.mock('node:fs', () => ({
}));
vi.mock('node:child_process', () => ({
- execSync: vi.fn((cmd: string, opts?: { encoding?: string }) => {
+ execSync: vi.fn((cmd: string) => {
state.execCalls.push(cmd);
- // killWatchdogTree: wmic query for parent PID
- if (cmd.includes('wmic process where') && cmd.includes('ParentProcessId')) {
- if (state.wmicParentPid !== null) {
- const result = `\r\nParentProcessId=${state.wmicParentPid}\r\n`;
- return opts?.encoding ? result : Buffer.from(result);
- }
- throw new Error('not found');
- }
- // killWatchdogTree: taskkill /f /t (tree kill)
- if (cmd.startsWith('taskkill /f /t /pid ')) return '';
- // belt-and-suspenders: taskkill /f /pid (single process)
if (cmd.startsWith('taskkill /f /pid ')) return '';
if (cmd.includes('schtasks /Run /TN imcodes-daemon')) {
if (!state.scheduledTaskRunOk) throw new Error('run failed');
@@ -77,7 +64,6 @@ describe('restartWindowsDaemon', () => {
state.alivePids = new Set();
state.execCalls = [];
state.spawnCalls = [];
- state.wmicParentPid = null;
vi.spyOn(process, 'kill').mockImplementation(((pid: number) => {
if (!state.alivePids.has(pid)) throw new Error('not running');
return true;
@@ -87,20 +73,18 @@ describe('restartWindowsDaemon', () => {
it('returns false when no restart path is available', async () => {
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(false);
- // No VBS, no schtask, no startup shortcut — nothing to trigger
expect(state.spawnCalls).toHaveLength(0);
});
- it('kills watchdog tree and launches VBS when available', async () => {
+ it('kills daemon and launches VBS when available', async () => {
state.pidContents = ['123', '456'];
state.alivePids = new Set([456]);
state.vbsExists = true;
- state.wmicParentPid = 999; // watchdog parent
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(true);
- // Should tree-kill the watchdog parent
- expect(state.execCalls).toContain('taskkill /f /t /pid 999');
+ // Should kill daemon
+ expect(state.execCalls).toContain('taskkill /f /pid 123');
// Should launch VBS (preferred over schtask)
expect(state.spawnCalls[0]).toEqual(
expect.objectContaining({ cmd: 'wscript', args: expect.arrayContaining(['C:\\Users\\tester\\.imcodes\\daemon-launcher.vbs']) }),
@@ -111,11 +95,10 @@ describe('restartWindowsDaemon', () => {
state.pidContents = ['123', '456'];
state.alivePids = new Set([456]);
state.scheduledTaskRunOk = true;
- state.wmicParentPid = 888;
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(true);
- expect(state.execCalls).toContain('taskkill /f /t /pid 888');
+ expect(state.execCalls).toContain('taskkill /f /pid 123');
expect(state.execCalls).toContain('schtasks /Run /TN imcodes-daemon');
});
@@ -127,20 +110,7 @@ describe('restartWindowsDaemon', () => {
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(true);
expect(state.spawnCalls).toHaveLength(1);
- // cmd resolved via COMSPEC — match any path ending in cmd.exe
expect(state.spawnCalls[0].cmd).toMatch(/cmd(\.exe)?$/i);
expect(state.spawnCalls[0].args).toEqual(['/c', 'C:\\Users\\tester\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\imcodes-daemon.cmd']);
});
-
- it('force-kills daemon if tree-kill misses it', async () => {
- state.pidContents = ['123', '456'];
- state.alivePids = new Set([123, 456]); // daemon still alive after tree-kill
- state.vbsExists = true;
- state.wmicParentPid = null; // wmic fails — no parent found
-
- const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
- expect(restartWindowsDaemon()).toBe(true);
- // Should fall back to direct taskkill
- expect(state.execCalls).toContain('taskkill /f /pid 123');
- });
});
diff --git a/test/util/windows-upgrade-script.test.ts b/test/util/windows-upgrade-script.test.ts
index 5b8217e5..39ae412a 100644
--- a/test/util/windows-upgrade-script.test.ts
+++ b/test/util/windows-upgrade-script.test.ts
@@ -19,38 +19,41 @@ describe('buildWindowsUpgradeBatch', () => {
pkgSpec: 'imcodes@1.2.3',
targetVer: '1.2.3',
vbsLauncherPath: 'C:\\Users\\tester\\.imcodes\\daemon-launcher.vbs',
+ upgradeLockFile: 'C:\\Users\\tester\\.imcodes\\upgrade.lock',
});
- it('kills watchdog tree before npm install to prevent MODULE_NOT_FOUND race', () => {
+ it('creates upgrade lock before npm install', () => {
+ const lockIdx = batch.indexOf('echo upgrade > "C:\\Users\\tester\\.imcodes\\upgrade.lock"');
const installIdx = batch.indexOf('call "C:\\Program Files\\nodejs\\npm.cmd" install');
- const killIdx = batch.indexOf('taskkill /f /t /pid');
- // Kill must come BEFORE the actual install command
- expect(killIdx).toBeGreaterThan(-1);
+ expect(lockIdx).toBeGreaterThan(-1);
expect(installIdx).toBeGreaterThan(-1);
- expect(killIdx).toBeLessThan(installIdx);
- // Should also read daemon PID and find parent via wmic
- expect(batch).toContain('set /p OLD_PID=');
- expect(batch).toContain('wmic process where');
- expect(batch).toContain('ParentProcessId');
+ expect(lockIdx).toBeLessThan(installIdx);
});
- it('deletes stale PID file after killing watchdog', () => {
- expect(batch).toContain('del "%PIDFILE%" >nul 2>&1');
+ it('removes lock on every abort path so watchdog resumes', () => {
+ const abortBlocks = batch.split('goto :done');
+ // At least 4 abort paths (install fail, no prefix, no shim, version mismatch)
+ const lockDeletes = abortBlocks.filter(b => b.includes('del "C:\\Users\\tester\\.imcodes\\upgrade.lock"'));
+ expect(lockDeletes.length).toBeGreaterThanOrEqual(4);
});
- it('installs the requested package with a quoted npm path', () => {
- expect(batch).toContain('call "C:\\Program Files\\nodejs\\npm.cmd" install -g imcodes@1.2.3');
+ it('starts new watchdog via VBS then removes lock after successful install', () => {
+ // After the actual repair-watchdog CLI call, batch should start VBS then remove lock
+ const repairCallIdx = batch.indexOf('call "%CLI_SHIM%" repair-watchdog');
+ const afterRepair = batch.slice(repairCallIdx);
+ const vbsIdx = afterRepair.indexOf('Starting new watchdog via VBS');
+ const delIdx = afterRepair.indexOf('Removing upgrade lock');
+ expect(vbsIdx).toBeGreaterThan(-1);
+ expect(delIdx).toBeGreaterThan(vbsIdx);
+ expect(afterRepair).toContain('del "C:\\Users\\tester\\.imcodes\\upgrade.lock"');
});
- it('restarts old daemon on install failure (does not leave it dead)', () => {
- // Every "Install FAILED" / abort path should restart via VBS before aborting
- expect(batch).toContain('Install FAILED');
- expect(batch).toContain('restarting current daemon');
- // Count VBS restarts in abort paths (each abort block has wscript call)
- const abortBlocks = batch.split('goto :done');
- // At least 4 abort paths (install fail, no prefix, no shim, version mismatch)
- const vbsRestarts = abortBlocks.filter(b => b.includes('wscript'));
- expect(vbsRestarts.length).toBeGreaterThanOrEqual(4);
+ it('kills daemon before npm install', () => {
+ expect(batch).toContain('taskkill /f /pid !OLD_PID!');
+ });
+
+ it('installs the requested package with a quoted npm path', () => {
+ expect(batch).toContain('call "C:\\Program Files\\nodejs\\npm.cmd" install -g imcodes@1.2.3');
});
it('verifies the installed CLI shim and version before restart', () => {
@@ -63,17 +66,15 @@ describe('buildWindowsUpgradeBatch', () => {
expect(batch).toContain('if /I not "%INSTALLED_VER%"=="1.2.3"');
});
- it('starts fresh watchdog via VBS launcher instead of imcodes restart', () => {
- // After successful install, should use VBS directly (not CLI restart)
- expect(batch).toContain('wscript "C:\\Users\\tester\\.imcodes\\daemon-launcher.vbs"');
- // Should NOT depend on `imcodes restart` for the normal path
- // (only as fallback if VBS missing)
- const afterRepair = batch.split('repair-watchdog')[1] ?? '';
- expect(afterRepair).toContain('Starting fresh watchdog via VBS');
- expect(afterRepair).toContain('wscript');
+ it('lets watchdog restart daemon after lock removal (no manual restart)', () => {
+ // After successful upgrade, should NOT call `imcodes restart`
+ // — the watchdog loop will detect lock removal and restart automatically
+ const afterLockRemoval = batch.split('Removing upgrade lock')[1] ?? '';
+ expect(afterLockRemoval).not.toContain('imcodes restart');
+ expect(afterLockRemoval).not.toContain('CLI_SHIM%" restart');
});
- it('runs health check after restart', () => {
+ it('runs health check after watchdog restarts daemon', () => {
expect(batch).toContain('Health check PASSED');
expect(batch).toContain('Health check FAILED');
expect(batch).toContain('tasklist /fi "PID eq !DAEMON_PID!"');
@@ -81,11 +82,5 @@ describe('buildWindowsUpgradeBatch', () => {
it('uses minimized cleanup windows', () => {
expect(batch).toContain('start "" /min cmd /c "C:\\Temp\\imcodes-upgrade-123\\cleanup.cmd" >nul 2>&1');
- expect(batch).not.toContain('rmdir /s /q ""');
- });
-
- it('kills stale watchdog loops not just the active one', () => {
- expect(batch).toContain('daemon-watchdog');
- expect(batch).toContain("Name='cmd.exe'");
});
});
From 6356c1fa08b308a36bf2f410363a7e0d0b8a3b01 Mon Sep 17 00:00:00 2001
From: imcodes-win
Date: Mon, 6 Apr 2026 10:08:35 +0800
Subject: [PATCH 04/57] test(windows): comprehensive daemon recovery tests
32 tests covering every daemon recovery scenario:
windows-daemon.test.ts (10 tests):
- Launcher priority: VBS > schtask > startup shortcut
- Daemon kill: by PID, handles already-dead, skips own PID
- PID wait: new PID detection, fresh start without previous PID
- windowsHide on all spawn calls
windows-upgrade-script.test.ts (16 tests):
- Lock file lifecycle: created before install, deleted on every exit path
- Every abort path: deletes lock AND restarts VBS (daemon never left dead)
- Success path: VBS started before lock removal (watchdog waits on lock)
- Old watchdog tree-kill before npm install
- Version verification and repair-watchdog ordering
- No imcodes restart on success path (watchdog handles it)
- Health check after restart
- No visible windows (minimized cleanup)
- "latest" target skips version comparison
windows-launch-artifacts.test.ts (6 tests):
- Watchdog.cmd: lock file check before daemon launch
- Watchdog.cmd: uses npm global shim, falls back to node+script
- Watchdog.cmd: infinite loop with 5s retry
- VBS launcher: hidden window (style 0)
- UPGRADE_LOCK_FILE path
Co-Authored-By: Claude Opus 4.6 (1M context)
---
test/util/windows-daemon.test.ts | 158 +++++++++++++++------
test/util/windows-launch-artifacts.test.ts | 149 +++++++++++++++++++
test/util/windows-upgrade-script.test.ts | 155 ++++++++++++++------
3 files changed, 376 insertions(+), 86 deletions(-)
create mode 100644 test/util/windows-launch-artifacts.test.ts
diff --git a/test/util/windows-daemon.test.ts b/test/util/windows-daemon.test.ts
index a149d49c..bef98882 100644
--- a/test/util/windows-daemon.test.ts
+++ b/test/util/windows-daemon.test.ts
@@ -1,5 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
+// ── Shared mock state ──────────────────────────────────────────────────────────
+
const state = vi.hoisted(() => ({
pidContents: [''],
pidIndex: 0,
@@ -11,37 +13,31 @@ const state = vi.hoisted(() => ({
spawnCalls: [] as Array<{ cmd: string; args: string[] }>,
}));
-vi.mock('node:os', () => ({
- homedir: () => 'C:\\Users\\tester',
-}));
+vi.mock('node:os', () => ({ homedir: () => 'C:\\Users\\tester' }));
vi.mock('node:path', async () => {
const actual = await vi.importActual('node:path');
- return {
- ...actual,
- resolve: (...parts: string[]) => parts.join('\\'),
- };
+ return { ...actual, resolve: (...parts: string[]) => parts.join('\\') };
});
vi.mock('node:fs', () => ({
existsSync: vi.fn((path: string) => {
if (path.endsWith('daemon-launcher.vbs')) return state.vbsExists;
- if (path.endsWith('Start Menu\\Programs\\Startup\\imcodes-daemon.cmd')) return state.startupCmdExists;
+ if (path.endsWith('imcodes-daemon.cmd')) return state.startupCmdExists;
return false;
}),
readFileSync: vi.fn(() => {
const idx = Math.min(state.pidIndex, state.pidContents.length - 1);
- const value = state.pidContents[idx] ?? '';
state.pidIndex += 1;
- return value;
+ return state.pidContents[idx] ?? '';
}),
}));
vi.mock('node:child_process', () => ({
execSync: vi.fn((cmd: string) => {
state.execCalls.push(cmd);
- if (cmd.startsWith('taskkill /f /pid ')) return '';
- if (cmd.includes('schtasks /Run /TN imcodes-daemon')) {
+ if (cmd.startsWith('taskkill ')) return '';
+ if (cmd.includes('schtasks /Run')) {
if (!state.scheduledTaskRunOk) throw new Error('run failed');
return '';
}
@@ -53,64 +49,138 @@ vi.mock('node:child_process', () => ({
}),
}));
+function reset(): void {
+ vi.resetModules();
+ state.pidContents = [''];
+ state.pidIndex = 0;
+ state.scheduledTaskRunOk = false;
+ state.vbsExists = false;
+ state.startupCmdExists = false;
+ state.alivePids = new Set();
+ state.execCalls = [];
+ state.spawnCalls = [];
+ vi.spyOn(process, 'kill').mockImplementation(((pid: number) => {
+ if (!state.alivePids.has(pid)) throw new Error('not running');
+ return true;
+ }) as typeof process.kill);
+}
+
+// ── restartWindowsDaemon tests ─────────────────────────────────────────────────
+
describe('restartWindowsDaemon', () => {
- beforeEach(() => {
- vi.resetModules();
- state.pidContents = [''];
- state.pidIndex = 0;
- state.scheduledTaskRunOk = false;
- state.vbsExists = false;
- state.startupCmdExists = false;
- state.alivePids = new Set();
- state.execCalls = [];
- state.spawnCalls = [];
- vi.spyOn(process, 'kill').mockImplementation(((pid: number) => {
- if (!state.alivePids.has(pid)) throw new Error('not running');
- return true;
- }) as typeof process.kill);
- });
+ beforeEach(reset);
+
+ // ── Launcher priority ──
- it('returns false when no restart path is available', async () => {
+ it('returns false when no launcher path is available', async () => {
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(false);
expect(state.spawnCalls).toHaveLength(0);
});
- it('kills daemon and launches VBS when available', async () => {
- state.pidContents = ['123', '456'];
- state.alivePids = new Set([456]);
+ it('prefers VBS launcher over scheduled task', async () => {
+ state.pidContents = ['', '100'];
+ state.alivePids = new Set([100]);
state.vbsExists = true;
+ state.scheduledTaskRunOk = true;
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(true);
- // Should kill daemon
- expect(state.execCalls).toContain('taskkill /f /pid 123');
- // Should launch VBS (preferred over schtask)
- expect(state.spawnCalls[0]).toEqual(
- expect.objectContaining({ cmd: 'wscript', args: expect.arrayContaining(['C:\\Users\\tester\\.imcodes\\daemon-launcher.vbs']) }),
- );
+ expect(state.spawnCalls[0].cmd).toBe('wscript');
+ // Should NOT have called schtasks since VBS succeeded
+ expect(state.execCalls.some(c => c.includes('schtasks /Run'))).toBe(false);
});
- it('falls back to scheduled task when VBS is not available', async () => {
- state.pidContents = ['123', '456'];
- state.alivePids = new Set([456]);
+ it('falls back to scheduled task when VBS missing', async () => {
+ state.pidContents = ['', '200'];
+ state.alivePids = new Set([200]);
state.scheduledTaskRunOk = true;
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(true);
- expect(state.execCalls).toContain('taskkill /f /pid 123');
+ expect(state.spawnCalls).toHaveLength(0);
expect(state.execCalls).toContain('schtasks /Run /TN imcodes-daemon');
});
it('falls back to startup shortcut as last resort', async () => {
- state.pidContents = ['', '901'];
- state.alivePids = new Set([901]);
+ state.pidContents = ['', '300'];
+ state.alivePids = new Set([300]);
state.startupCmdExists = true;
const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
expect(restartWindowsDaemon()).toBe(true);
expect(state.spawnCalls).toHaveLength(1);
expect(state.spawnCalls[0].cmd).toMatch(/cmd(\.exe)?$/i);
- expect(state.spawnCalls[0].args).toEqual(['/c', 'C:\\Users\\tester\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\imcodes-daemon.cmd']);
+ });
+
+ // ── Daemon kill ──
+
+ it('kills existing daemon by PID before launching', async () => {
+ state.pidContents = ['555', '666'];
+ state.alivePids = new Set([666]);
+ state.vbsExists = true;
+
+ const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
+ expect(restartWindowsDaemon()).toBe(true);
+ expect(state.execCalls).toContain('taskkill /f /pid 555');
+ });
+
+ it('handles daemon already dead gracefully', async () => {
+ // PID file has a value but process is not running — should not throw
+ state.pidContents = ['999', '888'];
+ state.alivePids = new Set([888]);
+ state.vbsExists = true;
+
+ const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
+ expect(restartWindowsDaemon()).toBe(true);
+ // taskkill was attempted (and silently failed)
+ expect(state.execCalls).toContain('taskkill /f /pid 999');
+ });
+
+ // ── PID wait logic ──
+
+ it('waits for new PID different from old', async () => {
+ // First read: old PID 100, second read: new PID 200
+ state.pidContents = ['100', '200'];
+ state.alivePids = new Set([200]);
+ state.vbsExists = true;
+
+ const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
+ expect(restartWindowsDaemon()).toBe(true);
+ });
+
+ it('works when no previous PID exists (fresh start)', async () => {
+ state.pidContents = ['', '500'];
+ state.alivePids = new Set([500]);
+ state.vbsExists = true;
+
+ const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
+ expect(restartWindowsDaemon()).toBe(true);
+ });
+
+ it('skips own PID when passed as currentPid', async () => {
+ // PID file contains our own PID — should treat as "no previous daemon"
+ state.pidContents = ['42', '700'];
+ state.alivePids = new Set([700]);
+ state.vbsExists = true;
+
+ const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
+ // currentPid=42 matches PID file → no taskkill
+ expect(restartWindowsDaemon(42)).toBe(true);
+ expect(state.execCalls.some(c => c.includes('taskkill'))).toBe(false);
+ });
+
+ // ── windowsHide on all spawn calls ──
+
+ it('passes windowsHide to VBS spawn', async () => {
+ state.pidContents = ['', '100'];
+ state.alivePids = new Set([100]);
+ state.vbsExists = true;
+
+ const { spawn } = await import('node:child_process');
+ const { restartWindowsDaemon } = await import('../../src/util/windows-daemon.js');
+ restartWindowsDaemon();
+ const call = (spawn as ReturnType).mock.calls[0];
+ expect(call[2]).toEqual(expect.objectContaining({ windowsHide: true }));
});
});
diff --git a/test/util/windows-launch-artifacts.test.ts b/test/util/windows-launch-artifacts.test.ts
new file mode 100644
index 00000000..70db7add
--- /dev/null
+++ b/test/util/windows-launch-artifacts.test.ts
@@ -0,0 +1,149 @@
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+
+// ── Mock fs so writeWatchdogCmd doesn't touch disk ─────────────────────────
+
+const written: Record = {};
+
+vi.mock('fs/promises', () => ({
+ writeFile: vi.fn(async (path: string, content: string) => { written[path] = content; }),
+ mkdir: vi.fn(async () => undefined),
+ stat: vi.fn(async () => ({ size: 0 })),
+ truncate: vi.fn(async () => undefined),
+}));
+
+vi.mock('fs', () => ({
+ existsSync: vi.fn((path: string) => {
+ // Simulate npm global shim exists
+ if (path.endsWith('imcodes.cmd')) return true;
+ return false;
+ }),
+}));
+
+vi.mock('child_process', () => ({
+ execSync: vi.fn(() => ''),
+}));
+
+describe('writeWatchdogCmd', () => {
+ beforeEach(() => {
+ for (const k of Object.keys(written)) delete written[k];
+ });
+
+ it('generates watchdog with upgrade lock check', async () => {
+ const { writeWatchdogCmd, UPGRADE_LOCK_FILE } = await import('../../src/util/windows-launch-artifacts.js');
+ const paths = {
+ nodeExe: 'C:\\Program Files\\nodejs\\node.exe',
+ imcodesScript: 'C:\\Users\\X\\AppData\\Roaming\\npm\\node_modules\\imcodes\\dist\\src\\index.js',
+ watchdogPath: 'C:\\Users\\X\\.imcodes\\daemon-watchdog.cmd',
+ vbsPath: 'C:\\Users\\X\\.imcodes\\daemon-launcher.vbs',
+ logPath: 'C:\\Users\\X\\.imcodes\\watchdog.log',
+ };
+
+ await writeWatchdogCmd(paths);
+
+ const cmd = written[paths.watchdogPath];
+ expect(cmd).toBeDefined();
+
+ // Must check lock file BEFORE launching daemon
+ const lockCheck = cmd.indexOf('if exist');
+ const launchCmd = cmd.indexOf('start --foreground');
+ expect(lockCheck).toBeGreaterThan(-1);
+ expect(launchCmd).toBeGreaterThan(lockCheck);
+
+ // Lock file path must be in the check
+ const lockPath = UPGRADE_LOCK_FILE.replace(/\//g, '\\');
+ expect(cmd).toContain(lockPath);
+
+ // When locked, should wait and loop back (not launch daemon)
+ expect(cmd).toContain('Upgrade in progress, waiting');
+ expect(cmd).toContain('goto loop');
+ });
+
+ it('uses npm global shim instead of hard-coded node+script paths', async () => {
+ const { writeWatchdogCmd } = await import('../../src/util/windows-launch-artifacts.js');
+ const paths = {
+ nodeExe: 'C:\\Program Files\\nodejs\\node.exe',
+ imcodesScript: 'C:\\Users\\X\\AppData\\Roaming\\npm\\node_modules\\imcodes\\dist\\src\\index.js',
+ watchdogPath: 'C:\\Users\\X\\.imcodes\\daemon-watchdog.cmd',
+ vbsPath: 'C:\\Users\\X\\.imcodes\\daemon-launcher.vbs',
+ logPath: 'C:\\Users\\X\\.imcodes\\watchdog.log',
+ };
+
+ await writeWatchdogCmd(paths);
+ const cmd = written[paths.watchdogPath];
+
+ // Should use the shim, not the raw node+script
+ expect(cmd).toContain('imcodes.cmd');
+ expect(cmd).not.toContain('node_modules');
+ });
+
+ it('falls back to node+script when shim not found', async () => {
+ // Override existsSync to return false for shim
+ const { existsSync } = await import('fs');
+ (existsSync as ReturnType).mockReturnValue(false);
+
+ const { writeWatchdogCmd } = await import('../../src/util/windows-launch-artifacts.js');
+ const paths = {
+ nodeExe: 'C:\\Program Files\\nodejs\\node.exe',
+ imcodesScript: 'C:\\dev\\imcodes\\dist\\src\\index.js',
+ watchdogPath: 'C:\\Users\\X\\.imcodes\\daemon-watchdog.cmd',
+ vbsPath: 'C:\\Users\\X\\.imcodes\\daemon-launcher.vbs',
+ logPath: 'C:\\Users\\X\\.imcodes\\watchdog.log',
+ };
+
+ await writeWatchdogCmd(paths);
+ const cmd = written[paths.watchdogPath];
+
+ // Should fall back to direct node+script
+ expect(cmd).toContain('node.exe');
+ expect(cmd).toContain('index.js');
+ });
+
+ it('watchdog is an infinite loop with 5s retry', async () => {
+ const { writeWatchdogCmd } = await import('../../src/util/windows-launch-artifacts.js');
+ const paths = {
+ nodeExe: 'node.exe',
+ imcodesScript: 'C:\\npm\\node_modules\\imcodes\\dist\\src\\index.js',
+ watchdogPath: 'out.cmd',
+ vbsPath: 'out.vbs',
+ logPath: 'out.log',
+ };
+
+ await writeWatchdogCmd(paths);
+ const cmd = written[paths.watchdogPath];
+
+ expect(cmd).toContain(':loop');
+ expect(cmd).toContain('goto loop');
+ expect(cmd).toContain('timeout /t 5');
+ });
+});
+
+describe('writeVbsLauncher', () => {
+ beforeEach(() => {
+ for (const k of Object.keys(written)) delete written[k];
+ });
+
+ it('runs watchdog CMD hidden (window style 0)', async () => {
+ const { writeVbsLauncher } = await import('../../src/util/windows-launch-artifacts.js');
+ const paths = {
+ nodeExe: '', imcodesScript: '', logPath: '',
+ watchdogPath: 'C:\\Users\\X\\.imcodes\\daemon-watchdog.cmd',
+ vbsPath: 'C:\\Users\\X\\.imcodes\\daemon-launcher.vbs',
+ };
+
+ await writeVbsLauncher(paths);
+ const vbs = written[paths.vbsPath];
+
+ expect(vbs).toContain('WshShell.Run');
+ expect(vbs).toContain('daemon-watchdog.cmd');
+ // Window style 0 = hidden
+ expect(vbs).toContain(', 0, False');
+ });
+});
+
+describe('UPGRADE_LOCK_FILE', () => {
+ it('is under .imcodes directory', async () => {
+ const { UPGRADE_LOCK_FILE } = await import('../../src/util/windows-launch-artifacts.js');
+ expect(UPGRADE_LOCK_FILE).toContain('.imcodes');
+ expect(UPGRADE_LOCK_FILE).toContain('upgrade.lock');
+ });
+});
diff --git a/test/util/windows-upgrade-script.test.ts b/test/util/windows-upgrade-script.test.ts
index 39ae412a..be6254a7 100644
--- a/test/util/windows-upgrade-script.test.ts
+++ b/test/util/windows-upgrade-script.test.ts
@@ -1,6 +1,17 @@
import { describe, expect, it } from 'vitest';
import { buildWindowsCleanupScript, buildWindowsUpgradeBatch } from '../../src/util/windows-upgrade-script.js';
+const INPUT = {
+ logFile: 'C:\\Temp\\upgrade.log',
+ scriptDir: 'C:\\Temp\\imcodes-upgrade-123',
+ cleanupPath: 'C:\\Temp\\imcodes-upgrade-123\\cleanup.cmd',
+ npmCmd: 'C:\\Program Files\\nodejs\\npm.cmd',
+ pkgSpec: 'imcodes@1.2.3',
+ targetVer: '1.2.3',
+ vbsLauncherPath: 'C:\\Users\\tester\\.imcodes\\daemon-launcher.vbs',
+ upgradeLockFile: 'C:\\Users\\tester\\.imcodes\\upgrade.lock',
+} as const;
+
describe('buildWindowsCleanupScript', () => {
it('generates a standalone cleanup cmd script', () => {
const script = buildWindowsCleanupScript('C:\\Temp\\imcodes-upgrade-123');
@@ -11,76 +22,136 @@ describe('buildWindowsCleanupScript', () => {
});
describe('buildWindowsUpgradeBatch', () => {
- const batch = buildWindowsUpgradeBatch({
- logFile: 'C:\\Temp\\upgrade.log',
- scriptDir: 'C:\\Temp\\imcodes-upgrade-123',
- cleanupPath: 'C:\\Temp\\imcodes-upgrade-123\\cleanup.cmd',
- npmCmd: 'C:\\Program Files\\nodejs\\npm.cmd',
- pkgSpec: 'imcodes@1.2.3',
- targetVer: '1.2.3',
- vbsLauncherPath: 'C:\\Users\\tester\\.imcodes\\daemon-launcher.vbs',
- upgradeLockFile: 'C:\\Users\\tester\\.imcodes\\upgrade.lock',
- });
-
- it('creates upgrade lock before npm install', () => {
- const lockIdx = batch.indexOf('echo upgrade > "C:\\Users\\tester\\.imcodes\\upgrade.lock"');
- const installIdx = batch.indexOf('call "C:\\Program Files\\nodejs\\npm.cmd" install');
+ const batch = buildWindowsUpgradeBatch(INPUT);
+
+ // ── Lock file lifecycle ──
+
+ it('creates upgrade lock BEFORE npm install', () => {
+ const lockIdx = batch.indexOf(`echo upgrade > "${INPUT.upgradeLockFile}"`);
+ const installIdx = batch.indexOf(`call "${INPUT.npmCmd}" install`);
expect(lockIdx).toBeGreaterThan(-1);
expect(installIdx).toBeGreaterThan(-1);
expect(lockIdx).toBeLessThan(installIdx);
});
- it('removes lock on every abort path so watchdog resumes', () => {
- const abortBlocks = batch.split('goto :done');
- // At least 4 abort paths (install fail, no prefix, no shim, version mismatch)
- const lockDeletes = abortBlocks.filter(b => b.includes('del "C:\\Users\\tester\\.imcodes\\upgrade.lock"'));
- expect(lockDeletes.length).toBeGreaterThanOrEqual(4);
+ it('every abort path deletes lock AND restarts VBS', () => {
+ // Split on `goto :done` — each abort block must have both del lock + wscript
+ const blocks = batch.split('goto :done');
+ // Last block is after :done label — skip it
+ const abortBlocks = blocks.slice(0, -1);
+ // At least 4 abort paths: install fail, no prefix, no shim, version mismatch
+ expect(abortBlocks.length).toBeGreaterThanOrEqual(4);
+ for (const block of abortBlocks) {
+ expect(block).toContain(`del "${INPUT.upgradeLockFile}"`);
+ expect(block).toContain('wscript');
+ }
});
- it('starts new watchdog via VBS then removes lock after successful install', () => {
- // After the actual repair-watchdog CLI call, batch should start VBS then remove lock
- const repairCallIdx = batch.indexOf('call "%CLI_SHIM%" repair-watchdog');
- const afterRepair = batch.slice(repairCallIdx);
+ it('success path: starts new watchdog via VBS then removes lock', () => {
+ const repairIdx = batch.indexOf('call "%CLI_SHIM%" repair-watchdog');
+ const afterRepair = batch.slice(repairIdx);
const vbsIdx = afterRepair.indexOf('Starting new watchdog via VBS');
const delIdx = afterRepair.indexOf('Removing upgrade lock');
expect(vbsIdx).toBeGreaterThan(-1);
- expect(delIdx).toBeGreaterThan(vbsIdx);
- expect(afterRepair).toContain('del "C:\\Users\\tester\\.imcodes\\upgrade.lock"');
+ expect(delIdx).toBeGreaterThan(-1);
+ // VBS start must come BEFORE lock removal — watchdog waits on lock
+ expect(vbsIdx).toBeLessThan(delIdx);
});
- it('kills daemon before npm install', () => {
+ // ── Daemon + old watchdog kill ──
+
+ it('kills old watchdog tree before npm install', () => {
+ const killTreeIdx = batch.indexOf('taskkill /f /t /pid !WD_PID!');
+ const installIdx = batch.indexOf(`call "${INPUT.npmCmd}" install`);
+ expect(killTreeIdx).toBeGreaterThan(-1);
+ expect(killTreeIdx).toBeLessThan(installIdx);
+ });
+
+ it('kills daemon directly as belt-and-suspenders', () => {
expect(batch).toContain('taskkill /f /pid !OLD_PID!');
});
- it('installs the requested package with a quoted npm path', () => {
- expect(batch).toContain('call "C:\\Program Files\\nodejs\\npm.cmd" install -g imcodes@1.2.3');
+ it('finds watchdog parent via wmic', () => {
+ expect(batch).toContain('wmic process where');
+ expect(batch).toContain('ParentProcessId');
});
- it('verifies the installed CLI shim and version before restart', () => {
- expect(batch).toContain('set "NPM_PREFIX="');
- expect(batch).toContain('prefix -g');
+ // ── npm install ──
+
+ it('installs with quoted npm path', () => {
+ expect(batch).toContain(`call "${INPUT.npmCmd}" install -g ${INPUT.pkgSpec}`);
+ });
+
+ // ── Version verification ──
+
+ it('verifies installed CLI shim exists', () => {
expect(batch).toContain('set "CLI_SHIM=%NPM_PREFIX%\\imcodes.cmd"');
expect(batch).toContain('if not exist "%CLI_SHIM%"');
- expect(batch).toContain('set "INSTALLED_VER="');
+ });
+
+ it('verifies installed version matches target', () => {
expect(batch).toContain('call "%CLI_SHIM%" --version');
- expect(batch).toContain('if /I not "%INSTALLED_VER%"=="1.2.3"');
+ expect(batch).toContain(`if /I not "%INSTALLED_VER%"=="${INPUT.targetVer}"`);
+ });
+
+ // ── repair-watchdog ──
+
+ it('calls repair-watchdog after successful install', () => {
+ const installOkIdx = batch.indexOf('Install succeeded');
+ const repairIdx = batch.indexOf('call "%CLI_SHIM%" repair-watchdog');
+ expect(repairIdx).toBeGreaterThan(installOkIdx);
});
- it('lets watchdog restart daemon after lock removal (no manual restart)', () => {
- // After successful upgrade, should NOT call `imcodes restart`
- // — the watchdog loop will detect lock removal and restart automatically
- const afterLockRemoval = batch.split('Removing upgrade lock')[1] ?? '';
- expect(afterLockRemoval).not.toContain('imcodes restart');
- expect(afterLockRemoval).not.toContain('CLI_SHIM%" restart');
+ // ── Success path does NOT use imcodes restart ──
+
+ it('does not call imcodes restart on success path', () => {
+ // After repair-watchdog, should use VBS + lock removal, not CLI restart
+ const afterRepair = batch.slice(batch.indexOf('call "%CLI_SHIM%" repair-watchdog'));
+ expect(afterRepair).not.toContain('CLI_SHIM%" restart');
});
- it('runs health check after watchdog restarts daemon', () => {
+ // ── Health check ──
+
+ it('runs health check after daemon restart', () => {
expect(batch).toContain('Health check PASSED');
expect(batch).toContain('Health check FAILED');
- expect(batch).toContain('tasklist /fi "PID eq !DAEMON_PID!"');
+ expect(batch).toContain('daemon.pid not found');
});
+ // ── No visible windows ──
+
it('uses minimized cleanup windows', () => {
- expect(batch).toContain('start "" /min cmd /c "C:\\Temp\\imcodes-upgrade-123\\cleanup.cmd" >nul 2>&1');
+ const cleanupCalls = batch.match(/start.*cmd.*cleanup/g) ?? [];
+ for (const call of cleanupCalls) {
+ expect(call).toContain('/min');
+ }
+ expect(cleanupCalls.length).toBeGreaterThan(0);
+ });
+
+ // ── Recovery guarantee: daemon is never left dead ──
+
+ it('daemon is always restarted — every code path ends with either VBS launch or lock removal', () => {
+ // Count all paths that could leave the function:
+ // 1. Each `goto :done` abort block must restart via VBS
+ // 2. The success path must start VBS + remove lock
+ // 3. The :done label itself just logs — that's fine since abort blocks already restarted
+
+ const abortBlocks = batch.split('goto :done').slice(0, -1);
+ for (const block of abortBlocks) {
+ // Every abort must restart the daemon via VBS
+ expect(block).toContain(`wscript "${INPUT.vbsLauncherPath}"`);
+ }
+
+ // Success path must start new watchdog
+ const successPath = batch.slice(batch.indexOf('Regenerating daemon launch chain'));
+ expect(successPath).toContain(`wscript "${INPUT.vbsLauncherPath}"`);
+ });
+});
+
+describe('buildWindowsUpgradeBatch with latest target', () => {
+ it('skips version comparison when target is "latest"', () => {
+ const batch = buildWindowsUpgradeBatch({ ...INPUT, targetVer: 'latest' });
+ expect(batch).toContain('if not "latest"=="latest"');
+ // The condition `if not "latest"=="latest"` is always false → skip mismatch abort
});
});
From 2290e7c125c5597f28da2e70ac1566626f5f90c6 Mon Sep 17 00:00:00 2001
From: "IM.codes"
Date: Mon, 6 Apr 2026 11:02:30 +0800
Subject: [PATCH 05/57] Add SDK transport support for Claude and Codex
---
README.md | 16 +-
landing/imcodes-watch0.png | Bin 0 -> 34727 bytes
landing/imcodes-watch1.png | Bin 0 -> 48093 bytes
landing/imcodes-watch2.png | Bin 0 -> 55520 bytes
landing/index.html | 37 +-
package-lock.json | 1578 ++++++++++++++++-
package.json | 4 +-
src/agent/detect.ts | 5 +-
src/agent/provider-registry.ts | 8 +
src/agent/providers/claude-code-sdk.ts | 350 ++++
src/agent/providers/codex-sdk.ts | 310 ++++
src/agent/session-manager.ts | 45 +
src/agent/transport-provider.ts | 19 +
src/agent/transport-session-runtime.ts | 9 +-
src/daemon/lifecycle.ts | 9 +-
test/agent/claude-code-sdk-provider.test.ts | 119 ++
test/agent/codex-sdk-provider.test.ts | 100 ++
test/agent/provider-registry.test.ts | 80 +-
test/daemon/sdk-transport-restore.test.ts | 186 ++
test/e2e/sdk-transport-flow.test.ts | 222 +++
web/src/components/NewSessionDialog.tsx | 4 +-
web/src/i18n/locales/en.json | 4 +-
web/src/i18n/locales/es.json | 4 +-
web/src/i18n/locales/ja.json | 4 +-
web/src/i18n/locales/ko.json | 4 +-
web/src/i18n/locales/ru.json | 4 +-
web/src/i18n/locales/zh-CN.json | 4 +-
web/src/i18n/locales/zh-TW.json | 4 +-
web/src/pages/AutoFixControls.tsx | 2 +-
web/src/pages/ProjectSettings.tsx | 2 +-
web/src/watch-projection.ts | 2 +
web/test/components/NewSessionDialog.test.tsx | 4 +-
32 files changed, 3070 insertions(+), 69 deletions(-)
create mode 100644 landing/imcodes-watch0.png
create mode 100644 landing/imcodes-watch1.png
create mode 100644 landing/imcodes-watch2.png
create mode 100644 src/agent/providers/claude-code-sdk.ts
create mode 100644 src/agent/providers/codex-sdk.ts
create mode 100644 test/agent/claude-code-sdk-provider.test.ts
create mode 100644 test/agent/codex-sdk-provider.test.ts
create mode 100644 test/daemon/sdk-transport-restore.test.ts
create mode 100644 test/e2e/sdk-transport-flow.test.ts
diff --git a/README.md b/README.md
index 5bc929d5..9a86f456 100644
--- a/README.md
+++ b/README.md
@@ -38,11 +38,21 @@ A specialized instant messenger for AI agents. Keep long-running coding-agent se
+### Apple Watch
+
+
+
+
+
+
+
+Watch support covers quick session monitoring, unread counts, push notifications, and quick replies directly from the wrist.
+
## Download
-Also available as a [web app](https://app.im.codes) and via `npm install -g imcodes` (daemon CLI).
+Supports iPhone, iPad, and Apple Watch. Also available as a [web app](https://app.im.codes) and via `npm install -g imcodes` (daemon CLI).
## Why
@@ -68,9 +78,9 @@ Browse project files with a tree view. Upload files, images, and photos from any
Preview your local dev server from any device — phone, tablet, or remote browser — without deploying. The daemon proxies `localhost` traffic through a secure WebSocket tunnel to the server. HTML rewriting and a runtime patch handle URL remapping so links, fetch, and WebSocket connections just work. Supports HMR/hot-reload via WebSocket tunneling. No public URLs, no third-party tunnels — traffic stays within your IM.codes server.
-### Mobile & Notifications
+### Mobile, Watch & Notifications
-Full mobile support with biometric auth and push notifications. Shell sessions allow interactive keyboard input on mobile (SSH-like). Sub-session preview cards always show latest messages. Toast notifications navigate directly to the relevant session.
+Full mobile support with biometric auth and push notifications. Shell sessions allow interactive keyboard input on mobile (SSH-like). Sub-session preview cards always show latest messages. Toast notifications navigate directly to the relevant session. Apple Watch support adds quick session monitoring, unread counts, and quick replies from the wrist.
### Multi-Agent Discussions & Audit
diff --git a/landing/imcodes-watch0.png b/landing/imcodes-watch0.png
new file mode 100644
index 0000000000000000000000000000000000000000..c8ae8eb7069e1954766f8b6ab69773a7ce801d71
GIT binary patch
literal 34727
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
z
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
literal 0
HcmV?d00001
diff --git a/landing/imcodes-watch1.png b/landing/imcodes-watch1.png
new file mode 100644
index 0000000000000000000000000000000000000000..17418906b5a0eff616ea43f955b0b6c9c1b64180
GIT binary patch
literal 48093
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