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
7 changes: 7 additions & 0 deletions .github/workflows/windows-recovery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ on:
- 'scripts/clean-build.mjs'
- 'scripts/clean-paths.mjs'
- 'scripts/windows-runtime-host-local-ipc-trust.ps1'
# The desktop launcher executes cmd.exe with native console code pages.
- 'apps/desktop/src/main/opencli-chrome.ts'
- 'apps/desktop/src/main/__tests__/opencli-chrome.test.ts'
# Every file in the import closure of the suites below that branches on
# `process.platform === 'win32'`. Generated, not curated:
# `scripts/windows-package-source-closure.test.mjs` recomputes this set
Expand Down Expand Up @@ -184,6 +187,10 @@ jobs:
- name: Build test artifacts
run: npm.cmd run build:test

- name: Verify Windows opencli launchers
shell: pwsh
run: node.exe --test apps/desktop/dist/main/__tests__/opencli-chrome.test.js

- name: Verify managed dependency alternate streams
shell: pwsh
run: |
Expand Down
63 changes: 60 additions & 3 deletions apps/desktop/src/main/__tests__/opencli-chrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
*/

import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { execFileSync, spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { test } from 'node:test';
Expand All @@ -45,10 +45,67 @@ test('Windows launchers set Node mode before starting the entry', () => {
assert.equal(launchers.command, join(dir, 'opencli-mcp.cmd'));
assert.equal(
readFileSync(launchers.host, 'utf8'),
'@echo off\r\nset ELECTRON_RUN_AS_NODE=1\r\n"C:\\Maka\\Maka.exe" "C:\\Maka\\main.js" host\r\n',
'@echo off\r\nsetlocal DisableDelayedExpansion\r\nfor /f "tokens=2 delims=:." %%c in (\'chcp\') do set "MAKA_OPENCLI_CODE_PAGE=%%c"\r\nchcp 65001 >nul\r\nset ELECTRON_RUN_AS_NODE=1\r\n"C:\\Maka\\Maka.exe" "C:\\Maka\\main.js" host\r\nset "MAKA_OPENCLI_EXIT_CODE=%ERRORLEVEL%"\r\nchcp %MAKA_OPENCLI_CODE_PAGE% >nul\r\nexit /b %MAKA_OPENCLI_EXIT_CODE%\r\n',
);
});

test('Windows launchers preserve Unicode and batch metacharacters without adding protocol output', { skip: process.platform !== 'win32' }, (t) => {
const root = mkdtempSync(join(tmpdir(), 'maka-opencli-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const directory = join(root, '中文 %MAKA_LAUNCHER_TEST% !MAKA_LAUNCHER_TEST! & files');
mkdirSync(directory);
const entry = join(directory, 'entry.mjs');
writeFileSync(entry, 'console.log(JSON.stringify({ args: process.argv.slice(2), node: process.env.ELECTRON_RUN_AS_NODE })); process.exitCode = 23;');
writeOpencliLaunchers(directory, 'win32', process.execPath, entry);
for (const page of [936, 65001]) {
for (const [file, mode] of [['opencli-mcp.cmd', 'stdio'], ['opencli-mcp-host.cmd', 'host']]) {
// CALL returns to the same console, where the caller must retain its code
// page as well as the child's exit status. Keep diagnostics off stdout.
writeFileSync(join(directory, 'caller.cmd'), `@echo off\r\nchcp ${page} >nul\r\ncall ${file}\r\nset "childExit=%errorlevel%"\r\nchcp >code-page.txt\r\nexit /b %childExit%\r\n`);
const result = spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/v:on', '/s', '/c', 'caller.cmd'], {
cwd: directory,
env: { ...process.env, MAKA_LAUNCHER_TEST: 'expanded' },
encoding: 'utf8',
windowsHide: true,
windowsVerbatimArguments: true,
timeout: 10_000,
});
assert.ifError(result.error);
assert.equal(result.status, 23, result.stderr);
assert.equal(result.stderr, '');
assert.deepEqual(JSON.parse(result.stdout), { args: [mode], node: '1' });
assert.equal(Number(readFileSync(join(directory, 'code-page.txt'), 'utf8').match(/:\s*(\d+)/u)?.[1]), page);
}
}
});

test('Windows launchers restore code pages reported with localized trailing punctuation', { skip: process.platform !== 'win32' }, (t) => {
const directory = mkdtempSync(join(tmpdir(), 'maka-opencli-locale-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
const entry = join(directory, 'entry.mjs');
writeFileSync(entry, 'process.stdout.write("protocol"); process.exitCode = 23;');
for (const page of [850, 936, 65001]) {
const launchers = writeOpencliLaunchers(directory, 'win32', process.execPath, entry);
// Simulate only the localized query response; cmd.exe still executes the
// generated parser, changes the real code page and restores it on return.
const script = readFileSync(launchers.command, 'utf8');
assert.ok(script.includes("in ('chcp')"));
writeFileSync(launchers.command, script.replace("in ('chcp')", `in ('echo Aktive Codepage: ${page}.')`));
writeFileSync(join(directory, 'caller.cmd'), `@echo off\r\nchcp ${page} >nul\r\ncall opencli-mcp.cmd\r\nset "childExit=%errorlevel%"\r\nchcp >code-page.txt\r\nexit /b %childExit%\r\n`);
const result = spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/v:on', '/c', 'caller.cmd'], {
cwd: directory,
encoding: 'utf8',
windowsHide: true,
timeout: 10_000,
});
assert.ifError(result.error);
assert.equal(result.status, 23, result.stderr);
assert.equal(result.stderr, '');
assert.equal(result.stdout, 'protocol');
assert.equal(Number(readFileSync(join(directory, 'code-page.txt'), 'utf8').match(/:\s*(\d+)/u)?.[1]), page);
}
});

test('the host manifest goes to Chrome and existing browsers, never over another live install', () => {
const root = mkdtempSync(join(tmpdir(), 'maka-opencli-'));
const host = join(root, 'opencli-mcp-host');
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/main/opencli-chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ export function writeOpencliLaunchers(
const write = (name: string, args: string): string => {
if (platform === 'win32') {
const file = join(dir, `${name}.cmd`);
writeFileSync(file, `@echo off\r\nset ELECTRON_RUN_AS_NODE=1\r\n"${executable}" "${entry}" ${args}\r\n`);
// cmd reads batch files using its console code page, even when paths are
// quoted. Keep UTF-8 paths literal through both batch expansion phases.
// Unlike environment variables, the console code page outlives SETLOCAL.
// Restore it before returning, without losing the child's exit status.
// chcp's localized response can end the numeric page with a period.
const quotePath = (path: string) => `"${path.replaceAll('%', '%%')}"`;
writeFileSync(file, `@echo off\r\nsetlocal DisableDelayedExpansion\r\nfor /f "tokens=2 delims=:." %%c in ('chcp') do set "MAKA_OPENCLI_CODE_PAGE=%%c"\r\nchcp 65001 >nul\r\nset ELECTRON_RUN_AS_NODE=1\r\n${quotePath(executable)} ${quotePath(entry)} ${args}\r\nset "MAKA_OPENCLI_EXIT_CODE=%ERRORLEVEL%"\r\nchcp %MAKA_OPENCLI_CODE_PAGE% >nul\r\nexit /b %MAKA_OPENCLI_EXIT_CODE%\r\n`);
return file;
}
const file = join(dir, name);
Expand Down