#!/usr/bin/env node import {mkdir,copyFile,chmod,readFile,writeFile,stat,realpath} from 'node:fs/promises'; import {homedir} from 'node:os'; import {join} from 'node:path'; import {spawnSync} from 'node:child_process'; const label='cn.xiaoshu.legacy-direct-sync-nightly',home=homedir(); const root=join(home,'Library/Application Support/XiaoshuSync'),env=join(home,'.config/xiaoshu/legacy-sync-nightly.env'),state=join(home,'.local/state/xiaoshu/legacy-sync'); if(process.platform!=='darwin')throw Error('This installer requires macOS'); if(new Intl.DateTimeFormat().resolvedOptions().timeZone!=='Asia/Shanghai')throw Error('macOS timezone must be Asia/Shanghai for the 23:00 calendar schedule'); if((await stat(env)).mode%512!==384)throw Error('Credential environment must have mode 0600'); await mkdir(join(root,'scripts/lib'),{recursive:true,mode:0o700});await mkdir(state,{recursive:true,mode:0o700}); for(const file of ['run-nightly-direct-sync.mjs','lib/legacy-direct-projection.mjs','lib/legacy-direct-specs.mjs']){const dest=join(root,'scripts',file);await copyFile(new URL(file,import.meta.url),dest);await chmod(dest,0o600);} const node=await realpath(process.execPath),script=join(root,'scripts/run-nightly-direct-sync.mjs'); const escape=s=>s.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>'); const xml=` Label${label} ProgramArguments${escape(node)}--env-file=${escape(env)}${escape(script)}--scheduled WorkingDirectory${escape(root)} StartCalendarIntervalHour23Minute0 RunAtLoad KeepAliveSuccessfulExit ThrottleInterval300 ProcessTypeBackground StandardOutPath${escape(join(state,'launchagent.log'))} StandardErrorPath${escape(join(state,'launchagent-error.log'))} EnvironmentVariablesTZAsia/ShanghaiNO_PROXYserver.xiaoshu.pro,localhost,127.0.0.1,::1 Umask63 `; const agents=join(home,'Library/LaunchAgents');await mkdir(agents,{recursive:true});const path=join(agents,label+'.plist');await writeFile(path,xml,{mode:0o600}); const lint=spawnSync('/usr/bin/plutil',['-lint',path],{encoding:'utf8'});if(lint.status!==0)throw Error('LaunchAgent plist validation failed'); const domain='gui/'+process.getuid(),target=domain+'/'+label; spawnSync('/bin/launchctl',['bootout',target],{encoding:'utf8'}); const installed=spawnSync('/bin/launchctl',['bootstrap',domain,path],{encoding:'utf8'});if(installed.status!==0)throw Error('LaunchAgent bootstrap failed: '+installed.stderr); const check=spawnSync('/bin/launchctl',['print',target],{encoding:'utf8'});if(check.status!==0||!check.stdout.includes(label)||!check.stdout.includes(script))throw Error('LaunchAgent readback failed'); await writeFile(join(state,'launchagent-install.json'),JSON.stringify({label,path,installedAt:new Date().toISOString(),timeZone:'Asia/Shanghai',hour:23,minute:0,node,script,credentialMode:'0600',readbackVerified:true},null,2),{mode:0o600}); console.log(JSON.stringify({label,installed:true,timeZone:'Asia/Shanghai',schedule:'23:00',runtime:root,systemReadbackVerified:true}));