SPB Git

Repository Public Research preview

osa.mjs 4.3 KB 78 lines javascript

1import { execFile } from "node:child_process";2import { readFileSync } from "node:fs";34/**5 * AppleScript bridge. Scripts are fixed strings; every variable part travels as an argv item, so no6 * value is ever interpolated into AppleScript source (no quoting bugs, no injection).7 */8export class SafariError extends Error {9  constructor(code, message, extra) { super(message); this.code = code; Object.assign(this, extra ?? {}); }10}1112export function osascript(script, args = [], { timeoutMs = 30000, language } = {}) {13  return new Promise((resolve, reject) => {14    const child = execFile("/usr/bin/osascript", [...(language ? ["-l", language] : []), "-", ...args.map(String)], { timeout: timeoutMs, maxBuffer: 256 * 1024 * 1024 }, (err, stdout, stderr) => {15      if (!err) return resolve(stdout.replace(/\n$/, ""));16      const msg = String(stderr || err.message).trim();17      if (err.killed) return reject(new SafariError("timeout", `Safari did not answer within ${Math.round(timeoutMs / 1000)} s (a page dialog or a busy page can block scripts).`));18      if (/-1743|Not authorized to send Apple events|pas autoris/i.test(msg)) return reject(new SafariError("automation_denied", "macOS blocked Apple events to Safari. Allow this app under System Settings → Privacy & Security → Automation → Safari."));19      if (/Allow JavaScript from Apple Events|JavaScript à partir d.Apple Events|AllowJavaScriptFromAppleEvents/i.test(msg)) return reject(new SafariError("js_from_apple_events_disabled", "Safari refuses JavaScript from Apple Events. Enable Safari → Settings → Developer → “Allow JavaScript from Apple Events” (show the Develop menu first under Settings → Advanced)."));20      if (/-1728|-1719|Can.t get (tab|window)|Impossible d.obtenir/i.test(msg)) return reject(new SafariError("no_such_tab", "That Safari window or tab no longer exists."));21      reject(new SafariError("applescript_error", msg.slice(0, 500)));22    });23    child.stdin.end(script);24  });25}2627const HELPER = readFileSync(new URL("./page.js", import.meta.url), "utf8");2829const RUN_JS = `on run argv30  set w to (item 1 of argv) as integer31  set t to (item 2 of argv) as integer32  set js to item 3 of argv33  tell application "Safari"34    set r to do JavaScript js in tab t of window id w35  end tell36  try37    return r38  on error39    return ""40  end try41end run`;4243// Same call, but also returns the tab's URL as Safari reports it. That URL switches to the44// destination as soon as a navigation starts, before the document itself changes: comparing the45// two is how a pending navigation is detected.46const RUN_JS_WITH_URL = `on run argv47  set w to (item 1 of argv) as integer48  set t to (item 2 of argv) as integer49  set js to item 3 of argv50  tell application "Safari"51    set u to URL of tab t of window id w52    if u is missing value then set u to ""53    set r to do JavaScript js in tab t of window id w54  end tell55  try56    return u & (ASCII character 10) & r57  on error58    return u & (ASCII character 10)59  end try60end run`;6162/** Run a helper API call inside the page and return its JSON result. */63export async function pageCall(win, tab, fn, args = {}, { timeoutMs = 30000, dialogMode, withTabUrl = false } = {}) {64  const js = `${dialogMode ? `window.__smcp_dialog_mode=${JSON.stringify(dialogMode)};` : ""}${HELPER}65(function(){try{var v=window.__smcp.api[${JSON.stringify(fn)}](${JSON.stringify(args)});return JSON.stringify({ok:true,v:v===undefined?null:v});}66catch(e){return JSON.stringify({ok:false,e:String(e&&e.message||e),code:e&&e.code||null,candidates:e&&e.candidates||null});}})()`;67  let raw = await osascript(withTabUrl ? RUN_JS_WITH_URL : RUN_JS, [win, tab, js], { timeoutMs });68  let tabUrl = null;69  if (withTabUrl) { const i = raw.indexOf("\n"); tabUrl = i === -1 ? raw : raw.slice(0, i); raw = i === -1 ? "" : raw.slice(i + 1); }70  if (raw === "" || raw === "missing value") throw new SafariError("no_result", "The page returned nothing (it may be navigating, or it is not an HTML document).");71  let out;72  try { out = JSON.parse(raw); } catch { throw new SafariError("bad_result", "The page returned an unreadable result."); }73  if (!out.ok) throw new SafariError(out.code ?? "page_error", out.e, out.candidates ? { candidates: out.candidates } : undefined);74  return withTabUrl ? { ...out.v, tab_url: tabUrl } : out.v;75}7677export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));78
Files main
Menu

Theme