rn-iso 0.2.0 → 0.3.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/README.md +1 -1
- package/bin/cli.js +4 -1
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/src/commands/release.js +50 -7
- package/src/commands/status.js +20 -5
- package/src/commands/stop.js +6 -16
- package/src/config.js +8 -0
- package/src/metro.js +7 -0
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ All commands below take the same `npx rn-iso` prefix.
|
|
|
47
47
|
| `status` | Show all projects' state |
|
|
48
48
|
| `reserve [ios\|android]` | Lock a manually-started sim/emulator to the current project (no build) |
|
|
49
49
|
| `unreserve [ios\|android]` | Drop the current project's lock without shutting the sim down |
|
|
50
|
-
| `release [<shortcut>\|<path>] [--platform <p>] [--shutdown]` | Free a project's assignment
|
|
50
|
+
| `release [<port>\|<shortcut>\|<path>] [--platform <p>] [--shutdown]` | Free a project's assignment. Target can be a Metro port (`8083`), a shortcut (label or unique basename), or an absolute path. `--shutdown` also stops the sim. |
|
|
51
51
|
|
|
52
52
|
## How it works
|
|
53
53
|
|
package/bin/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
4
|
import deviceCommand from '../src/commands/device.js';
|
|
4
5
|
import iosCommand from '../src/commands/ios.js';
|
|
@@ -10,11 +11,13 @@ import releaseCommand from '../src/commands/release.js';
|
|
|
10
11
|
import reserveCommand from '../src/commands/reserve.js';
|
|
11
12
|
import unreserveCommand from '../src/commands/unreserve.js';
|
|
12
13
|
|
|
14
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
15
|
+
|
|
13
16
|
const program = new Command();
|
|
14
17
|
program
|
|
15
18
|
.name('rn-iso')
|
|
16
19
|
.description('Isolated React Native dev environments per project/worktree')
|
|
17
|
-
.version(
|
|
20
|
+
.version(pkg.version);
|
|
18
21
|
|
|
19
22
|
deviceCommand(program);
|
|
20
23
|
iosCommand(program);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -82,7 +82,7 @@ Reserve binds the sim to the current project the same way `ios` does, but skips
|
|
|
82
82
|
- `npx rn-iso status` — show all projects, their assignments, and Metro state.
|
|
83
83
|
- `npx rn-iso start` — start Metro detached on the project's assigned port WITHOUT building/installing. Useful to keep Metro alive across builds.
|
|
84
84
|
- `npx rn-iso stop [<port>|<shortcut>|<path>]` — kill Metro. No arg = current project. Passing a port (e.g. `8083`) kills whatever is on it; a project shortcut (label or unique basename) or absolute path targets that project. Finds the process by port, so it works whether Metro was started by `npx rn-iso start` or by the build CLI.
|
|
85
|
-
- `npx rn-iso release [<shortcut>|<path>] [--platform <p>] [--shutdown]` — free a project's sim assignment. Defaults to the current project. `--shutdown` also stops the sim/emulator.
|
|
85
|
+
- `npx rn-iso release [<port>|<shortcut>|<path>] [--platform <p>] [--shutdown]` — free a project's sim assignment. Defaults to the current project. Target can also be a Metro port (`8083`) or a shortcut (label / unique basename). `--shutdown` also stops the sim/emulator.
|
|
86
86
|
|
|
87
87
|
### Project shortcuts (--label)
|
|
88
88
|
|
package/src/commands/release.js
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
// src/commands/release.js
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import prompts from 'prompts';
|
|
3
4
|
import { resolveRegisteredProject } from '../project.js';
|
|
4
|
-
import { getProject, clearDevice } from '../config.js';
|
|
5
|
+
import { getProject, clearDevice, findProjectByMetroPort } from '../config.js';
|
|
6
|
+
import { findPidListeningOnPort } from '../metro.js';
|
|
5
7
|
import { shutdownIosSim } from '../sim/ios.js';
|
|
6
8
|
import { shutdownAndroidEmulator } from '../sim/android.js';
|
|
7
9
|
|
|
8
10
|
export default function releaseCommand(program) {
|
|
9
11
|
program
|
|
10
12
|
.command('release [target]')
|
|
11
|
-
.description('Free a project assignment. [target] is an absolute
|
|
13
|
+
.description('Free a project assignment. [target] is a Metro port (e.g. 8083), a project shortcut (label or unique basename), or an absolute path. Defaults to the current project.')
|
|
12
14
|
.option('--platform <platform>', 'ios or android (default: both)')
|
|
13
15
|
.option('--shutdown', 'Also shut down the simulator/emulator after releasing')
|
|
14
|
-
.action((target, opts) => {
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
.action(async (target, opts) => {
|
|
17
|
+
let found;
|
|
18
|
+
if (target && /^\d+$/.test(target)) {
|
|
19
|
+
const port = parseInt(target, 10);
|
|
20
|
+
found = findProjectByMetroPort(port);
|
|
21
|
+
if (!found) {
|
|
22
|
+
await handleUnmatchedPort(port);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const result = resolveRegisteredProject(target);
|
|
27
|
+
if (!result.found) {
|
|
28
|
+
console.error(chalk.red(result.error));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
found = result.found;
|
|
19
32
|
}
|
|
20
33
|
const proj = getProject(found);
|
|
21
34
|
if (!proj) {
|
|
@@ -43,3 +56,33 @@ export default function releaseCommand(program) {
|
|
|
43
56
|
}
|
|
44
57
|
});
|
|
45
58
|
}
|
|
59
|
+
|
|
60
|
+
async function handleUnmatchedPort(port) {
|
|
61
|
+
const pid = findPidListeningOnPort(port);
|
|
62
|
+
if (!pid) {
|
|
63
|
+
console.error(chalk.red(`No registered project has Metro port ${port}, and nothing is listening on that port.`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk.dim(`No registered project has Metro port ${port}, but pid ${pid} is listening.`));
|
|
67
|
+
if (!process.stdin.isTTY) {
|
|
68
|
+
console.error(chalk.red('Refusing to kill an unrecognized process under non-TTY (no way to confirm).'));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const ok = await prompts({
|
|
72
|
+
type: 'confirm',
|
|
73
|
+
name: 'ok',
|
|
74
|
+
message: `Kill pid ${pid} on port ${port}?`,
|
|
75
|
+
initial: false,
|
|
76
|
+
});
|
|
77
|
+
if (!ok.ok) {
|
|
78
|
+
console.error(chalk.red('Cancelled.'));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
process.kill(pid, 'SIGTERM');
|
|
83
|
+
console.log(chalk.green(`Killed pid ${pid} on port ${port}.`));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error(chalk.red(`Could not kill pid ${pid}: ${e.message}`));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -3,7 +3,8 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { loadConfig } from '../config.js';
|
|
4
4
|
import { isMetroRunning } from '../ports.js';
|
|
5
5
|
import { isPidAlive } from '../metro.js';
|
|
6
|
-
import { findProjectRoot } from '../project.js';
|
|
6
|
+
import { findProjectRoot, projectShortcut } from '../project.js';
|
|
7
|
+
import { listAllIosSims } from '../sim/ios.js';
|
|
7
8
|
|
|
8
9
|
export default function statusCommand(program) {
|
|
9
10
|
program
|
|
@@ -19,11 +20,20 @@ export default function statusCommand(program) {
|
|
|
19
20
|
|
|
20
21
|
const cwdRoot = findProjectRoot(process.cwd());
|
|
21
22
|
|
|
23
|
+
// Build a UDID -> sim name map so we can show "iPhone 16 Pro (UDID)"
|
|
24
|
+
// rather than a raw UDID. Tolerate missing simctl (Linux dev box, etc.).
|
|
25
|
+
let iosNameByUdid = {};
|
|
26
|
+
try {
|
|
27
|
+
for (const sim of listAllIosSims()) iosNameByUdid[sim.udid] = sim.name;
|
|
28
|
+
} catch { /* no simctl available; we'll just print the udid */ }
|
|
29
|
+
|
|
22
30
|
for (const [path, proj] of Object.entries(cfg?.projects || {})) {
|
|
23
31
|
const isCurrent = path === cwdRoot;
|
|
24
|
-
const
|
|
32
|
+
const shortcut = projectShortcut(path, proj);
|
|
33
|
+
const headerText = `${shortcut} ${chalk.dim(`(${path})`)}`;
|
|
34
|
+
const header = isCurrent ? chalk.bold.cyan(`* ${shortcut}`) + ' ' + chalk.dim(`(${path})`) : headerText;
|
|
25
35
|
console.log('\n' + header);
|
|
26
|
-
console.log(chalk.dim(` app: ${proj.bundleId} (${proj.isExpo ? 'expo' : 'bare'})`));
|
|
36
|
+
console.log(chalk.dim(` app: ${proj.bundleId ?? '?'} (${proj.isExpo ? 'expo' : 'bare'})`));
|
|
27
37
|
|
|
28
38
|
if (proj.metroPort) {
|
|
29
39
|
const running = await isMetroRunning(proj.metroPort);
|
|
@@ -37,9 +47,14 @@ export default function statusCommand(program) {
|
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
const ios = proj.platforms?.ios;
|
|
40
|
-
if (ios)
|
|
50
|
+
if (ios) {
|
|
51
|
+
const name = iosNameByUdid[ios.deviceUdid] || chalk.dim('(unknown sim)');
|
|
52
|
+
console.log(` ios: ${chalk.cyan(name)} ${chalk.dim(`(${ios.deviceUdid})`)}`);
|
|
53
|
+
}
|
|
41
54
|
const android = proj.platforms?.android;
|
|
42
|
-
if (android)
|
|
55
|
+
if (android) {
|
|
56
|
+
console.log(` android: ${chalk.cyan(android.avdName)} ${chalk.dim(`(emulator-${android.consolePort})`)}`);
|
|
57
|
+
}
|
|
43
58
|
}
|
|
44
59
|
console.log('');
|
|
45
60
|
});
|
package/src/commands/stop.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// src/commands/stop.js
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { resolveRegisteredProject } from '../project.js';
|
|
4
|
-
import {
|
|
5
|
-
import { killMetroByPid } from '../metro.js';
|
|
6
|
-
import { getExecutor } from '../exec.js';
|
|
4
|
+
import { getProject, setMetro, findProjectByMetroPort } from '../config.js';
|
|
5
|
+
import { killMetroByPid, findPidListeningOnPort } from '../metro.js';
|
|
7
6
|
|
|
8
7
|
export default function stopCommand(program) {
|
|
9
8
|
program
|
|
@@ -65,19 +64,10 @@ function killByPort(port) {
|
|
|
65
64
|
console.log(chalk.green(`Killed pid ${pid} on port ${port}`));
|
|
66
65
|
|
|
67
66
|
// If a project owned this port, clear its recorded pid so `status` reflects.
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
console.log(chalk.dim(`Cleared metroPid for ${path}`));
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
67
|
+
const owner = findProjectByMetroPort(port);
|
|
68
|
+
if (owner) {
|
|
69
|
+
setMetro(owner, port, null);
|
|
70
|
+
console.log(chalk.dim(`Cleared metroPid for ${owner}`));
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
function findPidListeningOnPort(port) {
|
|
79
|
-
const out = getExecutor().runQuiet(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
|
|
80
|
-
if (!out) return null;
|
|
81
|
-
const pid = parseInt(out.split('\n')[0], 10);
|
|
82
|
-
return Number.isFinite(pid) ? pid : null;
|
|
83
|
-
}
|
package/src/config.js
CHANGED
|
@@ -96,6 +96,14 @@ export function allMetroPorts() {
|
|
|
96
96
|
.filter(p => typeof p === 'number');
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export function findProjectByMetroPort(port) {
|
|
100
|
+
const cfg = loadConfig();
|
|
101
|
+
for (const [path, proj] of Object.entries(cfg?.projects || {})) {
|
|
102
|
+
if (proj.metroPort === port) return path;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
export function allClaimedDevices() {
|
|
100
108
|
const cfg = loadConfig();
|
|
101
109
|
const result = {
|
package/src/metro.js
CHANGED
|
@@ -52,6 +52,13 @@ export function killMetroByPid(pid) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export function findPidListeningOnPort(port) {
|
|
56
|
+
const out = getExecutor().runQuiet(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
|
|
57
|
+
if (!out) return null;
|
|
58
|
+
const pid = parseInt(out.split('\n')[0], 10);
|
|
59
|
+
return Number.isFinite(pid) ? pid : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
export function isPidAlive(pid) {
|
|
56
63
|
if (!pid) return false;
|
|
57
64
|
try {
|