yaver-cli 1.0.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/bin/yaver-push +4 -0
- package/package.json +35 -0
- package/sdk-manifest.json +58 -0
- package/src/analyzer.js +117 -0
- package/src/bundler.js +115 -0
- package/src/commands/devices.js +23 -0
- package/src/commands/doctor.js +88 -0
- package/src/commands/init.js +90 -0
- package/src/commands/modules.js +43 -0
- package/src/commands/push.js +150 -0
- package/src/commands/reset.js +20 -0
- package/src/commands/status.js +42 -0
- package/src/discovery.js +119 -0
- package/src/index.js +93 -0
- package/src/transport.js +137 -0
package/bin/yaver-push
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yaver-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Push existing React Native projects to yaver.io for testing on real devices",
|
|
5
|
+
"bin": {
|
|
6
|
+
"yaver-push": "bin/yaver-push"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"hermesc/",
|
|
13
|
+
"sdk-manifest.json"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"react-native",
|
|
17
|
+
"testing",
|
|
18
|
+
"mobile",
|
|
19
|
+
"yaver",
|
|
20
|
+
"expo-go-alternative"
|
|
21
|
+
],
|
|
22
|
+
"author": "Yaver <hello@yaver.io>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/kivanccakmak/yaver.io.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://yaver.io",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"semver": "^7.6.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdkVersion": "1.0.0",
|
|
3
|
+
"reactNative": "0.81.5",
|
|
4
|
+
"react": "19.1.0",
|
|
5
|
+
"hermes": {
|
|
6
|
+
"version": "0.81.5",
|
|
7
|
+
"bytecodeVersion": 99
|
|
8
|
+
},
|
|
9
|
+
"arch": {
|
|
10
|
+
"newArch": true,
|
|
11
|
+
"fabric": true,
|
|
12
|
+
"bridgeless": false
|
|
13
|
+
},
|
|
14
|
+
"supportedRNRange": "0.81.x",
|
|
15
|
+
"nativeModules": {
|
|
16
|
+
"react-native-screens": "4.16.0",
|
|
17
|
+
"react-native-safe-area-context": "5.6.0",
|
|
18
|
+
"react-native-gesture-handler": "2.28.0",
|
|
19
|
+
"react-native-reanimated": "4.1.1",
|
|
20
|
+
"react-native-svg": "15.15.4",
|
|
21
|
+
"react-native-webview": "13.15.0",
|
|
22
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
23
|
+
"@react-native-community/netinfo": "11.4.1",
|
|
24
|
+
"react-native-maps": "1.20.1",
|
|
25
|
+
"react-native-ble-plx": "3.2.0",
|
|
26
|
+
"react-native-mmkv": "4.3.0",
|
|
27
|
+
"@shopify/react-native-skia": "2.6.2",
|
|
28
|
+
"react-native-udp": "4.1.7",
|
|
29
|
+
"react-native-markdown-display": "7.0.2",
|
|
30
|
+
"victory-native": "41.20.2",
|
|
31
|
+
"whisper.rn": "0.5.5",
|
|
32
|
+
"expo-camera": "17.0.10",
|
|
33
|
+
"expo-location": "19.0.8",
|
|
34
|
+
"expo-sensors": "15.0.8",
|
|
35
|
+
"expo-haptics": "15.0.8",
|
|
36
|
+
"expo-device": "8.0.10",
|
|
37
|
+
"expo-constants": "18.0.13",
|
|
38
|
+
"expo-notifications": "0.32.16",
|
|
39
|
+
"expo-file-system": "19.0.21",
|
|
40
|
+
"expo-asset": "12.0.12",
|
|
41
|
+
"expo-font": "14.0.11",
|
|
42
|
+
"expo-clipboard": "8.0.8",
|
|
43
|
+
"expo-linking": "8.0.11",
|
|
44
|
+
"expo-secure-store": "15.0.8",
|
|
45
|
+
"expo-av": "16.0.8",
|
|
46
|
+
"expo-image-picker": "17.0.10",
|
|
47
|
+
"expo-speech": "14.0.8",
|
|
48
|
+
"expo-web-browser": "15.0.10",
|
|
49
|
+
"expo-apple-authentication": "8.0.8",
|
|
50
|
+
"expo-battery": "10.0.8",
|
|
51
|
+
"expo-brightness": "14.0.8",
|
|
52
|
+
"expo-localization": "55.0.11",
|
|
53
|
+
"expo-share-intent": "3.2.3",
|
|
54
|
+
"expo-sharing": "14.0.8",
|
|
55
|
+
"expo-splash-screen": "31.0.13",
|
|
56
|
+
"expo-status-bar": "3.0.9"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/analyzer.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const semver = require('semver');
|
|
3
|
+
|
|
4
|
+
// Pure JS packages — never need native code
|
|
5
|
+
const PURE_JS_PACKAGES = new Set([
|
|
6
|
+
'axios', 'lodash', 'moment', 'date-fns', 'uuid', 'dayjs',
|
|
7
|
+
'zustand', 'jotai', 'redux', '@reduxjs/toolkit', 'mobx', 'mobx-react',
|
|
8
|
+
'react-query', '@tanstack/react-query',
|
|
9
|
+
'formik', 'yup', 'zod', 'react-hook-form',
|
|
10
|
+
'i18next', 'react-i18next',
|
|
11
|
+
'nativewind', 'twrnc', 'styled-components', '@emotion/native',
|
|
12
|
+
'swr', 'immer',
|
|
13
|
+
'@react-navigation/native', '@react-navigation/stack',
|
|
14
|
+
'@react-navigation/bottom-tabs', '@react-navigation/drawer',
|
|
15
|
+
'@react-navigation/native-stack',
|
|
16
|
+
'@react-navigation/material-top-tabs',
|
|
17
|
+
'react-native-web', 'react-dom',
|
|
18
|
+
'@expo/metro-runtime', 'expo-router', 'expo-splash-screen', 'expo-status-bar',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// Packages that LOOK native (react-native-*) but are pure JS
|
|
22
|
+
const FALSE_POSITIVE_NATIVE = new Set([
|
|
23
|
+
'react-native-paper', 'react-native-elements',
|
|
24
|
+
'react-native-size-matters', 'react-native-responsive-screen',
|
|
25
|
+
'react-native-toast-message', 'react-native-responsive-fontsize',
|
|
26
|
+
'react-native-iphone-x-helper', 'react-native-status-bar-height',
|
|
27
|
+
'react-native-markdown-display',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function analyzeProject(packageJson, sdkManifest) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
const warnings = [];
|
|
33
|
+
const availableModules = [];
|
|
34
|
+
const missingModules = [];
|
|
35
|
+
|
|
36
|
+
const allDeps = { ...packageJson.dependencies, ...packageJson.peerDependencies };
|
|
37
|
+
|
|
38
|
+
// 1. React Native version
|
|
39
|
+
const projectRN = cleanVersion(allDeps['react-native'] || '');
|
|
40
|
+
const sdkRN = sdkManifest.reactNative;
|
|
41
|
+
|
|
42
|
+
if (projectRN && sdkRN) {
|
|
43
|
+
const projParsed = semver.coerce(projectRN);
|
|
44
|
+
const sdkParsed = semver.coerce(sdkRN);
|
|
45
|
+
|
|
46
|
+
if (projParsed && sdkParsed) {
|
|
47
|
+
if (semver.major(projParsed) !== semver.major(sdkParsed)) {
|
|
48
|
+
errors.push({
|
|
49
|
+
type: 'rn_major_mismatch',
|
|
50
|
+
message: `React Native major version mismatch: project ${projectRN}, yaver ${sdkRN}. Incompatible.`,
|
|
51
|
+
});
|
|
52
|
+
} else if (semver.minor(projParsed) !== semver.minor(sdkParsed)) {
|
|
53
|
+
warnings.push({
|
|
54
|
+
type: 'rn_minor_mismatch',
|
|
55
|
+
message: `React Native ${projectRN} vs yaver ${sdkRN}. Minor version differs — may work.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Architecture check
|
|
62
|
+
const newArchEnabled = packageJson.reactNative?.newArchEnabled === true;
|
|
63
|
+
if (newArchEnabled && !sdkManifest.arch.newArch) {
|
|
64
|
+
errors.push({
|
|
65
|
+
type: 'arch_mismatch',
|
|
66
|
+
message: 'Project uses New Architecture but yaver uses classic bridge. Incompatible.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. Native module check
|
|
71
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
72
|
+
if (name === 'react' || name === 'react-native') continue;
|
|
73
|
+
if (PURE_JS_PACKAGES.has(name)) continue;
|
|
74
|
+
if (FALSE_POSITIVE_NATIVE.has(name)) continue;
|
|
75
|
+
if (packageJson.devDependencies?.[name] && !packageJson.dependencies?.[name]) continue;
|
|
76
|
+
if (!looksLikeNativeModule(name)) continue;
|
|
77
|
+
|
|
78
|
+
const sdkVersion = sdkManifest.nativeModules[name];
|
|
79
|
+
|
|
80
|
+
if (!sdkVersion) {
|
|
81
|
+
missingModules.push({ name, version: cleanVersion(version), reason: 'not in yaver SDK' });
|
|
82
|
+
errors.push({
|
|
83
|
+
type: 'missing_module', module: name, version,
|
|
84
|
+
message: `"${name}" requires native code but is NOT in the yaver SDK.`,
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
const cleanLocal = cleanVersion(version);
|
|
88
|
+
availableModules.push({ name, projectVersion: cleanLocal, sdkVersion });
|
|
89
|
+
|
|
90
|
+
const localParsed = semver.coerce(cleanLocal);
|
|
91
|
+
const sdkParsed = semver.coerce(sdkVersion);
|
|
92
|
+
if (localParsed && sdkParsed && semver.major(localParsed) !== semver.major(sdkParsed)) {
|
|
93
|
+
warnings.push({
|
|
94
|
+
type: 'version_mismatch', module: name,
|
|
95
|
+
message: `"${name}": project ${version}, yaver ${sdkVersion}. Major version differs.`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { reactNativeVersion: projectRN, errors, warnings, availableModules, missingModules };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function looksLikeNativeModule(name) {
|
|
105
|
+
return name.startsWith('react-native-') ||
|
|
106
|
+
name.startsWith('@react-native-') ||
|
|
107
|
+
name.startsWith('@react-native/') ||
|
|
108
|
+
name.startsWith('rn') ||
|
|
109
|
+
name.startsWith('expo-') ||
|
|
110
|
+
name.startsWith('@shopify/react-native-');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cleanVersion(v) {
|
|
114
|
+
return (v || '').replace(/[\^~>=<\s]/g, '');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { analyzeProject, PURE_JS_PACKAGES, FALSE_POSITIVE_NATIVE };
|
package/src/bundler.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const { execSync, execFileSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
function getHermescPath() {
|
|
7
|
+
const key = `${os.platform()}-${os.arch()}`;
|
|
8
|
+
const dir = path.join(__dirname, '..', 'hermesc');
|
|
9
|
+
const ext = os.platform() === 'win32' ? 'hermesc.exe' : 'hermesc';
|
|
10
|
+
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.join(dir, key, ext),
|
|
13
|
+
path.join(dir, 'darwin-arm64', ext),
|
|
14
|
+
path.join(dir, 'linux-x64', ext),
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const found = candidates.find(p => fs.existsSync(p));
|
|
18
|
+
if (!found) {
|
|
19
|
+
// Fall back to project's hermesc (react-native ships one)
|
|
20
|
+
const rnHermesc = findRNHermesc();
|
|
21
|
+
if (rnHermesc) return rnHermesc;
|
|
22
|
+
|
|
23
|
+
throw new Error(
|
|
24
|
+
`hermesc not found for ${key}. Install @yaver/cli hermesc binaries or ensure react-native is installed.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try { fs.chmodSync(found, 0o755); } catch {}
|
|
29
|
+
return found;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Find hermesc from the project's react-native installation */
|
|
33
|
+
function findRNHermesc() {
|
|
34
|
+
const candidates = [
|
|
35
|
+
// RN 0.81+
|
|
36
|
+
path.join('node_modules', 'react-native', 'sdks', 'hermesc', getPlatformBin(), 'hermesc'),
|
|
37
|
+
// Older RN
|
|
38
|
+
path.join('node_modules', 'hermes-engine', getPlatformBin(), 'hermesc'),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const c of candidates) {
|
|
42
|
+
if (fs.existsSync(c)) {
|
|
43
|
+
try { fs.chmodSync(c, 0o755); } catch {}
|
|
44
|
+
return c;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPlatformBin() {
|
|
51
|
+
const p = os.platform();
|
|
52
|
+
const a = os.arch();
|
|
53
|
+
if (p === 'darwin') return a === 'arm64' ? 'osx-bin' : 'osx-bin';
|
|
54
|
+
if (p === 'linux') return 'linux64-bin';
|
|
55
|
+
if (p === 'win32') return 'win64-bin';
|
|
56
|
+
return 'osx-bin';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function bundle({ platform, entryFile, outputDir, dev = false, minify = true }) {
|
|
60
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
61
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const bundlePath = path.join(outputDir, 'main.jsbundle');
|
|
64
|
+
const assetsDir = path.join(outputDir, 'assets');
|
|
65
|
+
|
|
66
|
+
const cmd = [
|
|
67
|
+
'npx react-native bundle',
|
|
68
|
+
`--platform ${platform}`,
|
|
69
|
+
`--entry-file ${entryFile}`,
|
|
70
|
+
`--bundle-output ${bundlePath}`,
|
|
71
|
+
`--assets-dest ${assetsDir}`,
|
|
72
|
+
`--dev ${dev}`,
|
|
73
|
+
`--minify ${minify}`,
|
|
74
|
+
'--reset-cache',
|
|
75
|
+
].join(' ');
|
|
76
|
+
|
|
77
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(bundlePath)) {
|
|
80
|
+
throw new Error(`Bundle not found at ${bundlePath}. Check react-native bundle output.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return bundlePath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function compileHermes({ inputPath, outputPath }) {
|
|
87
|
+
const hermesc = getHermescPath();
|
|
88
|
+
const tmp = inputPath + '.tmp';
|
|
89
|
+
fs.renameSync(inputPath, tmp);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
execFileSync(hermesc, ['-emit-binary', '-out', outputPath, '-O', tmp], { stdio: 'pipe' });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// Restore original on failure
|
|
95
|
+
fs.renameSync(tmp, inputPath);
|
|
96
|
+
const stderr = err.stderr?.toString() || err.message;
|
|
97
|
+
throw new Error(`Hermes compile failed: ${stderr}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fs.unlinkSync(tmp);
|
|
101
|
+
return outputPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readBytecodeVersion(hbcPath) {
|
|
105
|
+
const buf = fs.readFileSync(hbcPath);
|
|
106
|
+
if (buf.length < 8) return null;
|
|
107
|
+
|
|
108
|
+
// Hermes magic: 0x1F1903C1 (little-endian)
|
|
109
|
+
const magic = buf.readUInt32LE(0);
|
|
110
|
+
if (magic !== 0x1F1903C1) return null;
|
|
111
|
+
|
|
112
|
+
return buf.readUInt32LE(4);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { bundle, compileHermes, readBytecodeVersion, getHermescPath };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { scanLAN, fetchHealth, YAVER_PORT } = require('../discovery');
|
|
2
|
+
|
|
3
|
+
async function devices() {
|
|
4
|
+
console.log('📡 Scanning for yaver.io devices on your network...\n');
|
|
5
|
+
|
|
6
|
+
const found = await scanLAN();
|
|
7
|
+
|
|
8
|
+
if (found.length === 0) {
|
|
9
|
+
console.log(' No devices found.\n');
|
|
10
|
+
console.log(' Make sure the yaver.io app is open on your phone');
|
|
11
|
+
console.log(' and both devices are on the same WiFi network.\n');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log(` Found ${found.length} device(s):\n`);
|
|
16
|
+
for (const d of found) {
|
|
17
|
+
console.log(` 📱 ${d.name} (${d.platform})`);
|
|
18
|
+
console.log(` IP: ${d.ip}:${d.port}`);
|
|
19
|
+
console.log(` Push: yaver-push push --device ${d.ip}\n`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { devices };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { analyzeProject } = require('../analyzer');
|
|
3
|
+
|
|
4
|
+
async function doctor() {
|
|
5
|
+
if (!fs.existsSync('package.json')) {
|
|
6
|
+
console.error('❌ No package.json found. Run this from your RN project root.');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
11
|
+
const sdkManifest = require('../../sdk-manifest.json');
|
|
12
|
+
const analysis = analyzeProject(pkg, sdkManifest);
|
|
13
|
+
|
|
14
|
+
console.log('\n📋 Yaver Compatibility Report\n');
|
|
15
|
+
console.log(` Yaver SDK: v${sdkManifest.sdkVersion}`);
|
|
16
|
+
console.log(` SDK RN: ${sdkManifest.reactNative}`);
|
|
17
|
+
console.log(` SDK Hermes BC: ${sdkManifest.hermes.bytecodeVersion}`);
|
|
18
|
+
console.log(` Your RN: ${analysis.reactNativeVersion || 'not found'}`);
|
|
19
|
+
console.log(` New Arch: ${sdkManifest.arch.newArch ? 'enabled' : 'disabled'}\n`);
|
|
20
|
+
|
|
21
|
+
// Available modules
|
|
22
|
+
if (analysis.availableModules.length > 0) {
|
|
23
|
+
console.log('─── Available Native Modules ────────────────────\n');
|
|
24
|
+
console.log(' These will work in yaver.io:\n');
|
|
25
|
+
for (const m of analysis.availableModules) {
|
|
26
|
+
const warn = analysis.warnings.find(w => w.module === m.name);
|
|
27
|
+
if (warn) {
|
|
28
|
+
console.log(` ⚠️ ${m.name}: project ${m.projectVersion}, yaver ${m.sdkVersion}`);
|
|
29
|
+
} else {
|
|
30
|
+
console.log(` ✅ ${m.name}@${m.projectVersion}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
console.log('');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Missing modules
|
|
37
|
+
if (analysis.missingModules.length > 0) {
|
|
38
|
+
console.log('─── Missing Native Modules ─────────────────────\n');
|
|
39
|
+
console.log(' These need native code that yaver.io doesn\'t ship.');
|
|
40
|
+
console.log(' Your app WILL crash if it calls them.\n');
|
|
41
|
+
|
|
42
|
+
for (const m of analysis.missingModules) {
|
|
43
|
+
console.log(` ❌ ${m.name}@${m.version}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('\n Handle gracefully in your existing code:\n');
|
|
47
|
+
console.log(' import { NativeModules } from \'react-native\';');
|
|
48
|
+
console.log(' const isYaver = !!NativeModules.YaverInfo;\n');
|
|
49
|
+
console.log(' if (isYaver) {');
|
|
50
|
+
console.log(' // skip this feature or show placeholder');
|
|
51
|
+
console.log(' } else {');
|
|
52
|
+
console.log(' // use the native module normally');
|
|
53
|
+
console.log(' }\n');
|
|
54
|
+
|
|
55
|
+
console.log(' For lazy-loaded modules (avoids import crash):\n');
|
|
56
|
+
console.log(' const MyModule = NativeModules.MyModule');
|
|
57
|
+
console.log(' ? require(\'react-native-my-module\').default');
|
|
58
|
+
console.log(' : null;\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Errors
|
|
62
|
+
const hardErrors = analysis.errors.filter(e => e.type !== 'missing_module');
|
|
63
|
+
if (hardErrors.length > 0) {
|
|
64
|
+
console.log('─���─ Critical Issues ────────────────────────────\n');
|
|
65
|
+
for (const e of hardErrors) {
|
|
66
|
+
console.log(` 🚫 ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
console.log('');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// All SDK modules
|
|
72
|
+
console.log('─── All Yaver SDK Modules ──────────────────────\n');
|
|
73
|
+
for (const [name, version] of Object.entries(sdkManifest.nativeModules)) {
|
|
74
|
+
const inProject = analysis.availableModules.find(m => m.name === name);
|
|
75
|
+
console.log(` ${name}@${version}${inProject ? ' ← used in your project' : ''}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
// Summary
|
|
80
|
+
const total = analysis.availableModules.length + analysis.missingModules.length;
|
|
81
|
+
console.log(`─── Summary ────────────────────────────────────\n`);
|
|
82
|
+
console.log(` ${analysis.availableModules.length}/${total} native modules available`);
|
|
83
|
+
console.log(` ${analysis.missingModules.length} missing (push with --ignore-missing)`);
|
|
84
|
+
console.log(` ${analysis.warnings.length} warnings`);
|
|
85
|
+
console.log(` ${hardErrors.length} critical issues\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { doctor };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { analyzeProject } = require('../analyzer');
|
|
4
|
+
|
|
5
|
+
async function init() {
|
|
6
|
+
if (!fs.existsSync('package.json')) {
|
|
7
|
+
console.error('❌ No package.json found. Run this from your RN project root.');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
12
|
+
if (!pkg.dependencies?.['react-native']) {
|
|
13
|
+
console.error('❌ react-native not found in dependencies. Is this a React Native project?');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('🔍 Analyzing your project...\n');
|
|
18
|
+
|
|
19
|
+
const sdkManifest = require('../../sdk-manifest.json');
|
|
20
|
+
const analysis = analyzeProject(pkg, sdkManifest);
|
|
21
|
+
|
|
22
|
+
printAnalysis(analysis, sdkManifest);
|
|
23
|
+
|
|
24
|
+
// Create yaver.json — does NOT touch the developer's code
|
|
25
|
+
const yaverJson = {
|
|
26
|
+
sdkVersion: sdkManifest.sdkVersion,
|
|
27
|
+
analyzedAt: new Date().toISOString(),
|
|
28
|
+
projectRN: analysis.reactNativeVersion,
|
|
29
|
+
compatible: analysis.errors.length === 0,
|
|
30
|
+
missingModules: analysis.missingModules.map(m => m.name),
|
|
31
|
+
availableModules: analysis.availableModules.map(m => m.name),
|
|
32
|
+
warnings: analysis.warnings.map(w => w.message),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
fs.writeFileSync('yaver.json', JSON.stringify(yaverJson, null, 2));
|
|
36
|
+
console.log('\n✅ Created yaver.json');
|
|
37
|
+
|
|
38
|
+
if (analysis.errors.filter(e => e.type === 'missing_module').length > 0) {
|
|
39
|
+
console.log('\n⚠️ Your project has compatibility issues (see above).');
|
|
40
|
+
console.log(' You can still push with: yaver-push push --ignore-missing');
|
|
41
|
+
console.log(' Features using missing modules will crash.\n');
|
|
42
|
+
} else if (analysis.errors.length > 0) {
|
|
43
|
+
console.log('\n🚫 Incompatible — see errors above.\n');
|
|
44
|
+
} else {
|
|
45
|
+
console.log('\n🎉 Fully compatible! Run: yaver-push push\n');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printAnalysis(analysis, sdkManifest) {
|
|
50
|
+
// RN version
|
|
51
|
+
const rnStatus = analysis.errors.find(e => e.type === 'rn_major_mismatch') ? '❌' :
|
|
52
|
+
analysis.warnings.find(w => w.type === 'rn_minor_mismatch') ? '⚠️' : '✅';
|
|
53
|
+
console.log(` React Native: ${analysis.reactNativeVersion || 'unknown'} ${rnStatus} (yaver supports ${sdkManifest.supportedRNRange})`);
|
|
54
|
+
|
|
55
|
+
// Hermes
|
|
56
|
+
console.log(` Hermes: enabled ✅`);
|
|
57
|
+
|
|
58
|
+
// Arch
|
|
59
|
+
const archStatus = analysis.errors.find(e => e.type === 'arch_mismatch') ? '❌' : '✅';
|
|
60
|
+
console.log(` New Arch: ${sdkManifest.arch.newArch ? 'enabled' : 'disabled'} ${archStatus}`);
|
|
61
|
+
|
|
62
|
+
// Available modules
|
|
63
|
+
if (analysis.availableModules.length > 0) {
|
|
64
|
+
console.log('\n Native modules found in your project:');
|
|
65
|
+
for (const m of analysis.availableModules) {
|
|
66
|
+
const warn = analysis.warnings.find(w => w.module === m.name);
|
|
67
|
+
const icon = warn ? '⚠️' : '✅';
|
|
68
|
+
console.log(` ${m.name}@${m.projectVersion} ${icon} available in yaver`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Missing modules
|
|
73
|
+
if (analysis.missingModules.length > 0) {
|
|
74
|
+
console.log(`\n ⚠️ ${analysis.missingModules.length} native module(s) NOT available in yaver.io:`);
|
|
75
|
+
for (const m of analysis.missingModules) {
|
|
76
|
+
console.log(` • ${m.name}@${m.version} — ${m.reason}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Warnings
|
|
81
|
+
const otherWarnings = analysis.warnings.filter(w => w.type !== 'rn_minor_mismatch' && w.type !== 'version_mismatch');
|
|
82
|
+
if (otherWarnings.length > 0) {
|
|
83
|
+
console.log('\n Warnings:');
|
|
84
|
+
for (const w of otherWarnings) {
|
|
85
|
+
console.log(` ⚠️ ${w.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { init };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
async function modules() {
|
|
2
|
+
const sdkManifest = require('../../sdk-manifest.json');
|
|
3
|
+
|
|
4
|
+
console.log(`\n📦 Yaver SDK v${sdkManifest.sdkVersion} — Native Modules\n`);
|
|
5
|
+
console.log(` React Native: ${sdkManifest.reactNative}`);
|
|
6
|
+
console.log(` Hermes BC: ${sdkManifest.hermes.bytecodeVersion}`);
|
|
7
|
+
console.log(` New Arch: ${sdkManifest.arch.newArch ? 'enabled' : 'disabled'}\n`);
|
|
8
|
+
|
|
9
|
+
const entries = Object.entries(sdkManifest.nativeModules);
|
|
10
|
+
|
|
11
|
+
// Group by prefix
|
|
12
|
+
const expo = entries.filter(([n]) => n.startsWith('expo-'));
|
|
13
|
+
const rn = entries.filter(([n]) => n.startsWith('react-native-') || n.startsWith('@react-native'));
|
|
14
|
+
const other = entries.filter(([n]) => !n.startsWith('expo-') && !n.startsWith('react-native-') && !n.startsWith('@react-native'));
|
|
15
|
+
|
|
16
|
+
if (rn.length > 0) {
|
|
17
|
+
console.log(' React Native Community:');
|
|
18
|
+
for (const [name, version] of rn) {
|
|
19
|
+
console.log(` ${name}@${version}`);
|
|
20
|
+
}
|
|
21
|
+
console.log('');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (expo.length > 0) {
|
|
25
|
+
console.log(' Expo Modules:');
|
|
26
|
+
for (const [name, version] of expo) {
|
|
27
|
+
console.log(` ${name}@${version}`);
|
|
28
|
+
}
|
|
29
|
+
console.log('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (other.length > 0) {
|
|
33
|
+
console.log(' Other:');
|
|
34
|
+
for (const [name, version] of other) {
|
|
35
|
+
console.log(` ${name}@${version}`);
|
|
36
|
+
}
|
|
37
|
+
console.log('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(` Total: ${entries.length} native modules\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { modules };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { analyzeProject } = require('../analyzer');
|
|
4
|
+
const { bundle, compileHermes, readBytecodeVersion } = require('../bundler');
|
|
5
|
+
const { discoverDevice, fetchHealth } = require('../discovery');
|
|
6
|
+
const { pushBundle, pushAssets } = require('../transport');
|
|
7
|
+
|
|
8
|
+
async function push(options = {}) {
|
|
9
|
+
const startTime = Date.now();
|
|
10
|
+
const quiet = options.quiet;
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
12
|
+
const sdkManifest = require('../../sdk-manifest.json');
|
|
13
|
+
|
|
14
|
+
// Find device
|
|
15
|
+
if (!quiet) console.log('📡 Finding device...');
|
|
16
|
+
const device = await discoverDevice(options.device);
|
|
17
|
+
if (!quiet) console.log(`✅ Found: ${device.name} (${device.ip})`);
|
|
18
|
+
|
|
19
|
+
// Fetch health + verify SDK
|
|
20
|
+
const health = await fetchHealth(device);
|
|
21
|
+
const platform = health.platform || 'ios';
|
|
22
|
+
|
|
23
|
+
if (health.hermes?.bytecodeVersion !== sdkManifest.hermes.bytecodeVersion) {
|
|
24
|
+
console.error(`�� Hermes BC mismatch: device BC${health.hermes?.bytecodeVersion}, CLI BC${sdkManifest.hermes.bytecodeVersion}`);
|
|
25
|
+
console.error(' Update @yaver/cli or yaver.io app.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Analyze compatibility
|
|
30
|
+
const analysis = analyzeProject(pkg, sdkManifest);
|
|
31
|
+
|
|
32
|
+
const hardErrors = analysis.errors.filter(e =>
|
|
33
|
+
e.type === 'rn_major_mismatch' || e.type === 'arch_mismatch'
|
|
34
|
+
);
|
|
35
|
+
if (hardErrors.length > 0) {
|
|
36
|
+
console.error('\n🚫 INCOMPATIBLE:\n');
|
|
37
|
+
hardErrors.forEach(e => console.error(` ${e.message}`));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (analysis.missingModules.length > 0 && !options.ignoreMissing) {
|
|
42
|
+
console.warn(`\n⚠��� ${analysis.missingModules.length} native module(s) NOT in yaver SDK:`);
|
|
43
|
+
analysis.missingModules.forEach(m => console.warn(` • ${m.name}@${m.version}`));
|
|
44
|
+
console.warn('\n App will crash if it calls these modules.');
|
|
45
|
+
console.warn(' Push anyway: yaver-push push --ignore-missing\n');
|
|
46
|
+
if (!options.force) process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!quiet) console.log('✅ Compatible');
|
|
50
|
+
|
|
51
|
+
// Bundle
|
|
52
|
+
if (!quiet) console.log(`🔨 Bundling for ${platform}...`);
|
|
53
|
+
const entryFile = findEntryFile(pkg);
|
|
54
|
+
const buildDir = path.resolve('.yaver-build');
|
|
55
|
+
const bundlePath = await bundle({ platform, entryFile, outputDir: buildDir, dev: false, minify: true });
|
|
56
|
+
|
|
57
|
+
// Hermes compile
|
|
58
|
+
if (!quiet) console.log('⚡ Compiling Hermes bytecode...');
|
|
59
|
+
await compileHermes({ inputPath: bundlePath, outputPath: bundlePath });
|
|
60
|
+
|
|
61
|
+
const bcVersion = readBytecodeVersion(bundlePath);
|
|
62
|
+
if (bcVersion !== sdkManifest.hermes.bytecodeVersion) {
|
|
63
|
+
console.error(`❌ hermesc produced BC${bcVersion}, expected BC${sdkManifest.hermes.bytecodeVersion}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Push
|
|
68
|
+
const moduleName = getModuleName(pkg);
|
|
69
|
+
const bundleData = fs.readFileSync(bundlePath);
|
|
70
|
+
if (!quiet) console.log(`📤 Pushing ${(bundleData.length / 1024).toFixed(1)} KB...`);
|
|
71
|
+
|
|
72
|
+
const result = await pushBundle(device, bundleData, {
|
|
73
|
+
moduleName,
|
|
74
|
+
appName: moduleName,
|
|
75
|
+
sdkVersion: sdkManifest.sdkVersion,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (result.status !== 'ok') {
|
|
79
|
+
console.error(`❌ Device rejected: ${result.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Assets
|
|
84
|
+
const assetsDir = path.join(buildDir, 'assets');
|
|
85
|
+
if (fs.existsSync(assetsDir)) {
|
|
86
|
+
const files = fs.readdirSync(assetsDir, { recursive: true });
|
|
87
|
+
if (files.length > 0) {
|
|
88
|
+
if (!quiet) console.log('📤 Pushing assets...');
|
|
89
|
+
await pushAssets(device, assetsDir);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
94
|
+
if (!quiet) console.log(`\n🚀 Done in ${elapsed}s — app loading on ${device.name}\n`);
|
|
95
|
+
|
|
96
|
+
// Watch mode
|
|
97
|
+
if (options.watch) {
|
|
98
|
+
await watchAndPush(options, device, pkg, sdkManifest);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function watchAndPush(options, device, pkg, sdkManifest) {
|
|
103
|
+
console.log('👀 Watching for changes...');
|
|
104
|
+
|
|
105
|
+
const watchDirs = ['src', 'app', 'lib', 'components', 'screens', 'utils', 'hooks']
|
|
106
|
+
.filter(d => fs.existsSync(d));
|
|
107
|
+
|
|
108
|
+
if (watchDirs.length === 0) watchDirs.push('.');
|
|
109
|
+
|
|
110
|
+
const { watch } = require('fs');
|
|
111
|
+
let debounce = null;
|
|
112
|
+
|
|
113
|
+
for (const dir of watchDirs) {
|
|
114
|
+
fs.watch(dir, { recursive: true }, (event, filename) => {
|
|
115
|
+
if (!filename || !filename.match(/\.(js|jsx|ts|tsx|json)$/)) return;
|
|
116
|
+
if (filename.includes('node_modules') || filename.includes('.yaver-build')) return;
|
|
117
|
+
|
|
118
|
+
clearTimeout(debounce);
|
|
119
|
+
debounce = setTimeout(async () => {
|
|
120
|
+
console.log(`📝 ${filename} changed`);
|
|
121
|
+
try {
|
|
122
|
+
await push({ ...options, watch: false, quiet: true, device: device.ip });
|
|
123
|
+
console.log('📤 Re-pushed — done');
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`❌ Re-push failed: ${err.message}`);
|
|
126
|
+
}
|
|
127
|
+
}, 300);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findEntryFile(pkg) {
|
|
133
|
+
if (pkg.main && fs.existsSync(pkg.main)) return pkg.main;
|
|
134
|
+
const candidates = ['index.js', 'index.tsx', 'index.ts', 'src/index.js', 'src/index.tsx', 'App.js', 'App.tsx'];
|
|
135
|
+
return candidates.find(f => fs.existsSync(f)) || 'index.js';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getModuleName(pkg) {
|
|
139
|
+
if (fs.existsSync('app.json')) {
|
|
140
|
+
try {
|
|
141
|
+
const a = JSON.parse(fs.readFileSync('app.json', 'utf8'));
|
|
142
|
+
if (a.name) return a.name;
|
|
143
|
+
if (a.displayName) return a.displayName;
|
|
144
|
+
if (a.expo?.name) return a.expo.name;
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
147
|
+
return pkg.name || 'App';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { push };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { discoverDevice } = require('../discovery');
|
|
2
|
+
const { resetDevice } = require('../transport');
|
|
3
|
+
|
|
4
|
+
async function reset(options = {}) {
|
|
5
|
+
console.log('📡 Finding device...');
|
|
6
|
+
const device = await discoverDevice(options.device);
|
|
7
|
+
console.log(`✅ Found: ${device.name} (${device.ip})`);
|
|
8
|
+
|
|
9
|
+
console.log('🔄 Resetting device...');
|
|
10
|
+
const result = await resetDevice(device);
|
|
11
|
+
|
|
12
|
+
if (result.status === 'ok') {
|
|
13
|
+
console.log('✅ Bundle cleared — device will show default UI');
|
|
14
|
+
} else {
|
|
15
|
+
console.error(`❌ Reset failed: ${result.message}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { reset };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { discoverDevice, fetchHealth } = require('../discovery');
|
|
3
|
+
|
|
4
|
+
async function status(options = {}) {
|
|
5
|
+
// Project status
|
|
6
|
+
console.log('\n📋 Project Status\n');
|
|
7
|
+
|
|
8
|
+
if (fs.existsSync('yaver.json')) {
|
|
9
|
+
const yj = JSON.parse(fs.readFileSync('yaver.json', 'utf8'));
|
|
10
|
+
console.log(` SDK Version: ${yj.sdkVersion}`);
|
|
11
|
+
console.log(` Project RN: ${yj.projectRN}`);
|
|
12
|
+
console.log(` Compatible: ${yj.compatible ? '✅' : '❌'}`);
|
|
13
|
+
console.log(` Available: ${yj.availableModules?.length || 0} native modules`);
|
|
14
|
+
console.log(` Missing: ${yj.missingModules?.length || 0} native modules`);
|
|
15
|
+
console.log(` Analyzed: ${yj.analyzedAt}`);
|
|
16
|
+
} else {
|
|
17
|
+
console.log(' No yaver.json found. Run: yaver-push init');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Device status
|
|
21
|
+
console.log('\n📱 Device Status\n');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const device = await discoverDevice(options.device);
|
|
25
|
+
const health = await fetchHealth(device);
|
|
26
|
+
|
|
27
|
+
console.log(` Name: ${health.deviceName || device.name}`);
|
|
28
|
+
console.log(` Platform: ${health.platform}`);
|
|
29
|
+
console.log(` IP: ${device.ip}:${device.port}`);
|
|
30
|
+
console.log(` SDK Version: ${health.sdkVersion}`);
|
|
31
|
+
console.log(` RN Version: ${health.reactNative}`);
|
|
32
|
+
console.log(` Hermes BC: ${health.hermes?.bytecodeVersion}`);
|
|
33
|
+
console.log(` Has Bundle: ${health.hasBundle ? '✅' : '❌'}`);
|
|
34
|
+
console.log(` App Version: ${health.appVersion} (build ${health.build})`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.log(` Not connected: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { status };
|
package/src/discovery.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const dgram = require('dgram');
|
|
3
|
+
|
|
4
|
+
const YAVER_PORT = 8347;
|
|
5
|
+
const BEACON_PORT = 19837;
|
|
6
|
+
const DISCOVERY_TIMEOUT = 5000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Discover a yaver.io device on the network.
|
|
10
|
+
* Priority: 1) manual IP, 2) saved config, 3) UDP beacon scan, 4) mDNS
|
|
11
|
+
*/
|
|
12
|
+
async function discoverDevice(manualIp) {
|
|
13
|
+
if (manualIp) {
|
|
14
|
+
const health = await fetchHealth({ ip: manualIp, port: YAVER_PORT });
|
|
15
|
+
return { ip: manualIp, port: YAVER_PORT, name: health.deviceName || manualIp, platform: health.platform };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Try UDP beacon discovery
|
|
19
|
+
const beaconDevice = await listenForBeacon(DISCOVERY_TIMEOUT);
|
|
20
|
+
if (beaconDevice) {
|
|
21
|
+
return beaconDevice;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error(
|
|
25
|
+
'No yaver.io device found on network.\n' +
|
|
26
|
+
' Make sure the yaver.io app is open on your phone (same WiFi).\n' +
|
|
27
|
+
' Or specify device IP: yaver-push push --device <ip>'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Listen for UDP beacon from yaver.io app */
|
|
32
|
+
function listenForBeacon(timeout) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
socket.close();
|
|
37
|
+
resolve(null);
|
|
38
|
+
}, timeout);
|
|
39
|
+
|
|
40
|
+
socket.on('message', (msg) => {
|
|
41
|
+
try {
|
|
42
|
+
const beacon = JSON.parse(msg.toString());
|
|
43
|
+
if (beacon.v === 1 && beacon.p) {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
socket.close();
|
|
46
|
+
resolve({
|
|
47
|
+
ip: beacon.ip || socket.remoteAddress,
|
|
48
|
+
port: beacon.p,
|
|
49
|
+
name: beacon.n || 'Unknown Device',
|
|
50
|
+
id: beacon.id,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
socket.on('error', () => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
socket.close();
|
|
59
|
+
resolve(null);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
socket.bind(BEACON_PORT, () => {
|
|
63
|
+
socket.setBroadcast(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Fetch /health from a device */
|
|
69
|
+
function fetchHealth(device) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const url = `http://${device.ip}:${device.port || YAVER_PORT}/health`;
|
|
72
|
+
const req = http.get(url, { timeout: 5000 }, (res) => {
|
|
73
|
+
let data = '';
|
|
74
|
+
res.on('data', chunk => data += chunk);
|
|
75
|
+
res.on('end', () => {
|
|
76
|
+
try {
|
|
77
|
+
resolve(JSON.parse(data));
|
|
78
|
+
} catch {
|
|
79
|
+
reject(new Error(`Invalid JSON from ${url}`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
req.on('error', (err) => reject(new Error(`Cannot reach ${device.ip}:${device.port || YAVER_PORT} — ${err.message}`)));
|
|
84
|
+
req.on('timeout', () => {
|
|
85
|
+
req.destroy();
|
|
86
|
+
reject(new Error(`Timeout connecting to ${device.ip}:${device.port || YAVER_PORT}`));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Scan common LAN subnets for yaver.io devices */
|
|
92
|
+
async function scanLAN() {
|
|
93
|
+
const os = require('os');
|
|
94
|
+
const interfaces = os.networkInterfaces();
|
|
95
|
+
const found = [];
|
|
96
|
+
|
|
97
|
+
for (const [, addrs] of Object.entries(interfaces)) {
|
|
98
|
+
for (const addr of addrs) {
|
|
99
|
+
if (addr.family !== 'IPv4' || addr.internal) continue;
|
|
100
|
+
// Try common IPs on this subnet
|
|
101
|
+
const subnet = addr.address.split('.').slice(0, 3).join('.');
|
|
102
|
+
const promises = [];
|
|
103
|
+
for (let i = 1; i <= 254; i++) {
|
|
104
|
+
const ip = `${subnet}.${i}`;
|
|
105
|
+
if (ip === addr.address) continue;
|
|
106
|
+
promises.push(
|
|
107
|
+
fetchHealth({ ip, port: YAVER_PORT })
|
|
108
|
+
.then(h => found.push({ ip, port: YAVER_PORT, name: h.deviceName, platform: h.platform }))
|
|
109
|
+
.catch(() => {})
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
await Promise.all(promises);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { discoverDevice, fetchHealth, scanLAN, YAVER_PORT };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const { init } = require('./commands/init');
|
|
2
|
+
const { push } = require('./commands/push');
|
|
3
|
+
const { doctor } = require('./commands/doctor');
|
|
4
|
+
const { devices } = require('./commands/devices');
|
|
5
|
+
const { modules } = require('./commands/modules');
|
|
6
|
+
const { reset } = require('./commands/reset');
|
|
7
|
+
const { status } = require('./commands/status');
|
|
8
|
+
|
|
9
|
+
const HELP = `
|
|
10
|
+
yaver-push — Push existing React Native projects to yaver.io
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
init Analyze project, show compatibility, create yaver.json
|
|
14
|
+
push [--device <ip>] [--watch] Bundle + validate + push to device
|
|
15
|
+
push --ignore-missing Push even with missing native modules
|
|
16
|
+
doctor Deep compatibility report with fix suggestions
|
|
17
|
+
devices List discovered devices
|
|
18
|
+
modules List all SDK native modules
|
|
19
|
+
reset [--device <ip>] Clear pushed bundle on device
|
|
20
|
+
status [--device <ip>] Device + project status
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--device <ip> Target device IP (skips discovery)
|
|
24
|
+
--watch Re-push on file save
|
|
25
|
+
--ignore-missing Push despite missing native modules
|
|
26
|
+
--force Skip confirmation prompts
|
|
27
|
+
--help Show this help
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
async function run(args) {
|
|
31
|
+
const command = args[0];
|
|
32
|
+
const options = parseArgs(args.slice(1));
|
|
33
|
+
|
|
34
|
+
if (!command || command === '--help' || command === '-h') {
|
|
35
|
+
console.log(HELP);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
switch (command) {
|
|
41
|
+
case 'init':
|
|
42
|
+
await init(options);
|
|
43
|
+
break;
|
|
44
|
+
case 'push':
|
|
45
|
+
await push(options);
|
|
46
|
+
break;
|
|
47
|
+
case 'doctor':
|
|
48
|
+
await doctor(options);
|
|
49
|
+
break;
|
|
50
|
+
case 'devices':
|
|
51
|
+
await devices(options);
|
|
52
|
+
break;
|
|
53
|
+
case 'modules':
|
|
54
|
+
await modules(options);
|
|
55
|
+
break;
|
|
56
|
+
case 'reset':
|
|
57
|
+
await reset(options);
|
|
58
|
+
break;
|
|
59
|
+
case 'status':
|
|
60
|
+
await status(options);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
console.error(`Unknown command: ${command}`);
|
|
64
|
+
console.log(HELP);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`\n❌ ${err.message}`);
|
|
69
|
+
if (process.env.YAVER_DEBUG) console.error(err.stack);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseArgs(args) {
|
|
75
|
+
const opts = {};
|
|
76
|
+
for (let i = 0; i < args.length; i++) {
|
|
77
|
+
const arg = args[i];
|
|
78
|
+
if (arg === '--device' && args[i + 1]) {
|
|
79
|
+
opts.device = args[++i];
|
|
80
|
+
} else if (arg === '--watch') {
|
|
81
|
+
opts.watch = true;
|
|
82
|
+
} else if (arg === '--ignore-missing') {
|
|
83
|
+
opts.ignoreMissing = true;
|
|
84
|
+
} else if (arg === '--force') {
|
|
85
|
+
opts.force = true;
|
|
86
|
+
} else if (arg === '--quiet' || arg === '-q') {
|
|
87
|
+
opts.quiet = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return opts;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { run };
|
package/src/transport.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { YAVER_PORT } = require('./discovery');
|
|
6
|
+
|
|
7
|
+
/** Push a Hermes bytecode bundle to the device */
|
|
8
|
+
function pushBundle(device, bundleData, metadata = {}) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const options = {
|
|
11
|
+
hostname: device.ip,
|
|
12
|
+
port: device.port || YAVER_PORT,
|
|
13
|
+
path: '/bundle',
|
|
14
|
+
method: 'POST',
|
|
15
|
+
timeout: 30000,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/octet-stream',
|
|
18
|
+
'Content-Length': bundleData.length,
|
|
19
|
+
'X-Module-Name': metadata.moduleName || 'main',
|
|
20
|
+
'X-App-Name': metadata.appName || '',
|
|
21
|
+
'X-SDK-Version': metadata.sdkVersion || '',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const req = http.request(options, (res) => {
|
|
26
|
+
let data = '';
|
|
27
|
+
res.on('data', chunk => data += chunk);
|
|
28
|
+
res.on('end', () => {
|
|
29
|
+
try {
|
|
30
|
+
const result = JSON.parse(data);
|
|
31
|
+
if (res.statusCode !== 200) {
|
|
32
|
+
reject(new Error(result.message || `HTTP ${res.statusCode}`));
|
|
33
|
+
} else {
|
|
34
|
+
resolve(result);
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
reject(new Error(`Invalid response from device: ${data}`));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
req.on('error', (err) => reject(new Error(`Push failed: ${err.message}`)));
|
|
43
|
+
req.on('timeout', () => {
|
|
44
|
+
req.destroy();
|
|
45
|
+
reject(new Error('Push timed out'));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
req.write(bundleData);
|
|
49
|
+
req.end();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Push assets directory to the device */
|
|
54
|
+
async function pushAssets(device, assetsDir) {
|
|
55
|
+
// For now, send individual files. A tar approach would be more efficient.
|
|
56
|
+
const files = getAllFiles(assetsDir);
|
|
57
|
+
if (files.length === 0) return;
|
|
58
|
+
|
|
59
|
+
// Concatenate all assets into a simple tar-like format
|
|
60
|
+
// The device will unpack them
|
|
61
|
+
const buffers = [];
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
const relPath = path.relative(assetsDir, file);
|
|
64
|
+
const data = fs.readFileSync(file);
|
|
65
|
+
buffers.push(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const allData = Buffer.concat(buffers);
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const options = {
|
|
72
|
+
hostname: device.ip,
|
|
73
|
+
port: device.port || YAVER_PORT,
|
|
74
|
+
path: '/assets',
|
|
75
|
+
method: 'POST',
|
|
76
|
+
timeout: 30000,
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/octet-stream',
|
|
79
|
+
'Content-Length': allData.length,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const req = http.request(options, (res) => {
|
|
84
|
+
let data = '';
|
|
85
|
+
res.on('data', chunk => data += chunk);
|
|
86
|
+
res.on('end', () => {
|
|
87
|
+
try { resolve(JSON.parse(data)); } catch { resolve({}); }
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
req.on('error', (err) => reject(new Error(`Asset push failed: ${err.message}`)));
|
|
92
|
+
req.write(allData);
|
|
93
|
+
req.end();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Send POST /reset to device */
|
|
98
|
+
function resetDevice(device) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const options = {
|
|
101
|
+
hostname: device.ip,
|
|
102
|
+
port: device.port || YAVER_PORT,
|
|
103
|
+
path: '/reset',
|
|
104
|
+
method: 'POST',
|
|
105
|
+
timeout: 10000,
|
|
106
|
+
headers: { 'Content-Length': 0 },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const req = http.request(options, (res) => {
|
|
110
|
+
let data = '';
|
|
111
|
+
res.on('data', chunk => data += chunk);
|
|
112
|
+
res.on('end', () => {
|
|
113
|
+
try { resolve(JSON.parse(data)); } catch { resolve({}); }
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
req.on('error', (err) => reject(new Error(`Reset failed: ${err.message}`)));
|
|
118
|
+
req.end();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getAllFiles(dir) {
|
|
123
|
+
const results = [];
|
|
124
|
+
if (!fs.existsSync(dir)) return results;
|
|
125
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const full = path.join(dir, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
results.push(...getAllFiles(full));
|
|
130
|
+
} else {
|
|
131
|
+
results.push(full);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { pushBundle, pushAssets, resetDevice };
|