Merge pull request #1727 from HAHH9527/fix/windows-schtasks-tr-261-char-limit

fix(cli): externalize Windows startup PowerShell to .ps1 wrapper (schtasks /TR 261-char limit)
This commit is contained in:
Bohdan Triapitsyn
2026-08-28 01:22:57 +03:00
committed by GitHub
2 changed files with 64 additions and 9 deletions
+35
View File
@@ -43,6 +43,7 @@ import {
resolveServeHost,
resolveServeUiPassword,
} from './cli.js';
import { buildWindowsStartupTaskCommand } from './lib/cli-startup.js';
async function withTempOpenChamberDataDir(fn) {
const previous = process.env.OPENCHAMBER_DATA_DIR;
@@ -1421,3 +1422,37 @@ describe('lifecycle commands with unmanaged explicit ports', () => {
});
});
});
describe('Windows startup task command builder', () => {
it('default-path length stays under 200 chars', () => {
const cmd = buildWindowsStartupTaskCommand(
'C:\\Users\\test\\.config\\openchamber\\bin\\OpenChamber.ps1'
);
expect(cmd).toMatch(/^powershell\.exe -NoProfile -ExecutionPolicy Bypass -File /);
expect(cmd.length).toBeLessThan(200);
});
it('worst-case long path stays under 261-char Task Scheduler ceiling', () => {
// Build a wrapper path >= 180 chars (simulates long OPENCHAMBER_DATA_DIR)
// Overhead = 57 chars (prefix + closing quote), so max wrapper for <261 total is 203
const longPath =
'C:\\Users\\' +
'a'.repeat(139) +
'\\.config\\openchamber\\bin\\OpenChamber.ps1';
expect(longPath.length).toBeGreaterThanOrEqual(180);
const cmd = buildWindowsStartupTaskCommand(longPath);
expect(cmd.length).toBeLessThan(261);
});
it('does NOT inline SetEnvironmentVariable (externalization invariant)', () => {
const cmd = buildWindowsStartupTaskCommand('C:\\wrapper.ps1');
expect(cmd).not.toContain('SetEnvironmentVariable');
});
it('uses -File form, not -Command', () => {
const cmd = buildWindowsStartupTaskCommand('C:\\wrapper.ps1');
expect(cmd).toContain('-File ');
expect(cmd).not.toContain('-Command ');
});
});
+29 -9
View File
@@ -74,6 +74,10 @@ function getMacosStartupWrapperPath() {
return path.join(getDataDir(), 'bin', 'OpenChamber');
}
function getWindowsStartupWrapperPath() {
return path.join(getDataDir(), 'bin', 'OpenChamber.ps1');
}
function collectStartupEnv(options = {}) {
const env = options.envSnapshot === false ? {} : Object.fromEntries(
Object.entries(process.env)
@@ -189,6 +193,24 @@ exec ${startupShellQuote(process.execPath)} ${args}
return wrapperPath;
}
function writeWindowsStartupWrapper(options = {}) {
const wrapperPath = getWindowsStartupWrapperPath();
const envFilePath = getStartupEnvFilePath();
const startupArgs = buildStartupArgs(options).map(powershellQuote).join(' ');
const ps1Content = [
`$envFile=${powershellQuote(envFilePath)}`,
`if (Test-Path $envFile) { Get-Content $envFile | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { $v=$matches[2]; if ($v.StartsWith("'") -and $v.EndsWith("'")) { $v=$v.Substring(1,$v.Length-2).Replace("'\\''","'") }; [Environment]::SetEnvironmentVariable($matches[1], $v, 'Process') } } }`,
`& ${powershellQuote(process.execPath)} ${startupArgs}`,
].join('; ');
fs.mkdirSync(path.dirname(wrapperPath), { recursive: true, mode: 0o700 });
fs.writeFileSync(wrapperPath, ps1Content, { mode: 0o700 });
return wrapperPath;
}
function buildWindowsStartupTaskCommand(wrapperPath) {
return `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${wrapperPath}"`;
}
function buildMacosLaunchAgent(options = {}) {
const wrapperPath = writeMacosStartupWrapper(options);
const args = [wrapperPath];
@@ -318,21 +340,16 @@ function enableStartupService(options = {}) {
return getStartupStatus();
}
const envFilePath = writeStartupEnvFile(options);
const startupArgs = buildStartupArgs(options).map(powershellQuote).join(', ');
const powerShellCommand = [
`$envFile=${powershellQuote(envFilePath)}`,
`if (Test-Path $envFile) { Get-Content $envFile | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { $v=$matches[2]; if ($v.StartsWith("'") -and $v.EndsWith("'")) { $v=$v.Substring(1,$v.Length-2).Replace("'\\''","'") }; [Environment]::SetEnvironmentVariable($matches[1], $v, 'Process') } } }`,
`& ${powershellQuote(process.execPath)} ${startupArgs}`,
].join('; ');
const taskArgs = `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "${powerShellCommand.replace(/"/g, '\\"')}"`;
writeStartupEnvFile(options);
const wrapperPath = writeWindowsStartupWrapper(options);
const taskCommand = buildWindowsStartupTaskCommand(wrapperPath);
runStartupCommand('schtasks.exe', [
'/Create',
'/TN', STARTUP_SERVICE_ID,
'/SC', 'ONLOGON',
'/RL', 'LIMITED',
'/F',
'/TR', taskArgs,
'/TR', taskCommand,
]);
runStartupCommand('schtasks.exe', ['/Run', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
return getStartupStatus();
@@ -359,6 +376,8 @@ function disableStartupService() {
runStartupCommand('schtasks.exe', ['/End', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
runStartupCommand('schtasks.exe', ['/Delete', '/TN', STARTUP_SERVICE_ID, '/F'], { allowFailure: true });
try { fs.unlinkSync(getWindowsStartupWrapperPath()); } catch {}
removeStartupEnvFile();
return getStartupStatus();
}
@@ -367,4 +386,5 @@ export {
getStartupStatus,
enableStartupService,
disableStartupService,
buildWindowsStartupTaskCommand,
};