react-native-tapline 0.0.0-stage → 1.1.0
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/LICENSE +21 -0
- package/README.md +86 -2
- package/TaplineNative.podspec +14 -0
- package/dist/actions.d.ts +203 -0
- package/dist/actions.js +1991 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent.d.ts +117 -0
- package/dist/agent.js +352 -0
- package/dist/agent.js.map +1 -0
- package/dist/alerts.d.ts +35 -0
- package/dist/alerts.js +437 -0
- package/dist/alerts.js.map +1 -0
- package/dist/awake.d.ts +2 -0
- package/dist/awake.js +53 -0
- package/dist/awake.js.map +1 -0
- package/dist/checks.d.ts +35 -0
- package/dist/checks.js +501 -0
- package/dist/checks.js.map +1 -0
- package/dist/client.d.ts +111 -0
- package/dist/client.js +446 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +24 -0
- package/dist/commands.js +111 -0
- package/dist/commands.js.map +1 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +22 -0
- package/dist/events.js.map +1 -0
- package/dist/feed.d.ts +117 -0
- package/dist/feed.js +440 -0
- package/dist/feed.js.map +1 -0
- package/dist/fiber.d.ts +43 -0
- package/dist/fiber.js +100 -0
- package/dist/fiber.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +181 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +249 -0
- package/dist/inspect.js +1354 -0
- package/dist/inspect.js.map +1 -0
- package/dist/keyboard.d.ts +10 -0
- package/dist/keyboard.js +63 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/link.d.ts +21 -0
- package/dist/link.js +63 -0
- package/dist/link.js.map +1 -0
- package/dist/live.d.ts +30 -0
- package/dist/live.js +279 -0
- package/dist/live.js.map +1 -0
- package/dist/logs.d.ts +16 -0
- package/dist/logs.js +171 -0
- package/dist/logs.js.map +1 -0
- package/dist/methods.d.ts +3 -0
- package/dist/methods.js +49 -0
- package/dist/methods.js.map +1 -0
- package/dist/native.d.ts +34 -0
- package/dist/native.js +63 -0
- package/dist/native.js.map +1 -0
- package/dist/network.d.ts +27 -0
- package/dist/network.js +238 -0
- package/dist/network.js.map +1 -0
- package/dist/register.d.ts +24 -0
- package/dist/register.js +180 -0
- package/dist/register.js.map +1 -0
- package/dist/settle.d.ts +27 -0
- package/dist/settle.js +70 -0
- package/dist/settle.js.map +1 -0
- package/dist/source.d.ts +60 -0
- package/dist/source.js +214 -0
- package/dist/source.js.map +1 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.js +45 -0
- package/dist/storage.js.map +1 -0
- package/dist/touches.d.ts +15 -0
- package/dist/touches.js +89 -0
- package/dist/touches.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/dist/wait.d.ts +8 -0
- package/dist/wait.js +99 -0
- package/dist/wait.js.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/TaplineNativeModule.swift +79 -0
- package/metro.d.ts +26 -0
- package/metro.js +146 -0
- package/package.json +100 -4
- package/register.d.ts +1 -0
- package/register.js +3 -0
package/metro.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
/** @typedef {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void} Middleware */
|
|
9
|
+
|
|
10
|
+
/** Private LAN, link-local, loopback: the development offer stays on the Wi-Fi (never Tailscale or the internet). */
|
|
11
|
+
/** @param {string | undefined} address */
|
|
12
|
+
function isLocalNetwork(address) {
|
|
13
|
+
if (typeof address !== 'string') return false;
|
|
14
|
+
const a = address.toLowerCase().replace(/^::ffff:/, '');
|
|
15
|
+
if (a === '127.0.0.1' || a === '::1') return true;
|
|
16
|
+
const v4 = /^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(a);
|
|
17
|
+
if (v4) {
|
|
18
|
+
const x = Number(v4[1]);
|
|
19
|
+
const y = Number(v4[2]);
|
|
20
|
+
return x === 10 || (x === 192 && y === 168) || (x === 172 && y >= 16 && y <= 31) || (x === 169 && y === 254);
|
|
21
|
+
}
|
|
22
|
+
// unique-local IPv6, but not Tailscale's own range (fd7a:115c:a1e0::/48)
|
|
23
|
+
return (/^f[cd][0-9a-f]{2}:/.test(a) && !a.startsWith('fd7a:115c:a1e0:')) || a.startsWith('fe80:');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Claims the offer in `file` for `peer` (the first device to fetch it): false when another device has it.
|
|
28
|
+
* @param {string} file @param {string} peer
|
|
29
|
+
*/
|
|
30
|
+
function claimOffer(file, peer) {
|
|
31
|
+
try {
|
|
32
|
+
const o = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
33
|
+
if (typeof o.claimedBy === 'string') return o.claimedBy === peer;
|
|
34
|
+
o.claimedBy = peer;
|
|
35
|
+
fs.writeFileSync(file, JSON.stringify(o), { mode: 0o600 });
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The newest open development offer a Tapline host on this computer wrote after `pair` (Tapline 1.1, zero-QR
|
|
44
|
+
* pairing), or null. Files live in ~/.tapline/offers (or $TAPLINE_HOME/offers), one per host port.
|
|
45
|
+
* @param {string} dir
|
|
46
|
+
* @returns {{ code: string, name: string, port: number, file: string, claimedBy: string | undefined } | null}
|
|
47
|
+
*/
|
|
48
|
+
function readOffer(dir) {
|
|
49
|
+
let best = null;
|
|
50
|
+
let names = [];
|
|
51
|
+
try {
|
|
52
|
+
names = fs.readdirSync(dir).filter((n) => /^\d+\.json$/.test(n));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
try {
|
|
58
|
+
const file = path.join(dir, name);
|
|
59
|
+
const o = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
60
|
+
if (typeof o.code !== 'string' || typeof o.name !== 'string' || typeof o.port !== 'number' || typeof o.until !== 'number') continue;
|
|
61
|
+
if (o.until <= Date.now()) continue;
|
|
62
|
+
if (best === null || o.until > best.until) best = { ...o, file };
|
|
63
|
+
} catch {
|
|
64
|
+
// a file being written, or not ours
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return best === null ? null : { code: best.code, name: best.name, port: best.port, file: best.file, claimedBy: typeof best.claimedBy === 'string' ? best.claimedBy : undefined };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Answers `GET /tapline/offer` on Metro, which the phone always reaches in development.
|
|
72
|
+
* @param {string} dir
|
|
73
|
+
* @returns {Middleware}
|
|
74
|
+
*/
|
|
75
|
+
function offerMiddleware(dir) {
|
|
76
|
+
return (req, res, next) => {
|
|
77
|
+
if (!req.url || !req.url.startsWith('/tapline/offer')) return next();
|
|
78
|
+
/** @param {number} status @param {Record<string, unknown>} body */
|
|
79
|
+
const answer = (status, body) => {
|
|
80
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
81
|
+
res.end(JSON.stringify(body));
|
|
82
|
+
};
|
|
83
|
+
// Through a proxy (Tailscale Serve, a tunnel) the peer isn't on the Wi-Fi, whatever the socket says.
|
|
84
|
+
const forwarded = req.headers['x-forwarded-for'] !== undefined || req.headers['tailscale-user-login'] !== undefined;
|
|
85
|
+
// A web page could make this request (it sends an Origin, or reaches us under its own name: DNS rebinding).
|
|
86
|
+
const host = String(req.headers.host ?? '').toLowerCase().replace(/:\d+$/, '');
|
|
87
|
+
const byAddress = /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || (host.startsWith('[') && host.endsWith(']')) || host === 'localhost' || host.endsWith('.local');
|
|
88
|
+
const fromPage = req.headers.origin !== undefined || !byAddress;
|
|
89
|
+
const address = req.socket && req.socket.remoteAddress;
|
|
90
|
+
if (forwarded || fromPage || !isLocalNetwork(address)) return answer(403, { error: 'local network only' });
|
|
91
|
+
const offer = readOffer(dir);
|
|
92
|
+
const peer = (address ?? '').toLowerCase().replace(/^::ffff:/, '');
|
|
93
|
+
// The first device to fetch it claims it, and only that device can pair with it.
|
|
94
|
+
if (offer === null || !claimOffer(offer.file, peer)) return answer(404, { error: 'no offer open: call pair on the computer' });
|
|
95
|
+
return answer(200, { code: offer.code, name: offer.name, port: offer.port });
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Metro config helper: runs Tapline's `register` before the app's main module, so the app
|
|
101
|
+
* doesn't need its own entry file just to import it first. Only when Tapline is on
|
|
102
|
+
* (EXPO_PUBLIC_TAPLINE_ENABLED=1 by default), so store builds stay untouched.
|
|
103
|
+
*
|
|
104
|
+
* // metro.config.js
|
|
105
|
+
* const { getDefaultConfig } = require('expo/metro-config');
|
|
106
|
+
* const { withTapline } = require('react-native-tapline/metro');
|
|
107
|
+
* module.exports = withTapline(getDefaultConfig(__dirname));
|
|
108
|
+
*
|
|
109
|
+
* The app still calls startTapline() (for example at the top of app/_layout.tsx).
|
|
110
|
+
*
|
|
111
|
+
* @template {{ serializer?: { getModulesRunBeforeMainModule?: (entryFilePath: string) => string[] } & Record<string, unknown> } & Record<string, unknown>} C
|
|
112
|
+
* @param {C} config
|
|
113
|
+
* @param {{ enabled?: boolean }} [options]
|
|
114
|
+
* @returns {C}
|
|
115
|
+
*/
|
|
116
|
+
function withTapline(config, options = {}) {
|
|
117
|
+
const enabled = options.enabled ?? process.env.EXPO_PUBLIC_TAPLINE_ENABLED === '1';
|
|
118
|
+
if (!enabled) return config;
|
|
119
|
+
// The same file the package's own index imports, so Metro has it in the bundle.
|
|
120
|
+
const register = path.join(__dirname, 'dist', 'register.js');
|
|
121
|
+
const serializer = config.serializer ?? {};
|
|
122
|
+
const previous = serializer.getModulesRunBeforeMainModule;
|
|
123
|
+
const server = /** @type {{ enhanceMiddleware?: (m: Middleware, s: unknown) => Middleware } & Record<string, unknown>} */ (config.server ?? {});
|
|
124
|
+
const enhance = server.enhanceMiddleware;
|
|
125
|
+
const home = process.env.TAPLINE_HOME && process.env.TAPLINE_HOME.length > 0 ? process.env.TAPLINE_HOME : path.join(os.homedir(), '.tapline');
|
|
126
|
+
const offers = offerMiddleware(path.join(home, 'offers'));
|
|
127
|
+
return {
|
|
128
|
+
...config,
|
|
129
|
+
serializer: {
|
|
130
|
+
...serializer,
|
|
131
|
+
/** @param {string} entryFilePath */
|
|
132
|
+
getModulesRunBeforeMainModule: (entryFilePath) => [...(previous ? previous(entryFilePath) : []), register],
|
|
133
|
+
},
|
|
134
|
+
server: {
|
|
135
|
+
...server,
|
|
136
|
+
// Zero-QR pairing: the phone asks Metro for the offer a Tapline host on this computer opened.
|
|
137
|
+
/** @param {Middleware} middleware @param {unknown} metroServer @returns {Middleware} */
|
|
138
|
+
enhanceMiddleware: (middleware, metroServer) => {
|
|
139
|
+
const inner = enhance ? enhance(middleware, metroServer) : middleware;
|
|
140
|
+
return (req, res, next) => offers(req, res, () => inner(req, res, next));
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { withTapline, readOffer, offerMiddleware, isLocalNetwork };
|
package/package.json
CHANGED
|
@@ -1,6 +1,102 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-tapline",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Tapline in-app agent: lets AI agents see and drive your own React Native / Expo app on a test phone on the same Wi-Fi.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/BO5AMIS/tapline.git",
|
|
9
|
+
"directory": "packages/react-native"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/bo5amis/tapline/tree/main/packages/react-native#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/bo5amis/tapline/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"react-native",
|
|
17
|
+
"expo",
|
|
18
|
+
"testing",
|
|
19
|
+
"mcp",
|
|
20
|
+
"ai-agent",
|
|
21
|
+
"e2e",
|
|
22
|
+
"ios"
|
|
23
|
+
],
|
|
24
|
+
"main": "dist/index.js",
|
|
25
|
+
"types": "dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./register": {
|
|
32
|
+
"types": "./dist/register.d.ts",
|
|
33
|
+
"default": "./dist/register.js"
|
|
34
|
+
},
|
|
35
|
+
"./metro": {
|
|
36
|
+
"types": "./metro.d.ts",
|
|
37
|
+
"default": "./metro.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"typesVersions": {
|
|
42
|
+
"*": {
|
|
43
|
+
"register": [
|
|
44
|
+
"dist/register.d.ts"
|
|
45
|
+
],
|
|
46
|
+
"metro": [
|
|
47
|
+
"metro.d.ts"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"register.js",
|
|
54
|
+
"register.d.ts",
|
|
55
|
+
"metro.js",
|
|
56
|
+
"metro.d.ts",
|
|
57
|
+
"README.md",
|
|
58
|
+
"ios",
|
|
59
|
+
"expo-module.config.json",
|
|
60
|
+
"TaplineNative.podspec"
|
|
61
|
+
],
|
|
62
|
+
"sideEffects": [
|
|
63
|
+
"./dist/register.js",
|
|
64
|
+
"./register.js"
|
|
65
|
+
],
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsc -p tsconfig.build.json",
|
|
68
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
69
|
+
"test": "jest"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"expo-keep-awake": ">=12",
|
|
73
|
+
"expo-secure-store": ">=12",
|
|
74
|
+
"react": ">=18",
|
|
75
|
+
"react-native": ">=0.72",
|
|
76
|
+
"react-native-view-shot": ">=3"
|
|
77
|
+
},
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"expo-keep-awake": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"expo-secure-store": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"react-native-view-shot": {
|
|
86
|
+
"optional": true
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"@react-native/babel-preset": "0.88.1",
|
|
91
|
+
"@react-native/jest-preset": "0.88.1",
|
|
92
|
+
"@types/jest": "^29.5.14",
|
|
93
|
+
"@types/node": "^22.20.5",
|
|
94
|
+
"@types/react": "^19.1.1",
|
|
95
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
96
|
+
"jest": "^29.7.0",
|
|
97
|
+
"react": "19.3.1",
|
|
98
|
+
"react-native": "0.88.1",
|
|
99
|
+
"react-test-renderer": "19.3.1",
|
|
100
|
+
"typescript": "^5.9.0"
|
|
101
|
+
}
|
|
102
|
+
}
|
package/register.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/register';
|
package/register.js
ADDED