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/copy-to-clipboard.ts
1import { spawn } from 'node:child_process';2import fs from 'node:fs';3import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';4import os from 'node:os';5import path from 'node:path';6import process from 'node:process';7import { fileURLToPath } from 'node:url';8import decodeWebp, { init as initWebpDecode } from '@jsquash/webp/decode.js';9import { Jimp, JimpMime } from 'jimp';1011const SUPPORTED_IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);1213function printUsage(exitCode = 0): never {14console.log(`Copy image or HTML to system clipboard1516Supports:17- Image files (jpg, png, gif, webp) - copies as image data18- HTML content - copies as rich text for paste1920Usage:21# Copy image to clipboard22npx -y bun copy-to-clipboard.ts image /path/to/image.jpg2324# Copy HTML to clipboard25npx -y bun copy-to-clipboard.ts html "<p>Hello</p>"2627# Copy HTML from file28npx -y bun copy-to-clipboard.ts html --file /path/to/content.html29`);30process.exit(exitCode);31}3233function resolvePath(filePath: string): string {34return path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);35}3637function inferImageMimeType(imagePath: string): string {38const ext = path.extname(imagePath).toLowerCase();39switch (ext) {40case '.jpg':41case '.jpeg':42return 'image/jpeg';43case '.png':44return 'image/png';45case '.gif':46return 'image/gif';47case '.webp':48return 'image/webp';49default:50return 'application/octet-stream';51}52}5354type RunResult = { stdout: string; stderr: string; exitCode: number };5556async function runCommand(57command: string,58args: string[],59options?: { input?: string | Buffer; allowNonZeroExit?: boolean },60): Promise<RunResult> {61return await new Promise<RunResult>((resolve, reject) => {62const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });63const stdoutChunks: Buffer[] = [];64const stderrChunks: Buffer[] = [];6566child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));67child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));68child.on('error', reject);69child.on('close', (code) => {70resolve({71stdout: Buffer.concat(stdoutChunks).toString('utf8'),72stderr: Buffer.concat(stderrChunks).toString('utf8'),73exitCode: code ?? 0,74});75});7677if (options?.input != null) child.stdin.write(options.input);78child.stdin.end();79}).then((result) => {80if (!options?.allowNonZeroExit && result.exitCode !== 0) {81const details = result.stderr.trim() || result.stdout.trim();82throw new Error(`Command failed (${command}): exit ${result.exitCode}${details ? `\n${details}` : ''}`);83}84return result;85});86}8788async function commandExists(command: string): Promise<boolean> {89if (process.platform === 'win32') {90const result = await runCommand('where', [command], { allowNonZeroExit: true });91return result.exitCode === 0 && result.stdout.trim().length > 0;92}93const result = await runCommand('which', [command], { allowNonZeroExit: true });94return result.exitCode === 0 && result.stdout.trim().length > 0;95}9697async function runCommandWithFileStdin(command: string, args: string[], filePath: string): Promise<void> {98await new Promise<void>((resolve, reject) => {99const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });100const stderrChunks: Buffer[] = [];101const stdoutChunks: Buffer[] = [];102103child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));104child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));105child.on('error', reject);106child.on('close', (code) => {107const exitCode = code ?? 0;108if (exitCode !== 0) {109const details = Buffer.concat(stderrChunks).toString('utf8').trim() || Buffer.concat(stdoutChunks).toString('utf8').trim();110reject(111new Error(`Command failed (${command}): exit ${exitCode}${details ? `\n${details}` : ''}`),112);113return;114}115resolve();116});117118fs.createReadStream(filePath).on('error', reject).pipe(child.stdin);119});120}121122async function withTempDir<T>(prefix: string, fn: (tempDir: string) => Promise<T>): Promise<T> {123const tempDir = await mkdtemp(path.join(os.tmpdir(), prefix));124try {125return await fn(tempDir);126} finally {127await rm(tempDir, { recursive: true, force: true });128}129}130131function getMacSwiftClipboardSource(): string {132return `import AppKit133import Foundation134135func die(_ message: String, _ code: Int32 = 1) -> Never {136FileHandle.standardError.write(message.data(using: .utf8)!)137exit(code)138}139140if CommandLine.arguments.count < 3 {141die("Usage: clipboard.swift <image|html> <path>\\n")142}143144let mode = CommandLine.arguments[1]145let inputPath = CommandLine.arguments[2]146let pasteboard = NSPasteboard.general147pasteboard.clearContents()148149switch mode {150case "image":151guard let image = NSImage(contentsOfFile: inputPath) else {152die("Failed to load image: \\(inputPath)\\n")153}154if !pasteboard.writeObjects([image]) {155die("Failed to write image to clipboard\\n")156}157158case "html":159let url = URL(fileURLWithPath: inputPath)160let data: Data161do {162data = try Data(contentsOf: url)163} catch {164die("Failed to read HTML file: \\(inputPath)\\n")165}166167_ = pasteboard.setData(data, forType: .html)168169let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [170.documentType: NSAttributedString.DocumentType.html,171.characterEncoding: String.Encoding.utf8.rawValue172]173174if let attr = try? NSAttributedString(data: data, options: options, documentAttributes: nil) {175pasteboard.setString(attr.string, forType: .string)176if let rtf = try? attr.data(177from: NSRange(location: 0, length: attr.length),178documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf]179) {180_ = pasteboard.setData(rtf, forType: .rtf)181}182} else if let html = String(data: data, encoding: .utf8) {183pasteboard.setString(html, forType: .string)184}185186default:187die("Unknown mode: \\(mode)\\n")188}189`;190}191192function escapeAppleScriptString(value: string): string {193return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');194}195196function getAppleScriptImageType(imagePath: string): string {197const ext = path.extname(imagePath).toLowerCase();198switch (ext) {199case '.png':200return '«class PNGf»';201case '.jpg':202case '.jpeg':203return 'JPEG picture';204case '.gif':205return 'GIF picture';206default:207throw new Error(`macOS clipboard image copy supports PNG, JPEG, and GIF via AppleScript; convert ${ext || 'this file'} to PNG first.`);208}209}210211let webpDecoderReady: Promise<void> | undefined;212213async function ensureWebpDecoder(): Promise<void> {214if (!webpDecoderReady) {215webpDecoderReady = (async () => {216const __filename = fileURLToPath(import.meta.url);217const __dirname = path.dirname(__filename);218const wasmPath = path.resolve(__dirname, 'node_modules/@jsquash/webp/codec/dec/webp_dec.wasm');219const wasmModule = await WebAssembly.compile(await readFile(wasmPath));220await initWebpDecode(wasmModule, {});221})();222}223224await webpDecoderReady;225}226227async function convertWebpMacToPng(webpPath: string, tempDir: string): Promise<string> {228await ensureWebpDecoder();229const decoded = await decodeWebp(await readFile(webpPath));230const image = new Jimp({231data: Buffer.from(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength),232width: decoded.width,233height: decoded.height,234});235const pngPath = path.join(tempDir, `${path.basename(webpPath, path.extname(webpPath))}.png`);236await writeFile(pngPath, await image.getBuffer(JimpMime.png));237return pngPath;238}239240async function copyImageMacWithOsascript(imagePath: string): Promise<void> {241const imageType = getAppleScriptImageType(imagePath);242const escapedPath = escapeAppleScriptString(imagePath);243await runCommand('osascript', [244'-e',245`set the clipboard to (read (POSIX file "${escapedPath}") as ${imageType})`,246]);247}248249async function copyImageMac(imagePath: string): Promise<void> {250if (path.extname(imagePath).toLowerCase() === '.webp') {251await withTempDir('copy-to-clipboard-', async (tempDir) => {252const pngPath = await convertWebpMacToPng(imagePath, tempDir);253await copyImageMacWithOsascript(pngPath);254});255return;256}257258await copyImageMacWithOsascript(imagePath);259}260261async function copyHtmlMac(htmlFilePath: string): Promise<void> {262await withTempDir('copy-to-clipboard-', async (tempDir) => {263const swiftPath = path.join(tempDir, 'clipboard.swift');264await writeFile(swiftPath, getMacSwiftClipboardSource(), 'utf8');265await runCommand('swift', [swiftPath, 'html', htmlFilePath]);266});267}268269async function copyImageLinux(imagePath: string): Promise<void> {270const mime = inferImageMimeType(imagePath);271if (await commandExists('wl-copy')) {272await runCommandWithFileStdin('wl-copy', ['--type', mime], imagePath);273return;274}275if (await commandExists('xclip')) {276await runCommand('xclip', ['-selection', 'clipboard', '-t', mime, '-i', imagePath]);277return;278}279throw new Error('No clipboard tool found. Install `wl-clipboard` (wl-copy) or `xclip`.');280}281282async function copyHtmlLinux(htmlFilePath: string): Promise<void> {283if (await commandExists('wl-copy')) {284await runCommandWithFileStdin('wl-copy', ['--type', 'text/html'], htmlFilePath);285return;286}287if (await commandExists('xclip')) {288await runCommand('xclip', ['-selection', 'clipboard', '-t', 'text/html', '-i', htmlFilePath]);289return;290}291throw new Error('No clipboard tool found. Install `wl-clipboard` (wl-copy) or `xclip`.');292}293294async function copyImageWindows(imagePath: string): Promise<void> {295const escaped = imagePath.replace(/'/g, "''");296const ps = [297'Add-Type -AssemblyName System.Windows.Forms',298'Add-Type -AssemblyName System.Drawing',299`$img = [System.Drawing.Image]::FromFile('${escaped}')`,300'[System.Windows.Forms.Clipboard]::SetImage($img)',301'$img.Dispose()',302].join('; ');303await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps]);304}305306async function copyHtmlWindows(htmlFilePath: string): Promise<void> {307const escaped = htmlFilePath.replace(/'/g, "''");308const ps = [309'Add-Type -AssemblyName System.Windows.Forms',310`$html = Get-Content -Raw -LiteralPath '${escaped}'`,311'[System.Windows.Forms.Clipboard]::SetText($html, [System.Windows.Forms.TextDataFormat]::Html)',312].join('; ');313await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps]);314}315316async function copyImageToClipboard(imagePathInput: string): Promise<void> {317const imagePath = resolvePath(imagePathInput);318const ext = path.extname(imagePath).toLowerCase();319if (!SUPPORTED_IMAGE_EXTS.has(ext)) {320throw new Error(321`Unsupported image type: ${ext || '(none)'} (supported: ${Array.from(SUPPORTED_IMAGE_EXTS).join(', ')})`,322);323}324if (!fs.existsSync(imagePath)) throw new Error(`File not found: ${imagePath}`);325326switch (process.platform) {327case 'darwin':328await copyImageMac(imagePath);329return;330case 'linux':331await copyImageLinux(imagePath);332return;333case 'win32':334await copyImageWindows(imagePath);335return;336default:337throw new Error(`Unsupported platform: ${process.platform}`);338}339}340341async function copyHtmlFileToClipboard(htmlFilePathInput: string): Promise<void> {342const htmlFilePath = resolvePath(htmlFilePathInput);343if (!fs.existsSync(htmlFilePath)) throw new Error(`File not found: ${htmlFilePath}`);344345switch (process.platform) {346case 'darwin':347await copyHtmlMac(htmlFilePath);348return;349case 'linux':350await copyHtmlLinux(htmlFilePath);351return;352case 'win32':353await copyHtmlWindows(htmlFilePath);354return;355default:356throw new Error(`Unsupported platform: ${process.platform}`);357}358}359360async function readStdinText(): Promise<string | null> {361if (process.stdin.isTTY) return null;362const chunks: Buffer[] = [];363for await (const chunk of process.stdin) {364chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));365}366const text = Buffer.concat(chunks).toString('utf8');367return text.length > 0 ? text : null;368}369370async function copyHtmlToClipboard(args: string[]): Promise<void> {371let htmlFile: string | undefined;372const positional: string[] = [];373374for (let i = 0; i < args.length; i += 1) {375const arg = args[i] ?? '';376if (arg === '--help' || arg === '-h') printUsage(0);377if (arg === '--file') {378htmlFile = args[i + 1];379i += 1;380continue;381}382if (arg.startsWith('--file=')) {383htmlFile = arg.slice('--file='.length);384continue;385}386if (arg === '--') {387positional.push(...args.slice(i + 1));388break;389}390if (arg.startsWith('-')) {391throw new Error(`Unknown option: ${arg}`);392}393positional.push(arg);394}395396if (htmlFile && positional.length > 0) {397throw new Error('Do not pass HTML text when using --file.');398}399400if (htmlFile) {401await copyHtmlFileToClipboard(htmlFile);402return;403}404405const htmlFromArgs = positional.join(' ').trim();406const htmlFromStdin = (await readStdinText())?.trim() ?? '';407const html = htmlFromArgs || htmlFromStdin;408if (!html) throw new Error('Missing HTML input. Provide a string or use --file.');409410await withTempDir('copy-to-clipboard-', async (tempDir) => {411const htmlPath = path.join(tempDir, 'input.html');412await writeFile(htmlPath, html, 'utf8');413await copyHtmlFileToClipboard(htmlPath);414});415}416417async function main(): Promise<void> {418const argv = process.argv.slice(2);419if (argv.length === 0) printUsage(1);420421const command = argv[0];422if (command === '--help' || command === '-h') printUsage(0);423424if (command === 'image') {425const imagePath = argv[1];426if (!imagePath) throw new Error('Missing image path.');427await copyImageToClipboard(imagePath);428return;429}430431if (command === 'html') {432await copyHtmlToClipboard(argv.slice(1));433return;434}435436throw new Error(`Unknown command: ${command}`);437}438439await main().catch((err) => {440const message = err instanceof Error ? err.message : String(err);441console.error(`Error: ${message}`);442process.exit(1);443});444