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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -2
  3. package/TaplineNative.podspec +14 -0
  4. package/dist/actions.d.ts +203 -0
  5. package/dist/actions.js +1991 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent.d.ts +117 -0
  8. package/dist/agent.js +352 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/alerts.d.ts +35 -0
  11. package/dist/alerts.js +437 -0
  12. package/dist/alerts.js.map +1 -0
  13. package/dist/awake.d.ts +2 -0
  14. package/dist/awake.js +53 -0
  15. package/dist/awake.js.map +1 -0
  16. package/dist/checks.d.ts +35 -0
  17. package/dist/checks.js +501 -0
  18. package/dist/checks.js.map +1 -0
  19. package/dist/client.d.ts +111 -0
  20. package/dist/client.js +446 -0
  21. package/dist/client.js.map +1 -0
  22. package/dist/commands.d.ts +24 -0
  23. package/dist/commands.js +111 -0
  24. package/dist/commands.js.map +1 -0
  25. package/dist/events.d.ts +5 -0
  26. package/dist/events.js +22 -0
  27. package/dist/events.js.map +1 -0
  28. package/dist/feed.d.ts +117 -0
  29. package/dist/feed.js +440 -0
  30. package/dist/feed.js.map +1 -0
  31. package/dist/fiber.d.ts +43 -0
  32. package/dist/fiber.js +100 -0
  33. package/dist/fiber.js.map +1 -0
  34. package/dist/index.d.ts +50 -0
  35. package/dist/index.js +181 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/inspect.d.ts +249 -0
  38. package/dist/inspect.js +1354 -0
  39. package/dist/inspect.js.map +1 -0
  40. package/dist/keyboard.d.ts +10 -0
  41. package/dist/keyboard.js +63 -0
  42. package/dist/keyboard.js.map +1 -0
  43. package/dist/link.d.ts +21 -0
  44. package/dist/link.js +63 -0
  45. package/dist/link.js.map +1 -0
  46. package/dist/live.d.ts +30 -0
  47. package/dist/live.js +279 -0
  48. package/dist/live.js.map +1 -0
  49. package/dist/logs.d.ts +16 -0
  50. package/dist/logs.js +171 -0
  51. package/dist/logs.js.map +1 -0
  52. package/dist/methods.d.ts +3 -0
  53. package/dist/methods.js +49 -0
  54. package/dist/methods.js.map +1 -0
  55. package/dist/native.d.ts +34 -0
  56. package/dist/native.js +63 -0
  57. package/dist/native.js.map +1 -0
  58. package/dist/network.d.ts +27 -0
  59. package/dist/network.js +238 -0
  60. package/dist/network.js.map +1 -0
  61. package/dist/register.d.ts +24 -0
  62. package/dist/register.js +180 -0
  63. package/dist/register.js.map +1 -0
  64. package/dist/settle.d.ts +27 -0
  65. package/dist/settle.js +70 -0
  66. package/dist/settle.js.map +1 -0
  67. package/dist/source.d.ts +60 -0
  68. package/dist/source.js +214 -0
  69. package/dist/source.js.map +1 -0
  70. package/dist/storage.d.ts +10 -0
  71. package/dist/storage.js +45 -0
  72. package/dist/storage.js.map +1 -0
  73. package/dist/touches.d.ts +15 -0
  74. package/dist/touches.js +89 -0
  75. package/dist/touches.js.map +1 -0
  76. package/dist/types.d.ts +168 -0
  77. package/dist/types.js +32 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/wait.d.ts +8 -0
  80. package/dist/wait.js +99 -0
  81. package/dist/wait.js.map +1 -0
  82. package/expo-module.config.json +7 -0
  83. package/ios/TaplineNativeModule.swift +79 -0
  84. package/metro.d.ts +26 -0
  85. package/metro.js +146 -0
  86. package/package.json +100 -4
  87. package/register.d.ts +1 -0
  88. 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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
@@ -0,0 +1,3 @@
1
+ // Fallback for bundlers that do not read package.json "exports":
2
+ // `import 'react-native-tapline/register'` resolves to this file.
3
+ require('./dist/register');