first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
# executor
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `AppleScriptRunner` injectable seam that `MacOSContactsBridge` depends on, plus a default implementation backed by `node:child_process` against the platform `osascript` binary. The seam exists so tests run on any platform without macOS — inject a fake runner that returns fixture strings and the entire bridge can be exercised on Linux CI, Bun on WSL, browser-style runtimes that polyfill child_process, etc.
## Features
- `AppleScriptRunner` interface — single method `run(script, signal?)` returning the script's stdout as a string.
- `createDefaultAppleScriptRunner({ timeoutMs, maxBufferBytes?, binary? })` — produces a runner that calls `execFile` (argv-based, no shell interpolation) on the configured binary (default `"osascript"`), applies a per-invocation timeout, optionally caps stdout, and threads `AbortSignal` through so callers can cancel an in-flight invocation.
- `DefaultAppleScriptRunnerOptions` — public options bag for the default factory.
- No mocking framework required by consumers — the seam is the interface; tests provide fakes by `implements`-ing it.
## Usage: Basic
```ts
import { createDefaultAppleScriptRunner } from "@suaveplan/macos-contacts";
const runner = createDefaultAppleScriptRunner({ timeoutMs: 30_000 });
const stdout = await runner.run('return "hello"');
```
## Usage: Advanced
```ts
import type { AppleScriptRunner } from "@suaveplan/macos-contacts";
import { MacOSContactsBridge } from "@suaveplan/macos-contacts";
// Test-only fake — no macOS / no shell required.
const fakeRunner: AppleScriptRunner = {
async run(script) {
if (script.includes("every person")) {
return "id1|||A|||B||| ||| ||| |||false\nPHONES:\nEMAILS:\nADDRESSES:";
}
throw new Error("Unexpected script");
},
};
const bridge = new MacOSContactsBridge({ timeoutMs: 1000 }, { runner: fakeRunner });
const contacts = await bridge.getAllContacts();
// In production: pass an AbortSignal to cancel a slow invocation.
const controller = new AbortController();
const real = createDefaultAppleScriptRunner({ timeoutMs: 60_000 });
const promise = real.run(largeScript, controller.signal);
setTimeout(() => controller.abort(), 5_000);
```
## API Reference
### `AppleScriptRunner`
```ts
interface AppleScriptRunner {
run(script: string, signal?: AbortSignal): Promise<string>;
}
```
Implementations SHALL resolve with stdout on success and reject with an `Error` on failure (non-zero exit, timeout, abort, missing binary). The bridge maps rejections through its typed error-mapping rule; runner implementations SHOULD NOT wrap their rejection in `MacOSContactsError` themselves — leave that to the bridge.
### `DefaultAppleScriptRunnerOptions`
- `timeoutMs: number` — required. Applied to every invocation.
- `maxBufferBytes?: number` — optional. Defaults to 10 MiB.
- `binary?: string` — optional. Defaults to `"osascript"`. Override only for test harnesses (e.g. `"/bin/false"` to assert the failure path).
### `createDefaultAppleScriptRunner(options): AppleScriptRunner`
Returns a runner that wraps `node:child_process` via the argv-based file-execution path. The argument vector is fixed at `[binary, "-e", script]` so the AppleScript payload is never subject to shell interpolation. Honours the supplied `AbortSignal`.
## Implementation Notes
The runner uses the argv-based file-execution path rather than the shell-string path, so the AppleScript payload is never passed through `/bin/sh` and is therefore not subject to injection from quotes or backslashes inside the script body. This means template builders only need to escape `"` for AppleScript itself, not for any intermediate shell layer.
`AbortSignal` is forwarded directly to the child via the options bag; an already-aborted signal causes the child to be killed immediately and the returned promise to reject. The bridge does not currently surface this through its public API, but downstream callers that compose the runner directly can rely on cancellation working.
The `binary` override exists for tests: pointing at `"/bin/false"` exercises the rejection path without requiring `osascript`, and pointing at `"/bin/echo"` exercises the resolution path on any POSIX runner. Production callers should leave it at the default.