Skip to content
Open
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
19 changes: 17 additions & 2 deletions apps/cli/src/commands/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
resolveExecutableSiblingPackageJson,
renderLaunchdPlist,
renderServiceWrapper,
renderWindowsLauncher,
renderSystemdTimer,
runServiceAutoUpdate,
scheduleDescription,
Expand Down Expand Up @@ -596,10 +597,24 @@ describe("native scheduler templates", () => {
"/MO",
"5",
"/TR",
'"C:\\Users\\alex\\AppData\\Roaming\\tokenmaxxing/service-sync.cmd"',
'wscript.exe //nologo //b "C:\\Users\\alex\\AppData\\Roaming\\tokenmaxxing/service-sync-launcher.vbs"',
"/F",
]);
});

it("renders a hidden VBScript launcher for the sync wrapper", () => {
const launcher = renderWindowsLauncher(
"C:\\Users\\alex\\AppData\\Roaming\\tokenmaxxing/service-sync.cmd",
);

// window style 0 = hidden, bWaitOnReturn = True so the wrapper's exit code propagates.
expect(launcher).toContain(
'shell.Run("""C:\\Users\\alex\\AppData\\Roaming\\tokenmaxxing/service-sync.cmd""", 0, True)',
);
expect(launcher).toContain("WScript.Quit exitCode");
// Must never open a console window: no direct cmd/console invocation.
expect(launcher).not.toContain("cmd.exe");
});
});

describe("legacyServiceWrapperPaths", () => {
Expand Down Expand Up @@ -1972,7 +1987,7 @@ describe("serviceInstallProgram", () => {
installedAt: "2026-06-16T12:00:00.000Z",
runnerTarget: "darwin-arm64",
runnerVersion: "0.4.17",
templateVersion: 5,
templateVersion: 6,
});
expect(written[0]?.metadata).not.toHaveProperty("autoUpdate");
expect(state.logs).toContain("Automatic sync installed");
Expand Down
37 changes: 34 additions & 3 deletions apps/cli/src/commands/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ const gunzipPromise = promisify(gunzip);
const require = createRequire(import.meta.url);

const SERVICE_LABEL = "sh.tokenmaxxing.sync";
const SERVICE_TEMPLATE_VERSION = 5;
const SERVICE_TEMPLATE_VERSION = 6;
const SYSTEMD_NAME = "tokenmaxxing-sync";
const WINDOWS_TASK_NAME = "tokenmaxxing-sync";
const POSIX_WRAPPER_NAME = "tokenmaxxing.sh";
const LEGACY_POSIX_WRAPPER_NAME = "service-sync.sh";
const WINDOWS_WRAPPER_NAME = "service-sync.cmd";
const WINDOWS_LAUNCHER_NAME = "service-sync-launcher.vbs";
const PACKAGE_NAME = "@851-labs/tokenmaxxing";
const SERVICE_RUNNER_DIR_NAME = "service-runners";
const SERVICE_RUNNER_POINTER_NAME = "service-runner-current";
Expand Down Expand Up @@ -3301,6 +3302,10 @@ function windowsTaskNames(): string[] {
return [windowsTaskName(), ...LEGACY_SCHEDULE_TIMES.map((time) => legacyWindowsTaskName(time))];
}

function windowsLauncherPath(paths: ServicePaths): string {
return join(paths.configDir, WINDOWS_LAUNCHER_NAME);
}

function renderLaunchdStartInterval(): string {
return String(SERVICE_INTERVAL_SECONDS);
}
Expand Down Expand Up @@ -3851,6 +3856,21 @@ exit /b %ERRORLEVEL%\r
`;
}

// schtasks can only register a task that runs in the interactive session, so pointing the task
// action straight at the .cmd wrapper (a console program) makes a console window flash on every
// run — every 5 minutes — stealing focus from full-screen apps and games. wscript.exe is a
// GUI-subsystem host, so running the wrapper through this tiny .vbs with window style 0 performs
// the sync with no window at all, while still propagating the wrapper's exit code to Task Scheduler.
function renderWindowsLauncher(wrapperPath: string): string {
const command = vbsQuote(`"${wrapperPath}"`);
return `' Auto-generated by tokenmaxxing. Runs the sync wrapper without a console window.\r
Dim shell, exitCode\r
Set shell = CreateObject("WScript.Shell")\r
exitCode = shell.Run(${command}, 0, True)\r
WScript.Quit exitCode\r
`;
}

function renderWindowsLogRotation(logPath: string): string {
const quotedLogPath = cmdQuote(logPath);
const moves = Array.from({ length: SERVICE_LOG_ROTATIONS - 1 }, (_, index) => {
Expand Down Expand Up @@ -3929,7 +3949,9 @@ function writeServiceFiles(
await rm(legacyWrapperPath, { force: true });
}
await writeFileAtomic(paths.wrapperPath, wrapper);
if (paths.backend !== "windows-task-scheduler") {
if (paths.backend === "windows-task-scheduler") {
await writeFileAtomic(windowsLauncherPath(paths), renderWindowsLauncher(paths.wrapperPath));
} else {
await chmod(paths.wrapperPath, 0o755);
}
await writeFileAtomic(paths.metadataPath, `${JSON.stringify(metadata, null, 2)}\n`);
Expand All @@ -3950,6 +3972,9 @@ function removeServiceFiles(paths: ServicePaths): Effect.Effect<void, unknown> {
return Effect.tryPromise({
try: async () => {
await rm(paths.wrapperPath, { force: true });
if (paths.backend === "windows-task-scheduler") {
await rm(windowsLauncherPath(paths), { force: true });
}
for (const legacyWrapperPath of legacyServiceWrapperPaths(paths)) {
await rm(legacyWrapperPath, { force: true });
}
Expand Down Expand Up @@ -4017,7 +4042,7 @@ function windowsTaskCreateArgs(paths: ServicePaths): string[] {
"/MO",
String(SERVICE_INTERVAL_MINUTES),
"/TR",
cmdQuote(paths.wrapperPath),
`wscript.exe //nologo //b ${cmdQuote(windowsLauncherPath(paths))}`,
"/F",
];
}
Expand Down Expand Up @@ -4393,6 +4418,11 @@ function cmdQuote(value: string): string {
return `"${value.replaceAll('"', '\\"')}"`;
}

// VBScript string literals are double-quote delimited and escape an embedded quote by doubling it.
function vbsQuote(value: string): string {
return `"${value.replaceAll('"', '""')}"`;
}

function escapeCmdSetValue(value: string): string {
return value.replaceAll('"', '\\"');
}
Expand Down Expand Up @@ -4466,6 +4496,7 @@ export {
verifyNpmIntegrity,
windowsTaskNames,
windowsTaskCreateArgs,
renderWindowsLauncher,
ServiceCommandNotFoundError,
ServiceEnvTokenError,
ServiceEphemeralCommandError,
Expand Down