rn-iso 0.3.3 → 0.4.1
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 +29 -2
- package/bin/cli.js +2 -0
- package/package.json +1 -1
- package/skill/SKILL.md +2 -0
- package/src/commands/android.js +11 -6
- package/src/commands/config.js +102 -0
- package/src/commands/ios.js +11 -6
- package/src/config.js +64 -0
- package/src/runner.js +23 -6
package/README.md
CHANGED
|
@@ -27,6 +27,13 @@ Both run side by side. For non-interactive / agent use, pass `--auto` to skip th
|
|
|
27
27
|
npx rn-iso ios --auto
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
Forward extra flags to the underlying build command with `--`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx rn-iso ios -- --variant=release
|
|
34
|
+
npx rn-iso android -- --mode=diaRelease --terminal=Ghostty
|
|
35
|
+
```
|
|
36
|
+
|
|
30
37
|
For AI coding agents, install the skill so the agent knows how to drive the CLI:
|
|
31
38
|
|
|
32
39
|
```bash
|
|
@@ -39,8 +46,8 @@ All commands below take the same `npx rn-iso` prefix.
|
|
|
39
46
|
|
|
40
47
|
| Command | Purpose |
|
|
41
48
|
|---|---|
|
|
42
|
-
| `ios [--auto] [--device-type <name>] [--runtime <ver>] [--script <name>] [--pm <name>] [--no-script] [--no-install]` | Ensure iOS sim + Metro + build/install |
|
|
43
|
-
| `android [--auto] [--script <name>] [--pm <name>] [--no-script] [--no-install]` | Same for Android |
|
|
49
|
+
| `ios [--auto] [--device-type <name>] [--runtime <ver>] [--script <name>] [--pm <name>] [--no-script] [--no-install] [-- <extras...>]` | Ensure iOS sim + Metro + build/install. Extras after `--` are forwarded to the build command. |
|
|
50
|
+
| `android [--auto] [--script <name>] [--pm <name>] [--no-script] [--no-install] [-- <extras...>]` | Same for Android. |
|
|
44
51
|
| `start` | Start Metro detached, no platform action |
|
|
45
52
|
| `stop [<port>\|<shortcut>\|<path>]` | Kill Metro. No arg = current project; pass a port (e.g. 8083), a project shortcut (label or unique basename), or an absolute path. |
|
|
46
53
|
| `device [--platform ios\|android] [--json]` | Print the assigned device target |
|
|
@@ -48,6 +55,7 @@ All commands below take the same `npx rn-iso` prefix.
|
|
|
48
55
|
| `reserve [ios\|android]` | Lock a manually-started sim/emulator to the current project (no build) |
|
|
49
56
|
| `unreserve [ios\|android]` | Drop the current project's lock without shutting the sim down |
|
|
50
57
|
| `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. |
|
|
58
|
+
| `config [<key> [<value>]] [--unset] [--project <target>]` | Get / set a per-project setting (`packageManager`, `ios.script`, `android.script`). |
|
|
51
59
|
|
|
52
60
|
## How it works
|
|
53
61
|
|
|
@@ -71,6 +79,25 @@ npx rn-iso unreserve # drop the lock without shutting the sim
|
|
|
71
79
|
|
|
72
80
|
Reserve binds the sim to the current project the same way `ios` / `android` would, but without running a build. If the sim is already held by another project, the picker prompts you to take it over.
|
|
73
81
|
|
|
82
|
+
## Per-project settings (`rn-iso config`)
|
|
83
|
+
|
|
84
|
+
A few options can be persisted per project so you don't have to repeat the same flags every run. Resolution order:
|
|
85
|
+
|
|
86
|
+
1. CLI flag (`--script`, `--pm`)
|
|
87
|
+
2. Stored project setting (this section)
|
|
88
|
+
3. Default inferred from the project (`ios` / `android` script if present, package manager from lockfile)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx rn-iso config packageManager bun
|
|
92
|
+
npx rn-iso config ios.script dev:ios
|
|
93
|
+
npx rn-iso config android.script "dev:android --variant=debug"
|
|
94
|
+
npx rn-iso config # list current project's settings
|
|
95
|
+
npx rn-iso config ios.script # print one
|
|
96
|
+
npx rn-iso config ios.script --unset
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Allowed keys today: `packageManager` (one of `npm|yarn|pnpm|bun`), `ios.script`, `android.script`. Settings live in `~/.rn-iso/config.json` under the project's entry.
|
|
100
|
+
|
|
74
101
|
## Project shortcuts (--label)
|
|
75
102
|
|
|
76
103
|
Each registered project has a "shortcut" you can pass to `stop` / `release` instead of the full path. The first time you run `ios`, `android`, or `reserve` interactively you'll be prompted for one (the directory basename is the default — hit enter to accept). Override any time with `--label <name>`:
|
package/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import statusCommand from '../src/commands/status.js';
|
|
|
10
10
|
import releaseCommand from '../src/commands/release.js';
|
|
11
11
|
import reserveCommand from '../src/commands/reserve.js';
|
|
12
12
|
import unreserveCommand from '../src/commands/unreserve.js';
|
|
13
|
+
import configCommand from '../src/commands/config.js';
|
|
13
14
|
|
|
14
15
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
15
16
|
|
|
@@ -28,5 +29,6 @@ statusCommand(program);
|
|
|
28
29
|
releaseCommand(program);
|
|
29
30
|
reserveCommand(program);
|
|
30
31
|
unreserveCommand(program);
|
|
32
|
+
configCommand(program);
|
|
31
33
|
|
|
32
34
|
program.parse();
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -30,6 +30,7 @@ From the project root (or any subdirectory):
|
|
|
30
30
|
## CRITICAL rules
|
|
31
31
|
|
|
32
32
|
- **Pass `--auto` for non-interactive use** of `ios` or `android`. Without it, the command will prompt with an arrow-key picker if multiple unclaimed sims/AVDs exist. `--auto` is also implied automatically when stdin isn't a TTY (e.g., when an agent pipes the command), so under most agent harnesses you don't have to remember the flag — but passing it explicitly is harmless and clearer.
|
|
33
|
+
- **Forward extra flags to the build CLI with `--`.** `npx rn-iso ios -- --variant=release` (or `android -- --mode=diaRelease`) appends those flags to the underlying `react-native run-*` / `expo run:*` invocation. Useful for release-mode builds, custom terminals, etc. Last-wins semantics, so extras can override defaults rn-iso set earlier in the command.
|
|
33
34
|
- **`--auto` will NOT take over a claimed sim/AVD.** If every device is claimed by other rn-iso projects, `--auto` errors. To take one over, run the command interactively (no `--auto`, with a real TTY) and confirm at the prompt — only do this if the user explicitly asks.
|
|
34
35
|
- **Always use `npx rn-iso device` to discover your target.** Never assume `booted` is your sim — another project's simulator might be booted too.
|
|
35
36
|
- **Always pass the UDID/serial explicitly** to `xcrun simctl` and `adb -s`. Examples:
|
|
@@ -83,6 +84,7 @@ Reserve binds the sim to the current project the same way `ios` does, but skips
|
|
|
83
84
|
- `npx rn-iso start` — start Metro detached on the project's assigned port WITHOUT building/installing. Useful to keep Metro alive across builds.
|
|
84
85
|
- `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
86
|
- `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.
|
|
87
|
+
- `npx rn-iso config [<key> [<value>]] [--unset] [--project <target>]` — persist per-project settings. Allowed keys: `packageManager` (npm|yarn|pnpm|bun), `ios.script`, `android.script`. Resolution order on `ios`/`android`: CLI flag > stored setting > inferred default. Useful when a project's build script is named differently (`dev:ios` instead of `ios`) or when a different package manager is used than the lockfile suggests.
|
|
86
88
|
|
|
87
89
|
### Project shortcuts (--label)
|
|
88
90
|
|
package/src/commands/android.js
CHANGED
|
@@ -20,14 +20,15 @@ import { resolveLabel } from '../labels.js';
|
|
|
20
20
|
export default function androidCommand(program) {
|
|
21
21
|
program
|
|
22
22
|
.command('android')
|
|
23
|
-
.description('Ensure a dedicated Android emulator + Metro for the current project; build/install if needed')
|
|
23
|
+
.description('Ensure a dedicated Android emulator + Metro for the current project; build/install if needed. Pass extra flags to the build CLI after `--`, e.g. `rn-iso android -- --mode=diaRelease`.')
|
|
24
|
+
.argument('[extras...]', 'Flags forwarded as-is to the underlying build command (after `--`)')
|
|
24
25
|
.option('--auto', 'Non-interactive: pick the first unclaimed AVD without prompting (also implied when stdin is not a TTY)')
|
|
25
26
|
.option('--label <name>', 'Optional shortcut name; refer to the project as <name> in stop / release / etc.')
|
|
26
|
-
.option('--script <name>', 'package.json script to invoke for build/install (default: android
|
|
27
|
+
.option('--script <name>', 'package.json script to invoke for build/install (default: project setting `android.script`, else `android`)')
|
|
27
28
|
.option('--no-script', 'Skip the package.json script lookup; run expo/react-native CLI directly')
|
|
28
|
-
.option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: detected from lockfile)')
|
|
29
|
+
.option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: project setting `packageManager`, else detected from lockfile)')
|
|
29
30
|
.option('--no-install', 'Skip the build/install step')
|
|
30
|
-
.action(async (opts) => {
|
|
31
|
+
.action(async (extras, opts) => {
|
|
31
32
|
const root = findProjectRoot(process.cwd());
|
|
32
33
|
if (!root) {
|
|
33
34
|
console.error(chalk.red('Not in a React Native project (no package.json found).'));
|
|
@@ -142,9 +143,12 @@ export default function androidCommand(program) {
|
|
|
142
143
|
console.log(chalk.dim(`adb reverse tcp:${proj.metroPort} configured for ${serial}`));
|
|
143
144
|
|
|
144
145
|
if (opts.install !== false) {
|
|
145
|
-
const
|
|
146
|
+
const settings = proj.settings || {};
|
|
147
|
+
const packageManager = opts.pm ?? settings.packageManager ?? detectPackageManager(root);
|
|
146
148
|
const useScript = opts.script !== false;
|
|
147
|
-
const scriptName = useScript
|
|
149
|
+
const scriptName = useScript
|
|
150
|
+
? (typeof opts.script === 'string' ? opts.script : (settings.android?.script ?? 'android'))
|
|
151
|
+
: null;
|
|
148
152
|
const cmd = buildAndroidCommand({
|
|
149
153
|
projectRoot: root,
|
|
150
154
|
packageManager,
|
|
@@ -154,6 +158,7 @@ export default function androidCommand(program) {
|
|
|
154
158
|
serial,
|
|
155
159
|
port: proj.metroPort,
|
|
156
160
|
useScript,
|
|
161
|
+
extras,
|
|
157
162
|
});
|
|
158
163
|
console.log(chalk.dim(`> ${cmd}`));
|
|
159
164
|
const exec = getExecutor();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/commands/config.js
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import {
|
|
4
|
+
findProjectRoot,
|
|
5
|
+
resolveRegisteredProject,
|
|
6
|
+
detectIsExpo,
|
|
7
|
+
detectBundleId,
|
|
8
|
+
detectAndroidPackage,
|
|
9
|
+
} from '../project.js';
|
|
10
|
+
import {
|
|
11
|
+
getProject,
|
|
12
|
+
upsertProject,
|
|
13
|
+
getProjectSettings,
|
|
14
|
+
getProjectSetting,
|
|
15
|
+
setProjectSetting,
|
|
16
|
+
unsetProjectSetting,
|
|
17
|
+
} from '../config.js';
|
|
18
|
+
|
|
19
|
+
const ALLOWED_KEYS = ['packageManager', 'ios.script', 'android.script'];
|
|
20
|
+
const PM_VALUES = ['npm', 'yarn', 'pnpm', 'bun'];
|
|
21
|
+
|
|
22
|
+
export default function configCommand(program) {
|
|
23
|
+
program
|
|
24
|
+
.command('config [key] [value]')
|
|
25
|
+
.description(
|
|
26
|
+
'Get or set a per-project setting. Allowed keys: ' + ALLOWED_KEYS.join(', ') +
|
|
27
|
+
'. With no args, lists current settings.'
|
|
28
|
+
)
|
|
29
|
+
.option('--unset', 'Remove the value for <key>')
|
|
30
|
+
.option('--project <target>', 'Run against another project (label / unique basename / absolute path) instead of cwd')
|
|
31
|
+
.action((key, value, opts) => {
|
|
32
|
+
const found = resolveTarget(opts.project);
|
|
33
|
+
|
|
34
|
+
if (!key) {
|
|
35
|
+
const settings = getProjectSettings(found);
|
|
36
|
+
if (Object.keys(settings).length === 0) {
|
|
37
|
+
console.log(chalk.dim(`No settings for ${found}.`));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(found);
|
|
41
|
+
for (const k of ALLOWED_KEYS) {
|
|
42
|
+
const v = getProjectSetting(found, k);
|
|
43
|
+
if (v !== undefined) console.log(` ${k} = ${chalk.cyan(v)}`);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!ALLOWED_KEYS.includes(key)) {
|
|
49
|
+
console.error(chalk.red(`Unknown key "${key}". Allowed: ${ALLOWED_KEYS.join(', ')}.`));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (opts.unset) {
|
|
54
|
+
const removed = unsetProjectSetting(found, key);
|
|
55
|
+
if (removed) console.log(chalk.green(`Unset ${key} for ${found}.`));
|
|
56
|
+
else console.log(chalk.dim(`${key} was already unset.`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (value === undefined) {
|
|
61
|
+
const cur = getProjectSetting(found, key);
|
|
62
|
+
if (cur === undefined) console.log(chalk.dim('(unset)'));
|
|
63
|
+
else console.log(cur);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (key === 'packageManager' && !PM_VALUES.includes(value)) {
|
|
68
|
+
console.error(chalk.red(`Invalid packageManager "${value}". Must be one of: ${PM_VALUES.join(', ')}.`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setProjectSetting(found, key, value);
|
|
73
|
+
console.log(chalk.green(`Set ${key} = ${value} for ${found}.`));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// `--project` targets an existing registered project; without it we fall
|
|
78
|
+
// back to the cwd and auto-register if needed (so users can configure a
|
|
79
|
+
// project before running `ios` / `android` for the first time).
|
|
80
|
+
function resolveTarget(projectArg) {
|
|
81
|
+
if (projectArg) {
|
|
82
|
+
const { found, error } = resolveRegisteredProject(projectArg);
|
|
83
|
+
if (!found) {
|
|
84
|
+
console.error(chalk.red(error));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
const root = findProjectRoot(process.cwd());
|
|
90
|
+
if (!root) {
|
|
91
|
+
console.error(chalk.red('Not in a React Native project (no package.json found).'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
if (!getProject(root)) {
|
|
95
|
+
upsertProject(root, {
|
|
96
|
+
bundleId: detectBundleId(root),
|
|
97
|
+
androidPackage: detectAndroidPackage(root),
|
|
98
|
+
isExpo: detectIsExpo(root),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return root;
|
|
102
|
+
}
|
package/src/commands/ios.js
CHANGED
|
@@ -12,16 +12,17 @@ import { resolveLabel } from '../labels.js';
|
|
|
12
12
|
export default function iosCommand(program) {
|
|
13
13
|
program
|
|
14
14
|
.command('ios')
|
|
15
|
-
.description('Ensure a dedicated iOS simulator + Metro server for the current project; build/install if needed')
|
|
15
|
+
.description('Ensure a dedicated iOS simulator + Metro server for the current project; build/install if needed. Pass extra flags to the build CLI after `--`, e.g. `rn-iso ios -- --variant=release`.')
|
|
16
|
+
.argument('[extras...]', 'Flags forwarded as-is to the underlying build command (after `--`)')
|
|
16
17
|
.option('--device-type <name>', 'Explicit opt-in: create a NEW sim of this device type (e.g. "iPhone 17 Pro")')
|
|
17
18
|
.option('--runtime <version>', 'iOS runtime version when creating a new sim (e.g. "26.2"); defaults to latest')
|
|
18
19
|
.option('--auto', 'Non-interactive: pick the first unclaimed sim without prompting (also implied when stdin is not a TTY)')
|
|
19
20
|
.option('--label <name>', 'Optional shortcut name; refer to the project as <name> in stop / release / etc.')
|
|
20
|
-
.option('--script <name>', 'package.json script to invoke for build/install (default: ios
|
|
21
|
+
.option('--script <name>', 'package.json script to invoke for build/install (default: project setting `ios.script`, else `ios`)')
|
|
21
22
|
.option('--no-script', 'Skip the package.json script lookup; run expo/react-native CLI directly')
|
|
22
|
-
.option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: detected from lockfile)')
|
|
23
|
+
.option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: project setting `packageManager`, else detected from lockfile)')
|
|
23
24
|
.option('--no-install', 'Skip the build/install step (assume app is already installed)')
|
|
24
|
-
.action(async (opts) => {
|
|
25
|
+
.action(async (extras, opts) => {
|
|
25
26
|
const root = findProjectRoot(process.cwd());
|
|
26
27
|
if (!root) {
|
|
27
28
|
console.error(chalk.red('Not in a React Native project (no package.json found).'));
|
|
@@ -144,9 +145,12 @@ export default function iosCommand(program) {
|
|
|
144
145
|
// (without build/install), use `rn-iso start`.
|
|
145
146
|
|
|
146
147
|
if (opts.install !== false) {
|
|
147
|
-
const
|
|
148
|
+
const settings = proj.settings || {};
|
|
149
|
+
const packageManager = opts.pm ?? settings.packageManager ?? detectPackageManager(root);
|
|
148
150
|
const useScript = opts.script !== false;
|
|
149
|
-
const scriptName = useScript
|
|
151
|
+
const scriptName = useScript
|
|
152
|
+
? (typeof opts.script === 'string' ? opts.script : (settings.ios?.script ?? 'ios'))
|
|
153
|
+
: null;
|
|
150
154
|
const cmd = buildIosCommand({
|
|
151
155
|
projectRoot: root,
|
|
152
156
|
packageManager,
|
|
@@ -155,6 +159,7 @@ export default function iosCommand(program) {
|
|
|
155
159
|
udid,
|
|
156
160
|
port: proj.metroPort,
|
|
157
161
|
useScript,
|
|
162
|
+
extras,
|
|
158
163
|
});
|
|
159
164
|
console.log(chalk.dim(`> ${cmd}`));
|
|
160
165
|
const exec = getExecutor();
|
package/src/config.js
CHANGED
|
@@ -88,6 +88,70 @@ export function clearDevice(projectPath, platform) {
|
|
|
88
88
|
saveConfig(cfg);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// --- Per-project settings (scripts, package manager, ...) ---
|
|
92
|
+
|
|
93
|
+
export function getProjectSettings(projectPath) {
|
|
94
|
+
return getProject(projectPath)?.settings || {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getProjectSetting(projectPath, dottedKey) {
|
|
98
|
+
return readNested(getProjectSettings(projectPath), dottedKey);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function setProjectSetting(projectPath, dottedKey, value) {
|
|
102
|
+
const cfg = ensureConfig();
|
|
103
|
+
const proj = cfg.projects[projectPath];
|
|
104
|
+
if (!proj) throw new Error(`Project not registered: ${projectPath}`);
|
|
105
|
+
proj.settings = proj.settings || {};
|
|
106
|
+
writeNested(proj.settings, dottedKey, value);
|
|
107
|
+
saveConfig(cfg);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function unsetProjectSetting(projectPath, dottedKey) {
|
|
111
|
+
const cfg = loadConfig();
|
|
112
|
+
const proj = cfg?.projects?.[projectPath];
|
|
113
|
+
if (!proj?.settings) return false;
|
|
114
|
+
const removed = deleteNested(proj.settings, dottedKey);
|
|
115
|
+
if (removed) saveConfig(cfg);
|
|
116
|
+
return removed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readNested(obj, dottedKey) {
|
|
120
|
+
if (!obj) return undefined;
|
|
121
|
+
const keys = dottedKey.split('.');
|
|
122
|
+
let cur = obj;
|
|
123
|
+
for (const k of keys) {
|
|
124
|
+
if (cur == null || typeof cur !== 'object') return undefined;
|
|
125
|
+
cur = cur[k];
|
|
126
|
+
}
|
|
127
|
+
return cur;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function writeNested(obj, dottedKey, value) {
|
|
131
|
+
const keys = dottedKey.split('.');
|
|
132
|
+
let cur = obj;
|
|
133
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
134
|
+
if (typeof cur[keys[i]] !== 'object' || cur[keys[i]] === null) {
|
|
135
|
+
cur[keys[i]] = {};
|
|
136
|
+
}
|
|
137
|
+
cur = cur[keys[i]];
|
|
138
|
+
}
|
|
139
|
+
cur[keys[keys.length - 1]] = value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function deleteNested(obj, dottedKey) {
|
|
143
|
+
const keys = dottedKey.split('.');
|
|
144
|
+
let cur = obj;
|
|
145
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
146
|
+
if (cur[keys[i]] == null || typeof cur[keys[i]] !== 'object') return false;
|
|
147
|
+
cur = cur[keys[i]];
|
|
148
|
+
}
|
|
149
|
+
const leaf = keys[keys.length - 1];
|
|
150
|
+
if (!(leaf in cur)) return false;
|
|
151
|
+
delete cur[leaf];
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
91
155
|
export function allMetroPorts() {
|
|
92
156
|
const cfg = loadConfig();
|
|
93
157
|
if (!cfg?.projects) return [];
|
package/src/runner.js
CHANGED
|
@@ -79,7 +79,10 @@ export function detectScriptCli(scriptBody) {
|
|
|
79
79
|
// iOS run command. Prefers the project's `ios` script if present (the most
|
|
80
80
|
// reliable: respects user customization, picks the right CLI). Falls back to
|
|
81
81
|
// expo run:ios / react-native run-ios when no script exists or --no-script.
|
|
82
|
-
|
|
82
|
+
// Any `extras` are appended last so they can override earlier flags (CLIs
|
|
83
|
+
// using commander/yargs are last-wins on repeated options).
|
|
84
|
+
export function buildIosCommand({ projectRoot, packageManager, scriptName, isExpo, udid, port, useScript = true, extras = [] }) {
|
|
85
|
+
const tail = (extras || []).map(shQuote);
|
|
83
86
|
if (useScript && scriptName) {
|
|
84
87
|
const script = getProjectScript(projectRoot, scriptName);
|
|
85
88
|
if (script) {
|
|
@@ -89,16 +92,19 @@ export function buildIosCommand({ projectRoot, packageManager, scriptName, isExp
|
|
|
89
92
|
return buildScriptCommand(packageManager, scriptName, [
|
|
90
93
|
deviceFlag,
|
|
91
94
|
`--port ${port}`,
|
|
95
|
+
...tail,
|
|
92
96
|
]);
|
|
93
97
|
}
|
|
94
98
|
}
|
|
99
|
+
const tailStr = tail.length ? ' ' + tail.join(' ') : '';
|
|
95
100
|
if (isExpo) {
|
|
96
|
-
return `npx expo run:ios --device ${udid} --port ${port}`;
|
|
101
|
+
return `npx expo run:ios --device ${udid} --port ${port}${tailStr}`;
|
|
97
102
|
}
|
|
98
|
-
return `npx react-native run-ios --udid ${udid} --port ${port}`;
|
|
103
|
+
return `npx react-native run-ios --udid ${udid} --port ${port}${tailStr}`;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
export function buildAndroidCommand({ projectRoot, packageManager, scriptName, isExpo, avdName, serial, port, useScript = true }) {
|
|
106
|
+
export function buildAndroidCommand({ projectRoot, packageManager, scriptName, isExpo, avdName, serial, port, useScript = true, extras = [] }) {
|
|
107
|
+
const tail = (extras || []).map(shQuote);
|
|
102
108
|
if (useScript && scriptName) {
|
|
103
109
|
const script = getProjectScript(projectRoot, scriptName);
|
|
104
110
|
if (script) {
|
|
@@ -108,13 +114,24 @@ export function buildAndroidCommand({ projectRoot, packageManager, scriptName, i
|
|
|
108
114
|
return buildScriptCommand(packageManager, scriptName, [
|
|
109
115
|
deviceFlag,
|
|
110
116
|
`--port ${port}`,
|
|
117
|
+
...tail,
|
|
111
118
|
]);
|
|
112
119
|
}
|
|
113
120
|
}
|
|
121
|
+
const tailStr = tail.length ? ' ' + tail.join(' ') : '';
|
|
114
122
|
if (isExpo) {
|
|
115
|
-
return `npx expo run:android --device "${avdName}" --port ${port}`;
|
|
123
|
+
return `npx expo run:android --device "${avdName}" --port ${port}${tailStr}`;
|
|
116
124
|
}
|
|
117
|
-
return `RCT_METRO_PORT=${port} npx react-native run-android --deviceId ${serial}`;
|
|
125
|
+
return `RCT_METRO_PORT=${port} npx react-native run-android --deviceId ${serial}${tailStr}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// POSIX-safe single-quote shell escape. Leaves "safe" tokens (alnum and a
|
|
129
|
+
// few harmless punctuation marks like `=`, `.`, `,`, `:`, `/`, `-`, `@`,
|
|
130
|
+
// `+`, `_`, `%`) alone, single-quotes everything else.
|
|
131
|
+
export function shQuote(s) {
|
|
132
|
+
if (s === '') return "''";
|
|
133
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(s)) return s;
|
|
134
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
118
135
|
}
|
|
119
136
|
|
|
120
137
|
export function buildMetroCommand({ isExpo, port }) {
|