Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Post articles and image-text content to WeChat Official Account via API or Chrome CDP, with markdown-to-WeChat HTML conversion.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/check-permissions.ts
1import { spawnSync } from 'node:child_process';2import fs from 'node:fs';3import { mkdtemp, rm, writeFile } from 'node:fs/promises';4import os from 'node:os';5import path from 'node:path';6import process from 'node:process';7import { findChromeExecutable, getDefaultProfileDir } from './cdp.ts';89interface CheckResult {10name: string;11ok: boolean;12detail: string;13}1415const results: CheckResult[] = [];1617function log(label: string, ok: boolean, detail: string): void {18results.push({ name: label, ok, detail });19const icon = ok ? '✅' : '❌';20console.log(`${icon} ${label}: ${detail}`);21}2223function warn(label: string, detail: string): void {24results.push({ name: label, ok: true, detail });25console.log(`⚠️ ${label}: ${detail}`);26}2728async function checkChrome(): Promise<void> {29const chromePath = findChromeExecutable();30if (chromePath) {31log('Chrome', true, chromePath);32} else {33log('Chrome', false, 'Not found. Set WECHAT_BROWSER_CHROME_PATH env var or install Chrome.');34}35}3637async function checkProfileIsolation(): Promise<void> {38const profileDir = getDefaultProfileDir();39const userChromeDir = process.platform === 'darwin'40? path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome')41: process.platform === 'win32'42? path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data')43: path.join(os.homedir(), '.config', 'google-chrome');4445const isIsolated = !profileDir.startsWith(userChromeDir);46log('Profile isolation', isIsolated, `Skill profile: ${profileDir}`);4748if (isIsolated) {49const exists = fs.existsSync(profileDir);50if (exists) {51log('Profile dir', true, 'Exists and accessible');52} else {53try {54fs.mkdirSync(profileDir, { recursive: true });55log('Profile dir', true, 'Created successfully');56} catch (e) {57log('Profile dir', false, `Cannot create: ${e instanceof Error ? e.message : String(e)}`);58}59}60}61}6263async function checkAccessibility(): Promise<void> {64if (process.platform !== 'darwin') {65log('Accessibility', true, `Skipped (not macOS, platform: ${process.platform})`);66return;67}6869const result = spawnSync('osascript', ['-e', `70tell application "System Events"71set frontApp to name of first application process whose frontmost is true72return frontApp73end tell74`], { stdio: 'pipe', timeout: 10_000 });7576if (result.status === 0) {77const app = result.stdout?.toString().trim();78log('Accessibility (System Events)', true, `Frontmost app: ${app}`);79} else {80const stderr = result.stderr?.toString().trim() || '';81if (stderr.includes('not allowed assistive access') || stderr.includes('1002')) {82log('Accessibility (System Events)', false,83'Denied. Grant access: System Settings → Privacy & Security → Accessibility → enable your terminal app');84} else {85log('Accessibility (System Events)', false, `Failed: ${stderr}`);86}87}88}8990async function checkClipboardCopy(): Promise<void> {91if (process.platform !== 'darwin') {92log('Clipboard copy (image)', true, `Skipped (not macOS)`);93return;94}9596const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'wechat-check-'));97try {98const testPng = path.join(tmpDir, 'test.png');99const swiftSrc = `import AppKit100import Foundation101let size = NSSize(width: 2, height: 2)102let image = NSImage(size: size)103image.lockFocus()104NSColor.red.set()105NSBezierPath.fill(NSRect(origin: .zero, size: size))106image.unlockFocus()107guard let tiff = image.tiffRepresentation,108let rep = NSBitmapImageRep(data: tiff),109let png = rep.representation(using: .png, properties: [:]) else {110FileHandle.standardError.write("Failed to create test PNG\\n".data(using: .utf8)!)111exit(1)112}113try png.write(to: URL(fileURLWithPath: CommandLine.arguments[1]))114`;115const genScript = path.join(tmpDir, 'gen.swift');116await writeFile(genScript, swiftSrc, 'utf8');117const genResult = spawnSync('swift', [genScript, testPng], { stdio: 'pipe', timeout: 30_000 });118if (genResult.status !== 0) {119log('Clipboard copy (image)', false, `Cannot create test image: ${genResult.stderr?.toString().trim()}`);120return;121}122123const clipSrc = `import AppKit124import Foundation125guard let image = NSImage(contentsOfFile: CommandLine.arguments[1]) else {126FileHandle.standardError.write("Failed to load image\\n".data(using: .utf8)!)127exit(1)128}129let pb = NSPasteboard.general130pb.clearContents()131if !pb.writeObjects([image]) {132FileHandle.standardError.write("Failed to write to clipboard\\n".data(using: .utf8)!)133exit(1)134}135`;136const clipScript = path.join(tmpDir, 'clip.swift');137await writeFile(clipScript, clipSrc, 'utf8');138const clipResult = spawnSync('swift', [clipScript, testPng], { stdio: 'pipe', timeout: 30_000 });139if (clipResult.status === 0) {140log('Clipboard copy (image)', true, 'Can copy image to clipboard via Swift/AppKit');141} else {142log('Clipboard copy (image)', false, `Failed: ${clipResult.stderr?.toString().trim()}`);143}144} finally {145await rm(tmpDir, { recursive: true, force: true });146}147}148149async function checkPasteKeystroke(): Promise<void> {150if (process.platform === 'darwin') {151const result = spawnSync('osascript', ['-e', `152tell application "System Events"153set canSend to true154return canSend155end tell156`], { stdio: 'pipe', timeout: 10_000 });157158if (result.status === 0) {159log('Paste keystroke (osascript)', true, 'System Events can send keystrokes');160} else {161const stderr = result.stderr?.toString().trim() || '';162log('Paste keystroke (osascript)', false, `Cannot send keystrokes: ${stderr}`);163}164} else if (process.platform === 'linux') {165const xdotool = spawnSync('which', ['xdotool'], { stdio: 'pipe' });166const ydotool = spawnSync('which', ['ydotool'], { stdio: 'pipe' });167if (xdotool.status === 0) {168log('Paste keystroke', true, 'xdotool available (X11)');169} else if (ydotool.status === 0) {170log('Paste keystroke', true, 'ydotool available (Wayland)');171} else {172log('Paste keystroke', false, 'No tool found. Install xdotool (X11) or ydotool (Wayland).');173}174} else if (process.platform === 'win32') {175log('Paste keystroke', true, 'Windows uses PowerShell SendKeys (built-in)');176}177}178179async function checkBun(): Promise<void> {180const result = spawnSync('npx', ['-y', 'bun', '--version'], { stdio: 'pipe', timeout: 30_000 });181if (result.status === 0) {182log('Bun runtime', true, `v${result.stdout?.toString().trim()}`);183} else {184log('Bun runtime', false, 'Cannot run bun. Install: brew install oven-sh/bun/bun (macOS) or npm install -g bun');185}186}187188async function checkApiCredentials(): Promise<void> {189const cwd = process.cwd();190const projectEnv = path.join(cwd, '.baoyu-skills', '.env');191const userEnv = path.join(os.homedir(), '.baoyu-skills', '.env');192193let found = false;194for (const envPath of [projectEnv, userEnv]) {195if (fs.existsSync(envPath)) {196const content = fs.readFileSync(envPath, 'utf8');197if (content.includes('WECHAT_APP_ID')) {198log('API credentials', true, `Found in ${envPath}`);199found = true;200break;201}202}203}204205if (!found) {206warn('API credentials', 'Not found. Required for API publishing method. Run the skill to set up via guided flow.');207}208}209210async function checkRunningChromeConflict(): Promise<void> {211if (process.platform !== 'darwin') return;212213const result = spawnSync('pgrep', ['-f', 'Google Chrome'], { stdio: 'pipe' });214const pids = result.stdout?.toString().trim().split('\n').filter(Boolean) || [];215216if (pids.length > 0) {217warn('Running Chrome instances', `${pids.length} Chrome process(es) detected. The skill uses --user-data-dir for isolation, so this is safe.`);218} else {219log('Running Chrome instances', true, 'No existing Chrome processes');220}221}222223async function main(): Promise<void> {224console.log('=== baoyu-post-to-wechat: Permission & Environment Check ===\n');225226await checkChrome();227await checkProfileIsolation();228await checkBun();229await checkAccessibility();230await checkClipboardCopy();231await checkPasteKeystroke();232await checkApiCredentials();233await checkRunningChromeConflict();234235console.log('\n--- Summary ---');236const failed = results.filter((r) => !r.ok);237if (failed.length === 0) {238console.log('All checks passed. Ready to post to WeChat.');239} else {240console.log(`${failed.length} issue(s) found:`);241for (const f of failed) {242console.log(` ❌ ${f.name}: ${f.detail}`);243}244process.exit(1);245}246}247248await main().catch((err) => {249console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);250process.exit(1);251});252