rn-iso 0.3.3 → 0.4.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 CHANGED
@@ -48,6 +48,7 @@ All commands below take the same `npx rn-iso` prefix.
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
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
+ | `config [<key> [<value>]] [--unset] [--project <target>]` | Get / set a per-project setting (`packageManager`, `ios.script`, `android.script`). |
51
52
 
52
53
  ## How it works
53
54
 
@@ -71,6 +72,25 @@ npx rn-iso unreserve # drop the lock without shutting the sim
71
72
 
72
73
  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
74
 
75
+ ## Per-project settings (`rn-iso config`)
76
+
77
+ A few options can be persisted per project so you don't have to repeat the same flags every run. Resolution order:
78
+
79
+ 1. CLI flag (`--script`, `--pm`)
80
+ 2. Stored project setting (this section)
81
+ 3. Default inferred from the project (`ios` / `android` script if present, package manager from lockfile)
82
+
83
+ ```bash
84
+ npx rn-iso config packageManager bun
85
+ npx rn-iso config ios.script dev:ios
86
+ npx rn-iso config android.script "dev:android --variant=debug"
87
+ npx rn-iso config # list current project's settings
88
+ npx rn-iso config ios.script # print one
89
+ npx rn-iso config ios.script --unset
90
+ ```
91
+
92
+ 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.
93
+
74
94
  ## Project shortcuts (--label)
75
95
 
76
96
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-iso",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Isolated React Native dev environments per project/worktree",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -83,6 +83,7 @@ Reserve binds the sim to the current project the same way `ios` does, but skips
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
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
+ - `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
87
 
87
88
  ### Project shortcuts (--label)
88
89
 
@@ -23,9 +23,9 @@ export default function androidCommand(program) {
23
23
  .description('Ensure a dedicated Android emulator + Metro for the current project; build/install if needed')
24
24
  .option('--auto', 'Non-interactive: pick the first unclaimed AVD without prompting (also implied when stdin is not a TTY)')
25
25
  .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)', 'android')
26
+ .option('--script <name>', 'package.json script to invoke for build/install (default: project setting `android.script`, else `android`)')
27
27
  .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)')
28
+ .option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: project setting `packageManager`, else detected from lockfile)')
29
29
  .option('--no-install', 'Skip the build/install step')
30
30
  .action(async (opts) => {
31
31
  const root = findProjectRoot(process.cwd());
@@ -142,9 +142,12 @@ export default function androidCommand(program) {
142
142
  console.log(chalk.dim(`adb reverse tcp:${proj.metroPort} configured for ${serial}`));
143
143
 
144
144
  if (opts.install !== false) {
145
- const packageManager = opts.pm || detectPackageManager(root);
145
+ const settings = proj.settings || {};
146
+ const packageManager = opts.pm ?? settings.packageManager ?? detectPackageManager(root);
146
147
  const useScript = opts.script !== false;
147
- const scriptName = useScript ? (typeof opts.script === 'string' ? opts.script : 'android') : null;
148
+ const scriptName = useScript
149
+ ? (typeof opts.script === 'string' ? opts.script : (settings.android?.script ?? 'android'))
150
+ : null;
148
151
  const cmd = buildAndroidCommand({
149
152
  projectRoot: root,
150
153
  packageManager,
@@ -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
+ }
@@ -17,9 +17,9 @@ export default function iosCommand(program) {
17
17
  .option('--runtime <version>', 'iOS runtime version when creating a new sim (e.g. "26.2"); defaults to latest')
18
18
  .option('--auto', 'Non-interactive: pick the first unclaimed sim without prompting (also implied when stdin is not a TTY)')
19
19
  .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)', 'ios')
20
+ .option('--script <name>', 'package.json script to invoke for build/install (default: project setting `ios.script`, else `ios`)')
21
21
  .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)')
22
+ .option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: project setting `packageManager`, else detected from lockfile)')
23
23
  .option('--no-install', 'Skip the build/install step (assume app is already installed)')
24
24
  .action(async (opts) => {
25
25
  const root = findProjectRoot(process.cwd());
@@ -144,9 +144,12 @@ export default function iosCommand(program) {
144
144
  // (without build/install), use `rn-iso start`.
145
145
 
146
146
  if (opts.install !== false) {
147
- const packageManager = opts.pm || detectPackageManager(root);
147
+ const settings = proj.settings || {};
148
+ const packageManager = opts.pm ?? settings.packageManager ?? detectPackageManager(root);
148
149
  const useScript = opts.script !== false;
149
- const scriptName = useScript ? (typeof opts.script === 'string' ? opts.script : 'ios') : null;
150
+ const scriptName = useScript
151
+ ? (typeof opts.script === 'string' ? opts.script : (settings.ios?.script ?? 'ios'))
152
+ : null;
150
153
  const cmd = buildIosCommand({
151
154
  projectRoot: root,
152
155
  packageManager,
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 [];