🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

react-native-worktree

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-native-worktree - npm Package Compare versions

Comparing version
1.0.0
to
1.1.0
+1
-3
bin/cli.js
#!/usr/bin/env node
import { Command } from 'commander';
import initCommand from '../src/commands/init.js';
import addCommand from '../src/commands/add.js';

@@ -16,5 +15,4 @@ import switchCommand from '../src/commands/switch.js';

.description('Metro port switcher with mutex for multi-agent RN development')
.version('1.0.0');
.version('1.1.0');
initCommand(program);
addCommand(program);

@@ -21,0 +19,0 @@ switchCommand(program);

{
"name": "react-native-worktree",
"version": "1.0.0",
"version": "1.1.0",
"description": "Metro port switcher with mutex for multi-agent React Native development",

@@ -5,0 +5,0 @@ "bin": {

@@ -38,7 +38,6 @@ <img width="1440" height="505" alt="Header" src="https://github.com/user-attachments/assets/c77276e5-fef4-46a7-9704-281bc9826cb4" />

The agent will (guided by the skill):
1. Initialize the tool if needed (auto-detects bundle ID and platform)
2. Create a git worktree and register it with an auto-assigned port
3. Install dependencies and start Metro on that port
4. Call `react-native-worktree switch --platform ios` to acquire the device and preview
5. Heartbeat while you test, then release when done
1. Create a git worktree and register it with `add` (auto-detects bundle ID and platform on first run)
2. Install dependencies and start Metro on the assigned port
3. Call `react-native-worktree switch --platform ios` to acquire the device and preview
4. Heartbeat while you test, then release when done

@@ -65,6 +64,7 @@ Meanwhile, another agent in a separate session:

**Android Emulator** — remaps the default Metro port via `adb reverse`, then force-stops and relaunches:
**Android Emulator** — writes `debug_http_host` to the app's default SharedPreferences, sets up `adb reverse` for the actual port, then force-stops and relaunches:
```
adb reverse tcp:8081 tcp:<port>
echo '...localhost:<port>...' | adb shell run-as <packageName> sh -c 'cat > .../<packageName>_preferences.xml'
adb reverse tcp:<port> tcp:<port>
adb shell am force-stop <packageName>

@@ -91,15 +91,5 @@ adb shell monkey -p <packageName> -c android.intent.category.LAUNCHER 1

### `react-native-worktree init`
Initialize configuration. Auto-detects bundle ID from `app.json` / `app.config.js`.
```bash
react-native-worktree init # auto-detect, ios only
react-native-worktree init --bundle-id com.myapp # manual bundle ID
react-native-worktree init --platforms ios,android # both platforms
```
### `react-native-worktree add <name>`
Register a worktree. Port auto-assigned (reuses dead ports, or increments from max).
Register a worktree. On first run, auto-detects bundle ID and platforms from `app.json` / `app.config.js` and creates the config. Port auto-assigned (reuses dead ports, or increments from max).

@@ -106,0 +96,0 @@ ```bash

@@ -1,2 +0,2 @@

import { addWorktree, loadConfig, resolveApp, getAllPorts, computeNextPort } from '../config.js';
import { addWorktree, ensureConfig, ensureApp, saveConfig, getAllPorts, computeNextPort } from '../config.js';
import { isMetroRunning } from '../switcher.js';

@@ -6,13 +6,16 @@ import chalk from 'chalk';

async function findReusablePort(config) {
const ports = getAllPorts(config);
if (ports.length === 0) return null;
// Build port→worktree name map for logging
async function findReusablePort(config, excludeName) {
const ports = [];
const portOwners = {};
for (const [appId, app] of Object.entries(config.apps || {})) {
for (const [name, wt] of Object.entries(app.worktrees || {})) {
portOwners[wt.port] = { name, app: appId };
// Skip the worktree being (re-)added — don't reclaim our own port
if (name === excludeName) continue;
if (wt.port) {
ports.push(wt.port);
portOwners[wt.port] = { name, app: appId };
}
}
}
if (ports.length === 0) return null;

@@ -33,2 +36,10 @@ // Probe each port

function removeWorktreeEntry(config, appId, name) {
const app = config.apps[appId];
if (app?.worktrees?.[name]) {
delete app.worktrees[name];
saveConfig(config);
}
}
export default function addCommand(program) {

@@ -42,14 +53,12 @@ program

.action(async (name, opts) => {
const config = loadConfig();
if (!config) {
console.error(chalk.red('Not initialized. Run `react-native-worktree init` first.'));
process.exit(1);
}
const config = ensureConfig();
const bundleId = resolveApp(config, opts.app);
const { config: updatedConfig, bundleId } = ensureApp(config, opts.app);
if (!bundleId) {
if (opts.app) {
console.error(chalk.red(`App '${opts.app}' not found in config.`));
} else if (Object.keys(updatedConfig.apps).length > 1) {
console.error(chalk.red('Multiple apps configured. Use --app <bundleId> to specify which one.'));
} else {
console.error(chalk.red('Multiple apps configured. Use --app <bundleId> to specify which one.'));
console.error(chalk.red('Could not auto-detect bundle ID. Use --app <bundleId> or run from a directory with app.json.'));
}

@@ -62,9 +71,11 @@ process.exit(1);

if (!port) {
// Try port reclamation
const dead = await findReusablePort(config);
// Try port reclamation (excludes the worktree being added to avoid self-reclaim)
const dead = await findReusablePort(updatedConfig, name);
if (dead) {
port = dead.port;
console.log(chalk.dim(`Reusing port ${port} (Metro stopped for '${dead.owner.name}')`));
// Remove the old worktree entry that owned this port
removeWorktreeEntry(updatedConfig, dead.owner.app, dead.owner.name);
console.log(chalk.dim(`Reusing port ${port} (removed stale worktree '${dead.owner.name}')`));
} else {
port = computeNextPort(config);
port = computeNextPort(updatedConfig);
}

@@ -71,0 +82,0 @@ }

@@ -1,2 +0,2 @@

import { loadConfig } from '../config.js';
import { ensureConfig } from '../config.js';
import { getStatus } from '../lock.js';

@@ -12,6 +12,7 @@ import { isMetroRunning } from '../switcher.js';

.action(async (opts) => {
const config = loadConfig();
if (!config || !config.apps) {
console.error(chalk.red('Not initialized. Run `react-native-worktree init` first.'));
process.exit(1);
const config = ensureConfig();
if (Object.keys(config.apps).length === 0) {
console.log(chalk.dim('No apps configured. Run `react-native-worktree add <name>` to get started.'));
return;
}

@@ -18,0 +19,0 @@

@@ -1,2 +0,2 @@

import { loadConfig, getWorktree, resolveApp, getApp } from '../config.js';
import { ensureConfig, getWorktree, resolveApp, getApp, getPackageName } from '../config.js';
import { waitForLock } from '../lock.js';

@@ -14,7 +14,3 @@ import { switchPort, isMetroRunning } from '../switcher.js';

.action(async (name, opts) => {
const config = loadConfig();
if (!config) {
console.error(chalk.red('Not initialized. Run `react-native-worktree init` first.'));
process.exit(1);
}
const config = ensureConfig();

@@ -25,2 +21,4 @@ const bundleId = resolveApp(config, opts.app);

console.error(chalk.red(`App '${opts.app}' not found in config.`));
} else if (Object.keys(config.apps).length === 0) {
console.error(chalk.red('No apps configured. Run `react-native-worktree add <name>` first.'));
} else {

@@ -65,4 +63,5 @@ console.error(chalk.red('Multiple apps configured. Use --app <bundleId> to specify which one.'));

// Switch port and relaunch app
const packageName = getPackageName(app, bundleId, platform);
try {
switchPort(bundleId, wt.port, platform);
switchPort(packageName, wt.port, platform);
console.log(chalk.green(`[${platform}] Switched to '${chalk.bold(name)}' (port ${wt.port}). App restarting...`));

@@ -69,0 +68,0 @@ } catch (err) {

import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import chalk from 'chalk';

@@ -54,2 +55,105 @@ export function getConfigDir() {

export function detectBundleId(platform) {
// Try app.json
const appJsonPath = join(process.cwd(), 'app.json');
if (existsSync(appJsonPath)) {
try {
const appJson = JSON.parse(readFileSync(appJsonPath, 'utf-8'));
if (platform === 'ios') {
const id = appJson?.expo?.ios?.bundleIdentifier;
if (id) return id;
} else {
const id = appJson?.expo?.android?.package;
if (id) return id;
}
} catch { /* ignore */ }
}
// Try app.config.js (read as text and extract)
const appConfigPath = join(process.cwd(), 'app.config.js');
if (existsSync(appConfigPath)) {
try {
const content = readFileSync(appConfigPath, 'utf-8');
const key = platform === 'ios' ? 'bundleIdentifier' : 'package';
const match = content.match(new RegExp(`${key}\\s*:\\s*["']([^"']+)["']`));
if (match) return match[1];
} catch { /* ignore */ }
}
// Try app.config.ts
const appConfigTsPath = join(process.cwd(), 'app.config.ts');
if (existsSync(appConfigTsPath)) {
try {
const content = readFileSync(appConfigTsPath, 'utf-8');
const key = platform === 'ios' ? 'bundleIdentifier' : 'package';
const match = content.match(new RegExp(`${key}\\s*:\\s*["']([^"']+)["']`));
if (match) return match[1];
} catch { /* ignore */ }
}
return null;
}
export function ensureConfig() {
const existing = loadConfig();
if (existing) return existing;
const config = { apps: {} };
saveConfig(config);
return config;
}
export function ensureApp(config, bundleIdOpt) {
// If explicit bundleId provided, check if it exists
if (bundleIdOpt) {
if (config.apps[bundleIdOpt]) {
return { config, bundleId: bundleIdOpt };
}
return { config, bundleId: null };
}
// If apps already exist, resolve from them
const appIds = Object.keys(config.apps || {});
if (appIds.length === 1) {
return { config, bundleId: appIds[0] };
}
if (appIds.length > 1) {
// Try auto-detect from cwd app.json to match existing app
const iosBundleId = detectBundleId('ios');
const androidPkg = detectBundleId('android');
if (iosBundleId && config.apps[iosBundleId]) return { config, bundleId: iosBundleId };
if (androidPkg && config.apps[androidPkg]) return { config, bundleId: androidPkg };
return { config, bundleId: null };
}
// No apps — auto-detect and create
const iosId = detectBundleId('ios');
const androidId = detectBundleId('android');
let bundleId = iosId || androidId;
if (!bundleId) return { config, bundleId: null };
let platforms;
if (iosId && androidId) {
platforms = ['ios', 'android'];
} else if (iosId) {
platforms = ['ios'];
} else {
platforms = ['android'];
}
console.log(chalk.dim(`Auto-detected app: ${bundleId} (${platforms.join(', ')})`));
const appEntry = { platforms, worktrees: {} };
// Store androidPackage if it differs from the primary bundleId
if (androidId && androidId !== bundleId) {
appEntry.androidPackage = androidId;
console.log(chalk.dim(`Auto-detected Android package: ${androidId}`));
}
config.apps[bundleId] = appEntry;
saveConfig(config);
return { config, bundleId };
}
export function getApp(config, bundleId) {

@@ -59,2 +163,9 @@ return config.apps?.[bundleId] || null;

export function getPackageName(app, bundleId, platform) {
if (platform === 'android' && app.androidPackage) {
return app.androidPackage;
}
return bundleId;
}
export function resolveApp(config, bundleId) {

@@ -117,7 +228,7 @@ if (!config || !config.apps) {

if (!config || !config.apps) {
throw new Error('Not initialized. Run `react-native-worktree init` first.');
throw new Error('No config found. Run `react-native-worktree add <name>` from your project directory.');
}
const app = config.apps[bundleId];
if (!app) {
throw new Error(`App '${bundleId}' not found. Run \`react-native-worktree init --bundle-id ${bundleId}\` first.`);
throw new Error(`App '${bundleId}' not found in config.`);
}

@@ -124,0 +235,0 @@ if (!port) {

@@ -33,3 +33,7 @@ import { execSync } from 'child_process';

function switchAndroid(packageName, port) {
run(`adb reverse tcp:8081 tcp:${port}`);
// Write debug_http_host to default SharedPreferences (<package>_preferences.xml)
const prefsFile = `/data/data/${packageName}/shared_prefs/${packageName}_preferences.xml`;
const xml = `<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?><map><string name=\\"debug_http_host\\">localhost:${port}</string></map>`;
run(`adb shell "echo '${xml}' | run-as ${packageName} sh -c 'cat > ${prefsFile}'"`);
run(`adb reverse tcp:${port} tcp:${port}`);
runQuiet(`adb shell am force-stop ${packageName}`);

@@ -36,0 +40,0 @@ run(`adb shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`);

import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { saveConfig, loadConfig } from '../config.js';
import chalk from 'chalk';
function detectBundleId(platform) {
// Try app.json
const appJsonPath = join(process.cwd(), 'app.json');
if (existsSync(appJsonPath)) {
try {
const appJson = JSON.parse(readFileSync(appJsonPath, 'utf-8'));
if (platform === 'ios') {
const id = appJson?.expo?.ios?.bundleIdentifier;
if (id) return id;
} else {
const id = appJson?.expo?.android?.package;
if (id) return id;
}
} catch { /* ignore */ }
}
// Try app.config.js (read as text and extract)
const appConfigPath = join(process.cwd(), 'app.config.js');
if (existsSync(appConfigPath)) {
try {
const content = readFileSync(appConfigPath, 'utf-8');
const key = platform === 'ios' ? 'bundleIdentifier' : 'package';
const match = content.match(new RegExp(`${key}\\s*:\\s*["']([^"']+)["']`));
if (match) return match[1];
} catch { /* ignore */ }
}
// Try app.config.ts
const appConfigTsPath = join(process.cwd(), 'app.config.ts');
if (existsSync(appConfigTsPath)) {
try {
const content = readFileSync(appConfigTsPath, 'utf-8');
const key = platform === 'ios' ? 'bundleIdentifier' : 'package';
const match = content.match(new RegExp(`${key}\\s*:\\s*["']([^"']+)["']`));
if (match) return match[1];
} catch { /* ignore */ }
}
return null;
}
function parsePlatforms(input) {
const platforms = input.split(',').map(p => p.trim()).filter(Boolean);
for (const p of platforms) {
if (p !== 'ios' && p !== 'android') {
return { error: `Invalid platform: ${p}. Must be 'ios' or 'android'.` };
}
}
if (platforms.length === 0) {
return { error: 'At least one platform is required.' };
}
return { platforms: [...new Set(platforms)] };
}
export default function initCommand(program) {
program
.command('init')
.description('Initialize config with bundle ID and platforms')
.option('--bundle-id <id>', 'App bundle identifier')
.option('--platforms <list>', 'Target platforms, comma-separated (ios,android)', 'ios')
.action((opts) => {
const { platforms, error } = parsePlatforms(opts.platforms);
if (error) {
console.error(chalk.red(error));
process.exit(1);
}
let bundleId = opts.bundleId;
if (!bundleId) {
// Try detecting with first platform
bundleId = detectBundleId(platforms[0]);
if (bundleId) {
console.log(chalk.dim(`Auto-detected bundle ID: ${bundleId}`));
} else {
console.error(chalk.red('Could not auto-detect bundle ID. Use --bundle-id <id>.'));
process.exit(1);
}
}
const existing = loadConfig();
if (existing && existing.apps) {
// Add or update app entry
existing.apps[bundleId] = existing.apps[bundleId] || { platforms: [], worktrees: {} };
existing.apps[bundleId].platforms = platforms;
saveConfig(existing);
} else {
saveConfig({
apps: {
[bundleId]: {
platforms,
worktrees: {},
},
},
});
}
console.log(chalk.green(`Initialized react-native-worktree for ${chalk.bold(bundleId)} (${platforms.join(', ')})`));
});
}