pairling 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/bin/pairling.mjs +227 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# pairling
|
|
2
|
+
|
|
3
|
+
Pair your iPhone with the AI coding agents running on your Mac.
|
|
4
|
+
|
|
5
|
+
`pairling` is the Mac companion for the [Pairling](https://pairling.dev) iOS
|
|
6
|
+
app: a local runtime that lets your iPhone watch, steer, and spawn Claude Code
|
|
7
|
+
and Codex sessions on your own machine — over your local network or your
|
|
8
|
+
tailnet, authenticated per device.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install -g pairling
|
|
14
|
+
pairling setup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`npm install` only copies files. **Nothing runs at install time** — this
|
|
18
|
+
package ships zero lifecycle scripts and works under `--ignore-scripts`. All
|
|
19
|
+
system changes happen inside the explicit `pairling setup` flow, which prints
|
|
20
|
+
a preview of every change, supports `PAIRLING_DRY_RUN=1`, and appends every
|
|
21
|
+
action to a local audit ledger.
|
|
22
|
+
|
|
23
|
+
Then open Pairling on your iPhone and scan the QR code that `setup` prints.
|
|
24
|
+
|
|
25
|
+
## What setup does (and nothing else)
|
|
26
|
+
|
|
27
|
+
- Stages the runtime under `~/Library/Application Support/Pairling/runtime/`
|
|
28
|
+
(versioned releases, atomic `current` symlink flip, `pairling rollback`).
|
|
29
|
+
- Installs user-domain LaunchAgents (`dev.pairling.companiond`,
|
|
30
|
+
`dev.pairling.connectd`). No root. The optional power guardian is a separate,
|
|
31
|
+
explicit, sudo-gated step.
|
|
32
|
+
- Verifies the payload against the package's integrity manifest and verifies
|
|
33
|
+
the Developer ID signature of the bundled `pairling-connectd` binary before
|
|
34
|
+
staging — fail closed.
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
pairling setup | start | stop | restart | status
|
|
40
|
+
pairling doctor [--json]
|
|
41
|
+
pairling pair [--qr]
|
|
42
|
+
pairling devices | unpair <device_id> | rotate-token <device_id>
|
|
43
|
+
pairling logs | diagnose --redact
|
|
44
|
+
pairling rollback
|
|
45
|
+
pairling uninstall [--yes]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Security posture
|
|
49
|
+
|
|
50
|
+
- **No install scripts, ever.** A release gate fails if any lifecycle script
|
|
51
|
+
appears in a published manifest.
|
|
52
|
+
- **Provenance:** releases are published via npm Trusted Publishing (OIDC) with
|
|
53
|
+
provenance attestations. Verify with `npm audit signatures`.
|
|
54
|
+
- **Readable payload:** the runtime is Python/bash source plus one signed Go
|
|
55
|
+
binary; inspect it with `npm pack pairling --dry-run`.
|
|
56
|
+
- **Integrity chain:** CI records SHA-256 of every payload file in
|
|
57
|
+
`payload-manifest.json`; `pairling setup` re-verifies before staging;
|
|
58
|
+
`pairling doctor` re-verifies the staged runtime and the binary signature.
|
|
59
|
+
- **Local-first:** the daemon serves your devices with per-device scoped bearer
|
|
60
|
+
tokens. npm being down can never affect an installed Mac.
|
|
61
|
+
|
|
62
|
+
## Platform packages
|
|
63
|
+
|
|
64
|
+
The compiled runtime binary ships as platform-filtered optional dependencies:
|
|
65
|
+
`@pairling/runtime-darwin-arm64` and `@pairling/runtime-darwin-x64` — signed,
|
|
66
|
+
notarized, and hash-pinned by this package's integrity manifest.
|
|
67
|
+
|
|
68
|
+
## Links
|
|
69
|
+
|
|
70
|
+
- Product: https://pairling.dev
|
|
71
|
+
- Get started: https://pairling.dev/start
|
|
72
|
+
- Source mirror & publish pipeline: https://github.com/mergimg0/pairling-helper
|
package/bin/pairling.mjs
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pairling — Mac companion CLI for the Pairling iPhone app (https://pairling.dev).
|
|
3
|
+
//
|
|
4
|
+
// This shim is a locator, not an installer. It resolves the package payload and
|
|
5
|
+
// the platform runtime package, exports their paths, and hands control to the
|
|
6
|
+
// bundled bash CLI. All system mutation happens in the explicit, previewable
|
|
7
|
+
// `pairling setup` flow implemented by the payload — never at npm install time
|
|
8
|
+
// (this package ships zero lifecycle scripts) and never inside this shim.
|
|
9
|
+
//
|
|
10
|
+
// Imports are restricted to node: builtins by contract
|
|
11
|
+
// (mac/tests/test_pairling_npm_shim_contract.py enforces this).
|
|
12
|
+
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import process from "node:process";
|
|
20
|
+
|
|
21
|
+
const PRODUCT_URL = "https://pairling.dev";
|
|
22
|
+
const START_URL = "https://pairling.dev/start";
|
|
23
|
+
|
|
24
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
25
|
+
const payloadRoot = join(packageRoot, "payload");
|
|
26
|
+
const payloadCli = join(payloadRoot, "mac", "packaging", "bin", "pairling");
|
|
27
|
+
|
|
28
|
+
function readPackageVersion() {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(join(packageRoot, "package.json"), "utf8");
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
33
|
+
} catch {
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function appSupportRoot() {
|
|
39
|
+
return (
|
|
40
|
+
process.env.PAIRLING_APP_SUPPORT_ROOT ||
|
|
41
|
+
process.env.COMPANION_APP_SUPPORT_ROOT ||
|
|
42
|
+
join(homedir(), "Library", "Application Support", "Pairling")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stagedCliPath() {
|
|
47
|
+
const candidate = join(appSupportRoot(), "runtime", "current", "bin", "pairling");
|
|
48
|
+
return existsSync(candidate) ? candidate : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stagedRuntimeVersion() {
|
|
52
|
+
try {
|
|
53
|
+
const manifestPath = join(appSupportRoot(), "runtime", "current", "manifest.json");
|
|
54
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
55
|
+
return typeof manifest.runtime_version === "string" ? manifest.runtime_version : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runtimePackageDir() {
|
|
62
|
+
// Test/dev hook only. The installer independently re-verifies the binary's
|
|
63
|
+
// Developer ID signature and TeamID before staging, so this override cannot
|
|
64
|
+
// smuggle an unsigned binary into a real install.
|
|
65
|
+
const override = process.env.PAIRLING_RUNTIME_PACKAGE_DIR;
|
|
66
|
+
if (override) {
|
|
67
|
+
return existsSync(override) ? override : null;
|
|
68
|
+
}
|
|
69
|
+
const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
|
|
70
|
+
if (!arch) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const require = createRequire(import.meta.url);
|
|
75
|
+
const manifest = require.resolve(`@pairling/runtime-darwin-${arch}/package.json`);
|
|
76
|
+
return dirname(manifest);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function detectRosetta() {
|
|
83
|
+
if (process.platform !== "darwin" || process.arch !== "x64") {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const probe = spawnSync("/usr/sbin/sysctl", ["-in", "sysctl.proc_translated"], {
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
});
|
|
89
|
+
return probe.status === 0 && probe.stdout.trim() === "1";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function shimEnv() {
|
|
93
|
+
const runtimeDir = runtimePackageDir();
|
|
94
|
+
const connectd = runtimeDir ? join(runtimeDir, "bin", "pairling-connectd") : null;
|
|
95
|
+
const vendoredPython = runtimeDir ? join(runtimeDir, "python", "bin", "python3") : null;
|
|
96
|
+
return {
|
|
97
|
+
packageRoot,
|
|
98
|
+
packageVersion: readPackageVersion(),
|
|
99
|
+
payloadPresent: existsSync(payloadCli),
|
|
100
|
+
payloadRoot,
|
|
101
|
+
runtimePackageDir: runtimeDir,
|
|
102
|
+
connectdPath: connectd && existsSync(connectd) ? connectd : null,
|
|
103
|
+
vendoredPython: vendoredPython && existsSync(vendoredPython) ? vendoredPython : null,
|
|
104
|
+
stagedCli: stagedCliPath(),
|
|
105
|
+
stagedRuntimeVersion: stagedRuntimeVersion(),
|
|
106
|
+
platform: process.platform,
|
|
107
|
+
arch: process.arch,
|
|
108
|
+
rosetta: detectRosetta(),
|
|
109
|
+
node: process.version,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function exitWithChild(result) {
|
|
114
|
+
if (result.error) {
|
|
115
|
+
process.stderr.write(`pairling: failed to launch CLI: ${result.error.message}\n`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (result.signal) {
|
|
119
|
+
// Re-raise so the caller observes the same termination signal.
|
|
120
|
+
process.kill(process.pid, result.signal);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function delegate(cli, args, extraEnv) {
|
|
127
|
+
const env = { ...process.env };
|
|
128
|
+
for (const [key, value] of Object.entries(extraEnv)) {
|
|
129
|
+
// Caller-set values win: PAIRLING_REPO_ROOT et al. stay overridable for
|
|
130
|
+
// development against a repo checkout.
|
|
131
|
+
if (value && env[key] === undefined) {
|
|
132
|
+
env[key] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exitWithChild(spawnSync(cli, args, { stdio: "inherit", env }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function printPlaceholder() {
|
|
139
|
+
const lines = [
|
|
140
|
+
`pairling ${readPackageVersion()} — Pairling for Mac`,
|
|
141
|
+
"",
|
|
142
|
+
"This release reserves the package name while the full Mac runtime ships.",
|
|
143
|
+
"It contains no runtime payload yet and makes no changes to your system.",
|
|
144
|
+
"",
|
|
145
|
+
` Product: ${PRODUCT_URL}`,
|
|
146
|
+
` Get started: ${START_URL}`,
|
|
147
|
+
"",
|
|
148
|
+
"When the runtime ships here, install/update will be:",
|
|
149
|
+
"",
|
|
150
|
+
" npm install -g pairling",
|
|
151
|
+
" pairling setup",
|
|
152
|
+
"",
|
|
153
|
+
];
|
|
154
|
+
process.stdout.write(lines.join("\n"));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function main() {
|
|
158
|
+
const args = process.argv.slice(2);
|
|
159
|
+
|
|
160
|
+
if (args[0] === "--shim-print-env") {
|
|
161
|
+
process.stdout.write(JSON.stringify(shimEnv(), null, 2) + "\n");
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (process.platform !== "darwin") {
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
"pairling: the Pairling Mac runtime only supports macOS.\n" +
|
|
168
|
+
`Learn more: ${PRODUCT_URL}\n`,
|
|
169
|
+
);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
174
|
+
const staged = stagedRuntimeVersion();
|
|
175
|
+
process.stdout.write(
|
|
176
|
+
`pairling ${readPackageVersion()}` + (staged ? ` (staged runtime ${staged})` : "") + "\n",
|
|
177
|
+
);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (detectRosetta()) {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
"pairling: warning: x64 Node is running under Rosetta on Apple Silicon; " +
|
|
184
|
+
"the x64 runtime will be selected. Install arm64 Node for the native runtime.\n",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (existsSync(payloadCli)) {
|
|
189
|
+
const env = shimEnv();
|
|
190
|
+
if (!env.runtimePackageDir || !env.connectdPath) {
|
|
191
|
+
process.stderr.write(
|
|
192
|
+
[
|
|
193
|
+
"pairling: the platform runtime package is missing.",
|
|
194
|
+
"",
|
|
195
|
+
`Expected: @pairling/runtime-darwin-${process.arch === "arm64" ? "arm64" : "x64"}`,
|
|
196
|
+
"",
|
|
197
|
+
"This usually means npm skipped optional dependencies (network hiccup",
|
|
198
|
+
"or --no-optional / --omit=optional). Fix with:",
|
|
199
|
+
"",
|
|
200
|
+
" npm install -g pairling",
|
|
201
|
+
"",
|
|
202
|
+
].join("\n"),
|
|
203
|
+
);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
delegate(payloadCli, args, {
|
|
207
|
+
PAIRLING_REPO_ROOT: join(payloadRoot, "."),
|
|
208
|
+
PAIRLING_CONNECTD_PREBUILT: env.connectdPath,
|
|
209
|
+
PAIRLING_DAEMON_PYTHON: env.vendoredPython,
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Placeholder mode: no payload in this release. If a staged runtime already
|
|
215
|
+
// exists on this Mac (repo-local install), delegate so the command keeps
|
|
216
|
+
// working; otherwise print what this package is.
|
|
217
|
+
const staged = stagedCliPath();
|
|
218
|
+
if (staged) {
|
|
219
|
+
delegate(staged, args, {});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
printPlaceholder();
|
|
224
|
+
process.exit(args.length === 0 ? 0 : 1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pairling",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Pair your iPhone with the AI coding agents running on your Mac. CLI and local runtime installer for the Pairling iOS app.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pairling",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"codex",
|
|
9
|
+
"ios",
|
|
10
|
+
"companion",
|
|
11
|
+
"agents",
|
|
12
|
+
"cli"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://pairling.dev",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/mergimg0/pairling-helper/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/mergimg0/pairling-helper.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "UNLICENSED",
|
|
23
|
+
"author": "Pairling (https://pairling.dev)",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"pairling": "bin/pairling.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/pairling.mjs",
|
|
30
|
+
"payload",
|
|
31
|
+
"payload-manifest.json"
|
|
32
|
+
],
|
|
33
|
+
"os": [
|
|
34
|
+
"darwin"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|