| 1234567891011121314151617181920212223242526272829303132333435363738 |
- #!/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=`<?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
- <plist version="1.0"><dict>
- <key>Label</key><string>${label}</string>
- <key>ProgramArguments</key><array><string>${escape(node)}</string><string>--env-file=${escape(env)}</string><string>${escape(script)}</string><string>--scheduled</string></array>
- <key>WorkingDirectory</key><string>${escape(root)}</string>
- <key>StartCalendarInterval</key><dict><key>Hour</key><integer>23</integer><key>Minute</key><integer>0</integer></dict>
- <key>RunAtLoad</key><true/>
- <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
- <key>ThrottleInterval</key><integer>300</integer>
- <key>ProcessType</key><string>Background</string>
- <key>StandardOutPath</key><string>${escape(join(state,'launchagent.log'))}</string>
- <key>StandardErrorPath</key><string>${escape(join(state,'launchagent-error.log'))}</string>
- <key>EnvironmentVariables</key><dict><key>TZ</key><string>Asia/Shanghai</string><key>NO_PROXY</key><string>server.xiaoshu.pro,localhost,127.0.0.1,::1</string></dict>
- <key>Umask</key><integer>63</integer>
- </dict></plist>`;
- 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}));
|