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
6 changes: 6 additions & 0 deletions scripts/patch-linux-window-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const {
applyLinuxMultiInstanceBootstrapPatch,
patchLinuxMultiInstanceBootstrap,
} = require("./patches/bootstrap.js");
const {
applyAutomationScheduleMultiTimePatch,
patchAutomationScheduleAssets,
} = require("./patches/automation-schedule.js");
const {
applyLinuxChromePluginAutoInstallPatch,
} = require("./patches/chrome-plugin.js");
Expand Down Expand Up @@ -144,6 +148,7 @@ function applyLinuxBrowserUseIabVisibleOnCreatePatch(currentSource) {
module.exports = {
COMPUTER_USE_UI_ENV_VAR,
COMPUTER_USE_UI_SETTINGS_KEY,
applyAutomationScheduleMultiTimePatch,
applyBrowserAnnotationScreenshotPatch,
applyBrowserUseNodeReplApprovalPatch,
applyKeybindsSettingsIndexPatch,
Expand Down Expand Up @@ -200,6 +205,7 @@ module.exports = {
normalizePatchDescriptors,
parseOsRelease,
patchCommentPreloadBundle,
patchAutomationScheduleAssets,
patchExtractedApp,
patchKeybindsSettingsAssets,
patchLinuxMultiInstanceBootstrap,
Expand Down
63 changes: 63 additions & 0 deletions scripts/patch-linux-window-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const vm = require("node:vm");
const {
COMPUTER_USE_UI_ENV_VAR,
COMPUTER_USE_UI_SETTINGS_KEY,
applyAutomationScheduleMultiTimePatch,
applyKeybindsSettingsIndexPatch,
applyLinuxComputerUseFeaturePatch,
applyLinuxComputerUseInstallFlowPatch,
Expand Down Expand Up @@ -53,6 +54,7 @@ const {
patchExtractedApp,
patchPackageJson,
patchLinuxAppUpdaterBridge,
patchAutomationScheduleAssets,
createPatchReport,
corePatchDescriptors,
detectLinuxTargetContext,
Expand Down Expand Up @@ -114,6 +116,66 @@ function captureWarns(fn) {
}
}

function automationScheduleBundleFixture() {
return [
"var Cc={MO:1,TU:2,WE:3,TH:4,FR:5,SA:6,SU:0};",
"function wc(e){let t=Tc(e.byhour),n=Tc(e.byminute);return t!=null&&n!=null?{hour:t,minute:n}:e.dtstart?{hour:e.dtstart.getHours(),minute:e.dtstart.getMinutes()}:null}",
"function Tc(e){return Array.isArray(e)?typeof e[0]==`number`?e[0]:null:typeof e==`number`?e:null}",
"function Ec(e,t){let n=new Date(e),r=new Date(n.getFullYear(),n.getMonth(),n.getDate(),t.hour,t.minute,0,0);return r.getTime()<=e&&r.setDate(r.getDate()+1),r.getTime()}",
"function Dc(e,t,n){let r=new Date(e),i=r.getDay(),a=n.length>0?n:[0,1,2,3,4,5,6];for(let n=0;n<=7;n+=1){let o=(i+n)%7;if(!a.includes(o))continue;let s=new Date(r.getFullYear(),r.getMonth(),r.getDate()+n,t.hour,t.minute,0,0);if(s.getTime()>e)return s.getTime()}return e}",
"function Oc(e){return e?(Array.isArray(e)?e:[e]).map(e=>{if(typeof e==`number`)return Ac(e);if(kc(e))return Ac(e.weekday);let t=String(e);return t in Cc?Cc[t]:null}).filter(e=>e!=null):[]}",
"function kc(e){return typeof e!=`object`||!e||!(`weekday`in e)?!1:typeof e.weekday==`number`}",
"function Ac(e){return!Number.isInteger(e)||e<0||e>6?null:(e+1)%7}",
"var jc=`codex_chronicle`;",
].join("");
}

function evaluateAutomationSchedule(source, now, options) {
const context = { now, options, result: null };
vm.runInNewContext(
`${source};result=Dc(now,wc(options),Oc(options.byweekday));`,
context,
);
return context.result;
}

test("automation schedule patch honors multiple BYHOUR values", () => {
const patched = applyPatchTwice(applyAutomationScheduleMultiTimePatch, automationScheduleBundleFixture());
const options = {
byhour: [11, 14, 17, 20],
byminute: [0],
byweekday: ["MO", "TU", "WE", "TH", "FR"],
dtstart: new Date(2026, 4, 22, 16, 27, 0, 0),
};

assert.match(patched, /function codexLinuxNormalizeRruleNumbers/);
assert.equal(
evaluateAutomationSchedule(patched, new Date(2026, 4, 22, 16, 27, 0, 0).getTime(), options),
new Date(2026, 4, 22, 17, 0, 0, 0).getTime(),
);
assert.equal(
evaluateAutomationSchedule(patched, new Date(2026, 4, 22, 20, 1, 0, 0).getTime(), options),
new Date(2026, 4, 25, 11, 0, 0, 0).getTime(),
);
});

test("automation schedule asset patch updates workspace-root bundle", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codex-automation-schedule-"));
try {
const buildDir = path.join(tempRoot, ".vite", "build");
fs.mkdirSync(buildDir, { recursive: true });
const bundlePath = path.join(buildDir, "workspace-root-drop-handler-test.js");
fs.writeFileSync(bundlePath, automationScheduleBundleFixture(), "utf8");

assert.deepEqual(patchAutomationScheduleAssets(tempRoot), { matched: 1, changed: 1 });
const patched = fs.readFileSync(bundlePath, "utf8");
assert.match(patched, /function codexLinuxRruleTimes/);
assert.deepEqual(patchAutomationScheduleAssets(tempRoot), { matched: 1, changed: 0 });
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});

test("asset patch helpers match every file when passed a global regex", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codex-asset-global-"));
try {
Expand Down Expand Up @@ -445,6 +507,7 @@ test("default core patch descriptors are grouped and unique", () => {
"linux-launch-actions",
"linux-hotkey-window-prewarm",
"linux-git-origins-source-fallback",
"automation-schedule-multi-time-rrule",
"linux-app-sunset-gate",
"linux-app-server-feature-enablement",
"opaque-window-default-general-settings",
Expand Down
123 changes: 123 additions & 0 deletions scripts/patches/automation-schedule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use strict";

const fs = require("node:fs");
const path = require("node:path");

const {
findMatchingBrace,
readDirectoryNames,
} = require("./shared.js");

function findWorkspaceRootDropHandlerBundles(extractedDir) {
const buildDir = path.join(extractedDir, ".vite", "build");
return readDirectoryNames(buildDir)
.filter((name) => /^workspace-root-drop-handler(?:-[^.]+)?\.js$/.test(name))
.sort()
.map((name) => path.join(buildDir, name));
}

function findAutomationScheduleHelperBlock(source) {
const marker =
"return t!=null&&n!=null?{hour:t,minute:n}:e.dtstart?{hour:e.dtstart.getHours(),minute:e.dtstart.getMinutes()}:null";
const markerIndex = source.indexOf(marker);
if (markerIndex === -1) {
return null;
}

const start = source.lastIndexOf("var ", markerIndex);
if (start === -1) {
return null;
}

let cursor = start;
for (let index = 0; index < 7; index += 1) {
const functionIndex = source.indexOf("function ", cursor);
if (functionIndex === -1) {
return null;
}
const openBrace = source.indexOf("{", functionIndex);
if (openBrace === -1) {
return null;
}
const closeBrace = findMatchingBrace(source, openBrace);
if (closeBrace === -1) {
return null;
}
cursor = closeBrace + 1;
}

const text = source.slice(start, cursor);
const dayMapVar = text.match(/^var ([A-Za-z_$][\w$]*)=\{MO:1,TU:2,WE:3,TH:4,FR:5,SA:6,SU:0\};/)?.[1];
const functions = [...text.matchAll(/function ([A-Za-z_$][\w$]*)\(/g)].map((match) => match[1]);
if (dayMapVar == null || functions.length < 7) {
return null;
}

return {
start,
end: cursor,
dayMapVar,
timeOfDayFn: functions[0],
singleNumberFn: functions[1],
dailyFn: functions[2],
weeklyFn: functions[3],
weekdaysFn: functions[4],
weekdayObjectFn: functions[5],
normalizeWeekdayFn: functions[6],
};
}

function automationScheduleReplacement(block) {
return [
`var ${block.dayMapVar}={MO:1,TU:2,WE:3,TH:4,FR:5,SA:6,SU:0};`,
`function ${block.timeOfDayFn}(e){let t=codexLinuxNormalizeRruleNumbers(e.byhour,0,23),n=codexLinuxNormalizeRruleNumbers(e.byminute,0,59);return e.dtstart&&(t.length!==0||(t=[e.dtstart.getHours()]),n.length!==0||(n=[e.dtstart.getMinutes()])),t.length!==0&&n.length!==0?{hours:t,minutes:n}:null}`,
`function ${block.singleNumberFn}(e){let t=codexLinuxNormalizeRruleNumbers(e,-9007199254740991,9007199254740991);return t.length>0?t[0]:null}`,
"function codexLinuxNormalizeRruleNumbers(e,t,n){let r=Array.isArray(e)?e:[e];return Array.from(new Set(r.filter(e=>typeof e==`number`&&Number.isInteger(e)&&e>=t&&e<=n))).sort((e,t)=>e-t)}",
"function codexLinuxRruleTimes(e){let t=[];for(let n of e.hours)for(let r of e.minutes)t.push(n*60+r);return Array.from(new Set(t)).sort((e,t)=>e-t)}",
`function ${block.dailyFn}(e,t){return ${block.weeklyFn}(e,t,[])}`,
`function ${block.weeklyFn}(e,t,n){let r=new Date(e),i=r.getDay(),a=n.length>0?n:[0,1,2,3,4,5,6],o=codexLinuxRruleTimes(t);if(o.length===0)return e;for(let t=0;t<=7;t+=1){let n=(i+t)%7;if(!a.includes(n))continue;for(let n of o){let i=Math.floor(n/60),a=n%60,s=new Date(r.getFullYear(),r.getMonth(),r.getDate()+t,i,a,0,0);if(s.getTime()>e)return s.getTime()}}return e}`,
`function ${block.weekdaysFn}(e){return e?(Array.isArray(e)?e:[e]).map(e=>{if(typeof e==\`number\`)return ${block.normalizeWeekdayFn}(e);if(${block.weekdayObjectFn}(e))return ${block.normalizeWeekdayFn}(e.weekday);let t=String(e);return t in ${block.dayMapVar}?${block.dayMapVar}[t]:null}).filter(e=>e!=null):[]}`,
`function ${block.weekdayObjectFn}(e){return typeof e!=\`object\`||!e||!(\`weekday\`in e)?!1:typeof e.weekday==\`number\`}`,
`function ${block.normalizeWeekdayFn}(e){return!Number.isInteger(e)||e<0||e>6?null:(e+1)%7}`,
].join("");
}

function applyAutomationScheduleMultiTimePatch(source) {
if (source.includes("function codexLinuxNormalizeRruleNumbers(")) {
return source;
}

const block = findAutomationScheduleHelperBlock(source);
if (block == null) {
console.warn("WARN: Could not find automation schedule helper block — skipping RRULE multi-time patch");
return source;
}

return source.slice(0, block.start) + automationScheduleReplacement(block) + source.slice(block.end);
}

function patchAutomationScheduleAssets(extractedDir) {
const candidates = findWorkspaceRootDropHandlerBundles(extractedDir);
if (candidates.length === 0) {
const reason = `Could not find workspace-root-drop-handler bundle in ${path.join(extractedDir, ".vite", "build")}`;
console.warn(`WARN: ${reason} — skipping RRULE multi-time patch`);
return { matched: 0, changed: 0, reason };
}

let changed = 0;
for (const candidate of candidates) {
const source = fs.readFileSync(candidate, "utf8");
const patched = applyAutomationScheduleMultiTimePatch(source);
if (patched !== source) {
fs.writeFileSync(candidate, patched, "utf8");
changed += 1;
}
}

return { matched: candidates.length, changed };
}

module.exports = {
applyAutomationScheduleMultiTimePatch,
patchAutomationScheduleAssets,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use strict";

const {
patchAutomationScheduleAssets,
} = require("../../../../automation-schedule.js");

module.exports = {
id: "automation-schedule-multi-time-rrule",
phase: "extracted-app",
order: 240,
ciPolicy: "optional",
apply: patchAutomationScheduleAssets,
status: (result, warnings) => ({
status: result?.changed
? "applied"
: warnings.length > 0
? "skipped-optional"
: result?.matched
? "already-applied"
: "skipped-optional",
reason: result?.reason ?? warnings[0] ?? null,
}),
};
Loading