@symbiote-native/cli 0.1.2 → 0.2.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/bundle.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  import { styleText } from "node:util";
3
4
  import process$1, { stdin, stdout } from "node:process";
4
5
  import l__default from "node:readline";
5
- import * as fs from "node:fs";
6
+ import * as fs$1 from "node:fs";
6
7
  import { readFileSync } from "node:fs";
7
- import * as path from "node:path";
8
+ import * as path$1 from "node:path";
8
9
  import { join } from "node:path";
9
10
  import { fileURLToPath } from "node:url";
11
+ import { execFileSync } from "node:child_process";
10
12
  //#region \0rolldown/runtime.js
11
13
  var __create = Object.create;
12
14
  var __defProp = Object.defineProperty;
@@ -29,6 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
31
  value: mod,
30
32
  enumerable: true
31
33
  }) : target, mod));
34
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
32
35
  //#endregion
33
36
  //#region src/errors.ts
34
37
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -119,6 +122,32 @@ const EXPO_PACKAGE_LAYERS = [
119
122
  label: "Application info",
120
123
  symbiotePackage: "@symbiote-native/application"
121
124
  },
125
+ {
126
+ id: "audio",
127
+ label: "Audio",
128
+ symbiotePackage: "@symbiote-native/audio",
129
+ optionalManifestBundles: [{
130
+ id: "recording",
131
+ label: "Background audio recording",
132
+ warning: "Requesting FOREGROUND_SERVICE_MICROPHONE triggers Play Console policy review — Google requires a clear, prominent in-app disclosure and justification before you can publish.",
133
+ nextSteps: "Pass `allowsBackgroundRecording: true` to setAudioModeAsync so recording keeps running with the app backgrounded.",
134
+ manifestPermissions: ["android.permission.FOREGROUND_SERVICE_MICROPHONE", "android.permission.POST_NOTIFICATIONS"],
135
+ manifestServices: [{
136
+ name: "expo.modules.audio.service.AudioRecordingService",
137
+ foregroundServiceType: "microphone"
138
+ }]
139
+ }]
140
+ },
141
+ {
142
+ id: "background-fetch",
143
+ label: "Background fetch",
144
+ symbiotePackage: "@symbiote-native/background-fetch"
145
+ },
146
+ {
147
+ id: "background-task",
148
+ label: "Background task",
149
+ symbiotePackage: "@symbiote-native/background-task"
150
+ },
122
151
  {
123
152
  id: "battery",
124
153
  label: "Battery",
@@ -149,6 +178,11 @@ const EXPO_PACKAGE_LAYERS = [
149
178
  label: "Device info",
150
179
  symbiotePackage: "@symbiote-native/device"
151
180
  },
181
+ {
182
+ id: "file-system",
183
+ label: "File system",
184
+ symbiotePackage: "@symbiote-native/file-system"
185
+ },
152
186
  {
153
187
  id: "haptics",
154
188
  label: "Haptics",
@@ -169,11 +203,37 @@ const EXPO_PACKAGE_LAYERS = [
169
203
  label: "Localization",
170
204
  symbiotePackage: "@symbiote-native/localization"
171
205
  },
206
+ {
207
+ id: "location",
208
+ label: "Location",
209
+ symbiotePackage: "@symbiote-native/location",
210
+ optionalManifestBundles: [{
211
+ id: "background",
212
+ label: "Background location tracking",
213
+ warning: "Requesting ACCESS_BACKGROUND_LOCATION and the location foreground-service permissions triggers Play Console policy review — Google requires a clear, prominent in-app disclosure and justification before you can publish.",
214
+ nextSteps: "Pass a `foregroundService` option to `startLocationUpdatesAsync` (notification title/body) so the OS keeps tracking alive outside the app. expo-location's own LocationTaskService is already declared in its AndroidManifest.xml and merges automatically — this only adds the permissions it needs.",
215
+ manifestPermissions: [
216
+ "android.permission.ACCESS_BACKGROUND_LOCATION",
217
+ "android.permission.FOREGROUND_SERVICE",
218
+ "android.permission.FOREGROUND_SERVICE_LOCATION"
219
+ ]
220
+ }]
221
+ },
222
+ {
223
+ id: "media-library",
224
+ label: "Media library",
225
+ symbiotePackage: "@symbiote-native/media-library"
226
+ },
172
227
  {
173
228
  id: "network",
174
229
  label: "Network info",
175
230
  symbiotePackage: "@symbiote-native/network"
176
231
  },
232
+ {
233
+ id: "notifications",
234
+ label: "Notifications",
235
+ symbiotePackage: "@symbiote-native/notifications"
236
+ },
177
237
  {
178
238
  id: "screen-orientation",
179
239
  label: "Screen orientation",
@@ -199,6 +259,11 @@ const EXPO_PACKAGE_LAYERS = [
199
259
  label: "SMS",
200
260
  symbiotePackage: "@symbiote-native/sms"
201
261
  },
262
+ {
263
+ id: "sqlite",
264
+ label: "SQLite",
265
+ symbiotePackage: "@symbiote-native/sqlite"
266
+ },
202
267
  {
203
268
  id: "standard-web-crypto",
204
269
  label: "Web Crypto polyfill",
@@ -214,6 +279,11 @@ const EXPO_PACKAGE_LAYERS = [
214
279
  label: "System UI",
215
280
  symbiotePackage: "@symbiote-native/system-ui"
216
281
  },
282
+ {
283
+ id: "task-manager",
284
+ label: "Task manager",
285
+ symbiotePackage: "@symbiote-native/task-manager"
286
+ },
217
287
  {
218
288
  id: "tracking-transparency",
219
289
  label: "Tracking transparency",
@@ -229,6 +299,10 @@ const EXPO_PACKAGE_LAYER_IDS = new Set(EXPO_PACKAGE_LAYERS.map((layer) => layer.
229
299
  function isExpoPackageLayerName(value) {
230
300
  return EXPO_PACKAGE_LAYER_IDS.has(value);
231
301
  }
302
+ function expoPackagesWithOptionalManifestBundles(selected) {
303
+ const ids = selected instanceof Set ? selected : new Set(selected);
304
+ return EXPO_PACKAGE_LAYERS.filter((layer) => (layer.optionalManifestBundles?.length ?? 0) > 0 && ids.has(layer.id));
305
+ }
232
306
  //#endregion
233
307
  //#region src/utils/suggest.ts
234
308
  function editDistance(a, b) {
@@ -336,7 +410,19 @@ function parseArgv(argv) {
336
410
  const [command, ...rest] = argv;
337
411
  if (command === void 0 || command === "--help" || command === "-h") return { kind: "help" };
338
412
  if (command === "--version" || command === "-v") return { kind: "version" };
339
- if (command !== "new" && command !== "add") throw usageErrorFor("command", command, ["new", "add"]);
413
+ if (command === "grant") {
414
+ const [layer, ...extra] = rest;
415
+ if (extra.length > 0) throw new CliUsageError(`"grant" takes at most one layer — got "${layer}" and "${extra[0]}".`);
416
+ return {
417
+ kind: "grant",
418
+ layer
419
+ };
420
+ }
421
+ if (command !== "new" && command !== "add") throw usageErrorFor("command", command, [
422
+ "new",
423
+ "add",
424
+ "grant"
425
+ ]);
340
426
  let appName;
341
427
  let framework;
342
428
  let vueFlavor;
@@ -744,7 +830,7 @@ function wrapAnsi(string, columns, options) {
744
830
  }
745
831
  //#endregion
746
832
  //#region ../../node_modules/.pnpm/@clack+core@1.4.3/node_modules/@clack/core/dist/index.mjs
747
- var import_src = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
833
+ var import_src$1 = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
748
834
  const ESC = "\x1B";
749
835
  const CSI = `${ESC}[`;
750
836
  const beep = "\x07";
@@ -973,9 +1059,9 @@ var V = class {
973
1059
  escapeCodeTimeout: 50,
974
1060
  terminal: true
975
1061
  }), this.rl.prompt(), this.opts.initialUserInput !== void 0 && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), setRawMode(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
976
- this.output.write(import_src.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(this.value);
1062
+ this.output.write(import_src$1.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(this.value);
977
1063
  }), this.once("cancel", () => {
978
- this.output.write(import_src.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(CANCEL_SYMBOL);
1064
+ this.output.write(import_src$1.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(CANCEL_SYMBOL);
979
1065
  });
980
1066
  });
981
1067
  }
@@ -1024,7 +1110,7 @@ var V = class {
1024
1110
  trim: false
1025
1111
  }).split(`
1026
1112
  `).length - 1;
1027
- this.output.write(import_src.cursor.move(-999, t * -1));
1113
+ this.output.write(import_src$1.cursor.move(-999, t * -1));
1028
1114
  }
1029
1115
  render() {
1030
1116
  const t = wrapAnsi(this._render(this) ?? "", process.stdout.columns, {
@@ -1032,7 +1118,7 @@ var V = class {
1032
1118
  trim: false
1033
1119
  });
1034
1120
  if (t !== this._prevFrame) {
1035
- if (this.state === "initial") this.output.write(import_src.cursor.hide);
1121
+ if (this.state === "initial") this.output.write(import_src$1.cursor.hide);
1036
1122
  else {
1037
1123
  const e = diffLines(this._prevFrame, t), i = getRows(this.output);
1038
1124
  if (this.restoreCursor(), e) {
@@ -1043,18 +1129,18 @@ var V = class {
1043
1129
  return;
1044
1130
  }
1045
1131
  if (e.lines.length === 1) {
1046
- this.output.write(import_src.cursor.move(0, r - s)), this.output.write(import_src.erase.lines(1));
1132
+ this.output.write(import_src$1.cursor.move(0, r - s)), this.output.write(import_src$1.erase.lines(1));
1047
1133
  const o = t.split(`
1048
1134
  `);
1049
- this.output.write(o[r]), this._prevFrame = t, this.output.write(import_src.cursor.move(0, o.length - r - 1));
1135
+ this.output.write(o[r]), this._prevFrame = t, this.output.write(import_src$1.cursor.move(0, o.length - r - 1));
1050
1136
  return;
1051
1137
  } else if (e.lines.length > 1) {
1052
1138
  if (n < s) r = n;
1053
1139
  else {
1054
1140
  const h = r - s;
1055
- h > 0 && this.output.write(import_src.cursor.move(0, h));
1141
+ h > 0 && this.output.write(import_src$1.cursor.move(0, h));
1056
1142
  }
1057
- this.output.write(import_src.erase.down());
1143
+ this.output.write(import_src$1.erase.down());
1058
1144
  const f = t.split(`
1059
1145
  `).slice(r);
1060
1146
  this.output.write(f.join(`
@@ -1062,7 +1148,7 @@ var V = class {
1062
1148
  return;
1063
1149
  }
1064
1150
  }
1065
- this.output.write(import_src.erase.down());
1151
+ this.output.write(import_src$1.erase.down());
1066
1152
  }
1067
1153
  this.output.write(t), this.state === "initial" && (this.state = "active"), this._prevFrame = t;
1068
1154
  }
@@ -1079,7 +1165,7 @@ var r = class extends V {
1079
1165
  super(t, false), this.value = !!t.initialValue, this.on("userInput", () => {
1080
1166
  this.value = this._value;
1081
1167
  }), this.on("confirm", (i) => {
1082
- this.output.write(import_src.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
1168
+ this.output.write(import_src$1.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
1083
1169
  }), this.on("cursor", () => {
1084
1170
  this.value = !this.value;
1085
1171
  });
@@ -1255,6 +1341,11 @@ const S_RADIO_INACTIVE = unicodeOr("○", " ");
1255
1341
  const S_CHECKBOX_ACTIVE = unicodeOr("◻", "[•]");
1256
1342
  const S_CHECKBOX_SELECTED = unicodeOr("◼", "[+]");
1257
1343
  const S_CHECKBOX_INACTIVE = unicodeOr("◻", "[ ]");
1344
+ const S_BAR_H = unicodeOr("─", "-");
1345
+ const S_CORNER_TOP_RIGHT = unicodeOr("╮", "+");
1346
+ const S_CONNECT_LEFT = unicodeOr("├", "+");
1347
+ const S_CORNER_BOTTOM_RIGHT = unicodeOr("╯", "+");
1348
+ const S_CORNER_BOTTOM_LEFT = unicodeOr("╰", "+");
1258
1349
  const symbol = (o) => {
1259
1350
  switch (o) {
1260
1351
  case "initial":
@@ -1548,6 +1639,32 @@ const outro = (o = "", t) => {
1548
1639
  ${styleText("gray", S_BAR_END)} ` : "";
1549
1640
  i.write(`${e}${o}
1550
1641
 
1642
+ `);
1643
+ };
1644
+ const W$1 = (o) => o;
1645
+ const C = (o, e, s) => {
1646
+ const a = {
1647
+ hard: true,
1648
+ trim: false
1649
+ }, i = wrapAnsi(o, e, a).split(`
1650
+ `), c = i.reduce((n, t) => Math.max(fastStringWidth(t), n), 0);
1651
+ return wrapAnsi(o, e - (i.map(s).reduce((n, t) => Math.max(fastStringWidth(t), n), 0) - c), a);
1652
+ };
1653
+ const note = (o = "", e = "", s) => {
1654
+ const a = s?.output ?? process$1.stdout, i = s?.withGuide ?? settings.withGuide, c = s?.format ?? W$1, g = [
1655
+ "",
1656
+ ...C(o, getColumns(a) - 6, c).split(`
1657
+ `).map(c),
1658
+ ""
1659
+ ], n = fastStringWidth(e), t = Math.max(g.reduce((m, F) => {
1660
+ const O = fastStringWidth(F);
1661
+ return O > m ? O : m;
1662
+ }, 0), n) + 2, h = g.map((m) => `${styleText("gray", S_BAR)} ${m}${" ".repeat(t - fastStringWidth(m))}${styleText("gray", S_BAR)}`).join(`
1663
+ `), T = i ? `${styleText("gray", S_BAR)}
1664
+ ` : "", l$1 = i ? S_CONNECT_LEFT : S_CORNER_BOTTOM_LEFT;
1665
+ a.write(`${T}${styleText("green", S_STEP_SUBMIT)} ${styleText("reset", e)} ${styleText("gray", S_BAR_H.repeat(Math.max(t - n - 1, 1)) + S_CORNER_TOP_RIGHT)}
1666
+ ${h}
1667
+ ${styleText("gray", l$1 + S_BAR_H.repeat(t + 2) + S_CORNER_BOTTOM_RIGHT)}
1551
1668
  `);
1552
1669
  };
1553
1670
  const SELECT_INSTRUCTIONS = [`${styleText("dim", "↑/↓")} to navigate`, `${styleText("dim", "Enter:")} confirm`];
@@ -1668,44 +1785,44 @@ const TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
1668
1785
  ""
1669
1786
  ]);
1670
1787
  function replaceTextContent(filePath, identity) {
1671
- const content = fs.readFileSync(filePath, "utf8");
1788
+ const content = fs$1.readFileSync(filePath, "utf8");
1672
1789
  const replaced = content.split("com.canary").join(identity.bundleId).split("Canary").join(identity.nativeAppName);
1673
- if (replaced !== content) fs.writeFileSync(filePath, replaced);
1790
+ if (replaced !== content) fs$1.writeFileSync(filePath, replaced);
1674
1791
  }
1675
1792
  function walkTextFiles(dir, identity) {
1676
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1677
- const entryPath = path.join(dir, entry.name);
1793
+ for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
1794
+ const entryPath = path$1.join(dir, entry.name);
1678
1795
  if (entry.isDirectory()) {
1679
1796
  walkTextFiles(entryPath, identity);
1680
1797
  continue;
1681
1798
  }
1682
- if (TEXT_FILE_EXTENSIONS.has(path.extname(entry.name))) replaceTextContent(entryPath, identity);
1799
+ if (TEXT_FILE_EXTENSIONS.has(path$1.extname(entry.name))) replaceTextContent(entryPath, identity);
1683
1800
  }
1684
1801
  }
1685
1802
  function renameCanaryPaths(dir, nativeAppName) {
1686
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) renameCanaryPaths(path.join(dir, entry.name), nativeAppName);
1687
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1803
+ for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) renameCanaryPaths(path$1.join(dir, entry.name), nativeAppName);
1804
+ for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
1688
1805
  if (!entry.name.startsWith("Canary")) continue;
1689
- const from = path.join(dir, entry.name);
1690
- const to = path.join(dir, entry.name.replace("Canary", nativeAppName));
1691
- fs.renameSync(from, to);
1806
+ const from = path$1.join(dir, entry.name);
1807
+ const to = path$1.join(dir, entry.name.replace("Canary", nativeAppName));
1808
+ fs$1.renameSync(from, to);
1692
1809
  }
1693
1810
  }
1694
1811
  function moveAndroidPackageDir(javaSrcDir, bundleId) {
1695
- const oldDir = path.join(javaSrcDir, "com", "canary");
1696
- if (!fs.existsSync(oldDir)) return;
1697
- const newDir = path.join(javaSrcDir, ...bundleId.split("."));
1698
- fs.mkdirSync(path.dirname(newDir), { recursive: true });
1699
- fs.renameSync(oldDir, newDir);
1700
- const oldTopDir = path.join(javaSrcDir, "com");
1701
- if (fs.existsSync(oldTopDir) && fs.readdirSync(oldTopDir).length === 0) fs.rmdirSync(oldTopDir);
1812
+ const oldDir = path$1.join(javaSrcDir, "com", "canary");
1813
+ if (!fs$1.existsSync(oldDir)) return;
1814
+ const newDir = path$1.join(javaSrcDir, ...bundleId.split("."));
1815
+ fs$1.mkdirSync(path$1.dirname(newDir), { recursive: true });
1816
+ fs$1.renameSync(oldDir, newDir);
1817
+ const oldTopDir = path$1.join(javaSrcDir, "com");
1818
+ if (fs$1.existsSync(oldTopDir) && fs$1.readdirSync(oldTopDir).length === 0) fs$1.rmdirSync(oldTopDir);
1702
1819
  }
1703
1820
  function applyAppIdentity(root, identity) {
1704
1821
  walkTextFiles(root, identity);
1705
- const iosDir = path.join(root, "ios");
1706
- if (fs.existsSync(iosDir)) renameCanaryPaths(iosDir, identity.nativeAppName);
1707
- const androidAppSrc = path.join(root, "android", "app", "src");
1708
- if (fs.existsSync(androidAppSrc)) for (const sourceSet of fs.readdirSync(androidAppSrc)) moveAndroidPackageDir(path.join(androidAppSrc, sourceSet, "java"), identity.bundleId);
1822
+ const iosDir = path$1.join(root, "ios");
1823
+ if (fs$1.existsSync(iosDir)) renameCanaryPaths(iosDir, identity.nativeAppName);
1824
+ const androidAppSrc = path$1.join(root, "android", "app", "src");
1825
+ if (fs$1.existsSync(androidAppSrc)) for (const sourceSet of fs$1.readdirSync(androidAppSrc)) moveAndroidPackageDir(path$1.join(androidAppSrc, sourceSet, "java"), identity.bundleId);
1709
1826
  }
1710
1827
  //#endregion
1711
1828
  //#region src/utils/json.ts
@@ -1752,7 +1869,7 @@ function sortDependencies(packageJson) {
1752
1869
  //#endregion
1753
1870
  //#region src/utils/render-template.ts
1754
1871
  function readJsonObject(filePath) {
1755
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
1872
+ const parsed = JSON.parse(fs$1.readFileSync(filePath, "utf8"));
1756
1873
  if (!isJsonObject(parsed)) throw new Error(`${filePath}: expected a JSON object`);
1757
1874
  return parsed;
1758
1875
  }
@@ -1782,37 +1899,37 @@ function stripTypescriptInfix(filename) {
1782
1899
  }
1783
1900
  const DEFAULT_OPTIONS = { hasTypescript: true };
1784
1901
  function renderTemplate(src, dest, options = DEFAULT_OPTIONS) {
1785
- if (fs.statSync(src).isDirectory()) {
1786
- fs.mkdirSync(dest, { recursive: true });
1787
- for (const entry of fs.readdirSync(src)) renderTemplate(path.resolve(src, entry), path.resolve(dest, entry), options);
1902
+ if (fs$1.statSync(src).isDirectory()) {
1903
+ fs$1.mkdirSync(dest, { recursive: true });
1904
+ for (const entry of fs$1.readdirSync(src)) renderTemplate(path$1.resolve(src, entry), path$1.resolve(dest, entry), options);
1788
1905
  return;
1789
1906
  }
1790
- const filename = path.basename(src);
1907
+ const filename = path$1.basename(src);
1791
1908
  if (filename === "README.md") return;
1792
1909
  if (!options.hasTypescript && (isTypescriptOnlyFile(filename) || filename.includes(".typescript."))) return;
1793
1910
  if (options.hasTypescript && isJavascriptOnlyFile(filename)) return;
1794
1911
  const destFilename = filename.includes(".typescript.") ? stripTypescriptInfix(filename) : filename;
1795
- const resolvedDest = path.join(path.dirname(dest), destFilename);
1912
+ const resolvedDest = path$1.join(path$1.dirname(dest), destFilename);
1796
1913
  if (destFilename === "package.json.fragment.json") {
1797
- const destPackageJsonPath = path.resolve(path.dirname(resolvedDest), "package.json");
1914
+ const destPackageJsonPath = path$1.resolve(path$1.dirname(resolvedDest), "package.json");
1798
1915
  const fragment = readJsonObject(src);
1799
- const merged = sortDependencies(deepMerge(fs.existsSync(destPackageJsonPath) ? readJsonObject(destPackageJsonPath) : {}, fragment));
1800
- fs.writeFileSync(destPackageJsonPath, `${JSON.stringify(merged, null, 2)}\n`);
1916
+ const merged = sortDependencies(deepMerge(fs$1.existsSync(destPackageJsonPath) ? readJsonObject(destPackageJsonPath) : {}, fragment));
1917
+ fs$1.writeFileSync(destPackageJsonPath, `${JSON.stringify(merged, null, 2)}\n`);
1801
1918
  return;
1802
1919
  }
1803
- if (destFilename === ".gitignore" && fs.existsSync(resolvedDest)) {
1804
- const existing = fs.readFileSync(resolvedDest, "utf8");
1805
- const addition = fs.readFileSync(src, "utf8");
1806
- fs.writeFileSync(resolvedDest, `${existing}\n${addition}`);
1920
+ if (destFilename === ".gitignore" && fs$1.existsSync(resolvedDest)) {
1921
+ const existing = fs$1.readFileSync(resolvedDest, "utf8");
1922
+ const addition = fs$1.readFileSync(src, "utf8");
1923
+ fs$1.writeFileSync(resolvedDest, `${existing}\n${addition}`);
1807
1924
  return;
1808
1925
  }
1809
- fs.copyFileSync(src, resolvedDest);
1926
+ fs$1.copyFileSync(src, resolvedDest);
1810
1927
  }
1811
1928
  //#endregion
1812
1929
  //#region src/utils/apply-bootsplash-logo.ts
1813
1930
  function applyBootsplashLogo(root, templatesRoot, framework) {
1814
- const overlayRoot = path.join(templatesRoot, "bootsplash-logo", framework);
1815
- if (fs.existsSync(overlayRoot)) renderTemplate(overlayRoot, root);
1931
+ const overlayRoot = path$1.join(templatesRoot, "bootsplash-logo", framework);
1932
+ if (fs$1.existsSync(overlayRoot)) renderTemplate(overlayRoot, root);
1816
1933
  }
1817
1934
  //#endregion
1818
1935
  //#region src/utils/apply-expo-modules-manifest.ts
@@ -1827,54 +1944,54 @@ const EXPO_MODULES_BACKUP_ATTRS = `android:supportsRtl="true"
1827
1944
  android:dataExtractionRules="@xml/secure_store_data_extraction_rules"
1828
1945
  android:fullBackupContent="@xml/secure_store_backup_rules">`;
1829
1946
  function applyExpoModulesAndroidManifest(root) {
1830
- const manifestPath = path.join(root, "android/app/src/main/AndroidManifest.xml");
1831
- const manifest = fs.readFileSync(manifestPath, "utf8");
1947
+ const manifestPath = path$1.join(root, "android/app/src/main/AndroidManifest.xml");
1948
+ const manifest = fs$1.readFileSync(manifestPath, "utf8");
1832
1949
  const withPermissions = manifest.includes("USE_BIOMETRIC") ? manifest : manifest.replace(" <uses-permission android:name=\"android.permission.VIBRATE\" />", EXPO_MODULES_PERMISSIONS);
1833
1950
  const withBackupAttrs = withPermissions.includes("dataExtractionRules") ? withPermissions : withPermissions.replace("android:supportsRtl=\"true\">", EXPO_MODULES_BACKUP_ATTRS);
1834
- fs.writeFileSync(manifestPath, withBackupAttrs);
1951
+ fs$1.writeFileSync(manifestPath, withBackupAttrs);
1835
1952
  }
1836
1953
  //#endregion
1837
1954
  //#region src/utils/apply-expo-modules-podfile-autolinking.ts
1838
1955
  const AUTOLINKING_REQUIRE = "require File.join(File.dirname(`node --print \"require.resolve('expo/package.json')\"`), \"scripts/autolinking\")";
1839
1956
  const NATIVE_MODULES_LINE = "config = use_native_modules!";
1840
1957
  function applyExpoModulesPodfileAutolinking(root) {
1841
- const podfilePath = path.join(root, "ios", "Podfile");
1842
- if (!fs.existsSync(podfilePath)) return;
1843
- let podfile = fs.readFileSync(podfilePath, "utf8");
1958
+ const podfilePath = path$1.join(root, "ios", "Podfile");
1959
+ if (!fs$1.existsSync(podfilePath)) return;
1960
+ let podfile = fs$1.readFileSync(podfilePath, "utf8");
1844
1961
  if (podfile.includes("use_expo_modules!")) return;
1845
1962
  if (!podfile.includes(AUTOLINKING_REQUIRE)) podfile = `${AUTOLINKING_REQUIRE}\n\n${podfile}`;
1846
1963
  podfile = podfile.replace(NATIVE_MODULES_LINE, `${NATIVE_MODULES_LINE}\n\n use_expo_modules!`);
1847
- fs.writeFileSync(podfilePath, podfile);
1964
+ fs$1.writeFileSync(podfilePath, podfile);
1848
1965
  }
1849
1966
  //#endregion
1850
1967
  //#region src/utils/apply-expo-modules-podfile.ts
1851
1968
  const DEFAULT_PLATFORM_LINE = "platform :ios, min_ios_version_supported.to_s";
1852
1969
  const RAISED_PLATFORM_LINE = "platform :ios, [min_ios_version_supported.to_f, 16.4].max.to_s";
1853
1970
  function applyExpoModulesPodfilePlatform(root) {
1854
- const podfilePath = path.join(root, "ios", "Podfile");
1855
- if (!fs.existsSync(podfilePath)) return;
1856
- const podfile = fs.readFileSync(podfilePath, "utf8");
1971
+ const podfilePath = path$1.join(root, "ios", "Podfile");
1972
+ if (!fs$1.existsSync(podfilePath)) return;
1973
+ const podfile = fs$1.readFileSync(podfilePath, "utf8");
1857
1974
  if (!podfile.includes(DEFAULT_PLATFORM_LINE)) return;
1858
- fs.writeFileSync(podfilePath, podfile.replace(DEFAULT_PLATFORM_LINE, RAISED_PLATFORM_LINE));
1975
+ fs$1.writeFileSync(podfilePath, podfile.replace(DEFAULT_PLATFORM_LINE, RAISED_PLATFORM_LINE));
1859
1976
  }
1860
1977
  //#endregion
1861
1978
  //#region src/utils/apply-expo-modules-xcode-deployment-target.ts
1862
1979
  const MIN_EXPO_IOS_VERSION = 16.4;
1863
1980
  const DEPLOYMENT_TARGET_LINE = /IPHONEOS_DEPLOYMENT_TARGET = (\d+(?:\.\d+)?);/g;
1864
1981
  function findPbxprojPath(root) {
1865
- const iosDir = path.join(root, "ios");
1866
- if (!fs.existsSync(iosDir)) return void 0;
1867
- const xcodeproj = fs.readdirSync(iosDir).find((entry) => entry.endsWith(".xcodeproj"));
1982
+ const iosDir = path$1.join(root, "ios");
1983
+ if (!fs$1.existsSync(iosDir)) return void 0;
1984
+ const xcodeproj = fs$1.readdirSync(iosDir).find((entry) => entry.endsWith(".xcodeproj"));
1868
1985
  if (!xcodeproj) return void 0;
1869
- const pbxprojPath = path.join(iosDir, xcodeproj, "project.pbxproj");
1870
- return fs.existsSync(pbxprojPath) ? pbxprojPath : void 0;
1986
+ const pbxprojPath = path$1.join(iosDir, xcodeproj, "project.pbxproj");
1987
+ return fs$1.existsSync(pbxprojPath) ? pbxprojPath : void 0;
1871
1988
  }
1872
1989
  function applyExpoModulesXcodeDeploymentTarget(root) {
1873
1990
  const pbxprojPath = findPbxprojPath(root);
1874
1991
  if (!pbxprojPath) return;
1875
- const pbxproj = fs.readFileSync(pbxprojPath, "utf8");
1992
+ const pbxproj = fs$1.readFileSync(pbxprojPath, "utf8");
1876
1993
  const raised = pbxproj.replace(DEPLOYMENT_TARGET_LINE, (line, version) => Number.parseFloat(version) < MIN_EXPO_IOS_VERSION ? `IPHONEOS_DEPLOYMENT_TARGET = ${MIN_EXPO_IOS_VERSION};` : line);
1877
- if (raised !== pbxproj) fs.writeFileSync(pbxprojPath, raised);
1994
+ if (raised !== pbxproj) fs$1.writeFileSync(pbxprojPath, raised);
1878
1995
  }
1879
1996
  //#endregion
1880
1997
  //#region src/utils/apply-splash-screen-hide.ts
@@ -1891,8 +2008,8 @@ const APP_COMPONENT_FILENAMES = /* @__PURE__ */ new Set([
1891
2008
  "App.svelte"
1892
2009
  ]);
1893
2010
  function findAppComponentFile(dir) {
1894
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1895
- const entryPath = path.join(dir, entry.name);
2011
+ for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
2012
+ const entryPath = path$1.join(dir, entry.name);
1896
2013
  if (entry.isDirectory()) {
1897
2014
  if (SKIPPED_DIR_NAMES$1.has(entry.name)) continue;
1898
2015
  const found = findAppComponentFile(entryPath);
@@ -1938,9 +2055,9 @@ function patchSvelte(source) {
1938
2055
  function applySplashScreenAppHide(root, options) {
1939
2056
  const appFilePath = findAppComponentFile(root);
1940
2057
  if (appFilePath === void 0) return;
1941
- const source = fs.readFileSync(appFilePath, "utf8");
2058
+ const source = fs$1.readFileSync(appFilePath, "utf8");
1942
2059
  if (/\bhide\(\)/.test(source)) return;
1943
- fs.writeFileSync(appFilePath, patch(source, options.framework, appFilePath));
2060
+ fs$1.writeFileSync(appFilePath, patch(source, options.framework, appFilePath));
1944
2061
  }
1945
2062
  function patch(source, framework, appFilePath) {
1946
2063
  switch (framework) {
@@ -1956,16 +2073,16 @@ function patch(source, framework, appFilePath) {
1956
2073
  const INLINE_STYLE_BLOCK_FRAMEWORKS = /* @__PURE__ */ new Set(["vue-sfc", "svelte"]);
1957
2074
  const STYLE_TAG_PATTERN = /<style(?=[ >])/;
1958
2075
  function addStyleLangAttribute(dir, lang) {
1959
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1960
- const entryPath = path.join(dir, entry.name);
2076
+ for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
2077
+ const entryPath = path$1.join(dir, entry.name);
1961
2078
  if (entry.isDirectory()) {
1962
2079
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
1963
2080
  addStyleLangAttribute(entryPath, lang);
1964
2081
  continue;
1965
2082
  }
1966
2083
  if (!entry.name.endsWith(".vue") && !entry.name.endsWith(".svelte")) continue;
1967
- const content = fs.readFileSync(entryPath, "utf8");
1968
- if (STYLE_TAG_PATTERN.test(content)) fs.writeFileSync(entryPath, content.replace(STYLE_TAG_PATTERN, `<style lang="${lang}"`));
2084
+ const content = fs$1.readFileSync(entryPath, "utf8");
2085
+ if (STYLE_TAG_PATTERN.test(content)) fs$1.writeFileSync(entryPath, content.replace(STYLE_TAG_PATTERN, `<style lang="${lang}"`));
1969
2086
  }
1970
2087
  }
1971
2088
  const PREPROCESSOR_EXTENSIONS = {
@@ -1979,42 +2096,42 @@ const SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
1979
2096
  "node_modules"
1980
2097
  ]);
1981
2098
  function findAppCssFile(dir) {
1982
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2099
+ for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
1983
2100
  if (entry.isDirectory()) {
1984
2101
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
1985
- const found = findAppCssFile(path.join(dir, entry.name));
2102
+ const found = findAppCssFile(path$1.join(dir, entry.name));
1986
2103
  if (found !== void 0) return found;
1987
2104
  continue;
1988
2105
  }
1989
- if (entry.name === "App.css") return path.join(dir, entry.name);
2106
+ if (entry.name === "App.css") return path$1.join(dir, entry.name);
1990
2107
  }
1991
2108
  }
1992
2109
  function replaceInTextFiles(dir, from, to) {
1993
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1994
- const entryPath = path.join(dir, entry.name);
2110
+ for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
2111
+ const entryPath = path$1.join(dir, entry.name);
1995
2112
  if (entry.isDirectory()) {
1996
2113
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
1997
2114
  replaceInTextFiles(entryPath, from, to);
1998
2115
  continue;
1999
2116
  }
2000
- const content = fs.readFileSync(entryPath, "utf8");
2001
- if (content.includes(from)) fs.writeFileSync(entryPath, content.split(from).join(to));
2117
+ const content = fs$1.readFileSync(entryPath, "utf8");
2118
+ if (content.includes(from)) fs$1.writeFileSync(entryPath, content.split(from).join(to));
2002
2119
  }
2003
2120
  }
2004
2121
  function renameStylesheetExtension(root, extension) {
2005
2122
  const cssFile = findAppCssFile(root);
2006
2123
  if (cssFile === void 0) return;
2007
- fs.renameSync(cssFile, path.join(path.dirname(cssFile), `App.${extension}`));
2124
+ fs$1.renameSync(cssFile, path$1.join(path$1.dirname(cssFile), `App.${extension}`));
2008
2125
  replaceInTextFiles(root, "'./App.css'", `'./App.${extension}'`);
2009
2126
  }
2010
2127
  function applyStyling(root, options) {
2011
2128
  const { styling, jsTemplateDir, hasNavigation, hasTypescript, templatesRoot } = options;
2012
2129
  if (styling === "css") return;
2013
2130
  if (styling === "css-modules" || styling === "stylesheet") {
2014
- renderTemplate(path.join(templatesRoot, "styling", "js", jsTemplateDir, styling), root, { hasTypescript });
2015
- if (hasNavigation) renderTemplate(path.join(templatesRoot, "styling", "navigation", jsTemplateDir, styling), root, { hasTypescript });
2131
+ renderTemplate(path$1.join(templatesRoot, "styling", "js", jsTemplateDir, styling), root, { hasTypescript });
2132
+ if (hasNavigation) renderTemplate(path$1.join(templatesRoot, "styling", "navigation", jsTemplateDir, styling), root, { hasTypescript });
2016
2133
  const orphanedCss = findAppCssFile(root);
2017
- if (orphanedCss !== void 0) fs.rmSync(orphanedCss, { force: true });
2134
+ if (orphanedCss !== void 0) fs$1.rmSync(orphanedCss, { force: true });
2018
2135
  return;
2019
2136
  }
2020
2137
  if (INLINE_STYLE_BLOCK_FRAMEWORKS.has(jsTemplateDir)) {
@@ -2053,50 +2170,50 @@ var TargetDirNotEmptyError = class extends Error {
2053
2170
  }
2054
2171
  };
2055
2172
  function resolveTemplatesRoot() {
2056
- const packageDir = path.dirname(fileURLToPath(import.meta.url));
2057
- const sibling = path.join(packageDir, "templates");
2058
- return fs.existsSync(sibling) ? sibling : path.resolve(packageDir, "../templates");
2173
+ const packageDir = path$1.dirname(fileURLToPath(import.meta.url));
2174
+ const sibling = path$1.join(packageDir, "templates");
2175
+ return fs$1.existsSync(sibling) ? sibling : path$1.resolve(packageDir, "../templates");
2059
2176
  }
2060
2177
  const templatesRoot = resolveTemplatesRoot();
2061
2178
  function isEmptyDir(dir) {
2062
- if (!fs.existsSync(dir)) return true;
2063
- const entries = fs.readdirSync(dir);
2179
+ if (!fs$1.existsSync(dir)) return true;
2180
+ const entries = fs$1.readdirSync(dir);
2064
2181
  return entries.length === 0 || entries.length === 1 && entries[0] === ".git";
2065
2182
  }
2066
2183
  function scaffoldApp(options) {
2067
- const root = path.resolve(process.cwd(), options.appName);
2184
+ const root = path$1.resolve(process.cwd(), options.appName);
2068
2185
  const jsTemplateDir = resolveJsTemplateDir(options.framework, options.vueFlavor);
2069
2186
  if (!isEmptyDir(root)) throw new TargetDirNotEmptyError(root);
2070
- fs.mkdirSync(root, { recursive: true });
2071
- const packageNameSource = options.appName === "." ? path.basename(root) : options.appName;
2072
- fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({
2187
+ fs$1.mkdirSync(root, { recursive: true });
2188
+ const packageNameSource = options.appName === "." ? path$1.basename(root) : options.appName;
2189
+ fs$1.writeFileSync(path$1.join(root, "package.json"), `${JSON.stringify({
2073
2190
  name: toValidPackageName(packageNameSource),
2074
2191
  version: "0.0.1",
2075
2192
  private: true
2076
2193
  }, null, 2)}\n`);
2077
- renderTemplate(path.join(templatesRoot, "native"), root, { hasTypescript: options.hasTypescript });
2078
- renderTemplate(path.join(templatesRoot, "js", jsTemplateDir), root, { hasTypescript: options.hasTypescript });
2194
+ renderTemplate(path$1.join(templatesRoot, "native"), root, { hasTypescript: options.hasTypescript });
2195
+ renderTemplate(path$1.join(templatesRoot, "js", jsTemplateDir), root, { hasTypescript: options.hasTypescript });
2079
2196
  applyBootsplashLogo(root, templatesRoot, options.framework);
2080
2197
  const hasExpoModules = options.hasExpoModules || (options.expoPackages?.size ?? 0) > 0;
2081
2198
  if (options.hasNavigation) {
2082
- const navigationLayerRoot = path.join(templatesRoot, "layers", "navigation");
2083
- renderTemplate(path.join(navigationLayerRoot, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
2084
- renderTemplate(path.join(navigationLayerRoot, "app", jsTemplateDir), root, { hasTypescript: options.hasTypescript });
2199
+ const navigationLayerRoot = path$1.join(templatesRoot, "layers", "navigation");
2200
+ renderTemplate(path$1.join(navigationLayerRoot, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
2201
+ renderTemplate(path$1.join(navigationLayerRoot, "app", jsTemplateDir), root, { hasTypescript: options.hasTypescript });
2085
2202
  }
2086
- if (hasExpoModules) renderTemplate(path.join(templatesRoot, "layers", "expo-modules"), root);
2203
+ if (hasExpoModules) renderTemplate(path$1.join(templatesRoot, "layers", "expo-modules"), root);
2087
2204
  if (options.hasSplashScreen) {
2088
- const splashScreenLayerRoot = path.join(templatesRoot, "layers", "splash-screen");
2089
- renderTemplate(path.join(splashScreenLayerRoot, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
2090
- renderTemplate(path.join(splashScreenLayerRoot, "native"), root);
2205
+ const splashScreenLayerRoot = path$1.join(templatesRoot, "layers", "splash-screen");
2206
+ renderTemplate(path$1.join(splashScreenLayerRoot, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
2207
+ renderTemplate(path$1.join(splashScreenLayerRoot, "native"), root);
2091
2208
  }
2092
- if (options.hasSlider) renderTemplate(path.join(templatesRoot, "layers", "slider", "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
2093
- for (const layerName of options.expoPackages ?? []) renderTemplate(path.join(templatesRoot, "layers", layerName, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
2209
+ if (options.hasSlider) renderTemplate(path$1.join(templatesRoot, "layers", "slider", "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
2210
+ for (const layerName of options.expoPackages ?? []) renderTemplate(path$1.join(templatesRoot, "layers", layerName, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
2094
2211
  if (options.hasTesting) {
2095
- const testingLayerRoot = path.join(templatesRoot, "layers", "testing");
2212
+ const testingLayerRoot = path$1.join(templatesRoot, "layers", "testing");
2096
2213
  const fragmentName = options.framework === "angular" ? "angular" : "base";
2097
- renderTemplate(path.join(testingLayerRoot, "fragment", fragmentName, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
2098
- renderTemplate(path.join(testingLayerRoot, "detox.config.js"), path.join(root, "detox.config.js"));
2099
- renderTemplate(path.join(testingLayerRoot, "e2e"), path.join(root, "e2e"), { hasTypescript: true });
2214
+ renderTemplate(path$1.join(testingLayerRoot, "fragment", fragmentName, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
2215
+ renderTemplate(path$1.join(testingLayerRoot, "detox.config.js"), path$1.join(root, "detox.config.js"));
2216
+ renderTemplate(path$1.join(testingLayerRoot, "e2e"), path$1.join(root, "e2e"), { hasTypescript: true });
2100
2217
  }
2101
2218
  if (hasExpoModules) {
2102
2219
  applyExpoModulesAndroidManifest(root);
@@ -2121,9 +2238,9 @@ function scaffoldApp(options) {
2121
2238
  //#endregion
2122
2239
  //#region src/utils/find-ios-app-dir.ts
2123
2240
  function findIosAppDir(root) {
2124
- const iosDir = path.join(root, "ios");
2125
- if (!fs.existsSync(iosDir)) return void 0;
2126
- for (const entry of fs.readdirSync(iosDir, { withFileTypes: true })) if (entry.isDirectory() && entry.name !== "Pods") return path.join(iosDir, entry.name);
2241
+ const iosDir = path$1.join(root, "ios");
2242
+ if (!fs$1.existsSync(iosDir)) return void 0;
2243
+ for (const entry of fs$1.readdirSync(iosDir, { withFileTypes: true })) if (entry.isDirectory() && entry.name !== "Pods") return path$1.join(iosDir, entry.name);
2127
2244
  }
2128
2245
  //#endregion
2129
2246
  //#region src/utils/apply-expo-modules-plist.ts
@@ -2135,14 +2252,14 @@ const USAGE_DESCRIPTIONS = [
2135
2252
  function applyExpoModulesInfoPlist(root) {
2136
2253
  const iosAppDir = findIosAppDir(root);
2137
2254
  if (iosAppDir === void 0) return;
2138
- const plistPath = path.join(iosAppDir, "Info.plist");
2139
- if (!fs.existsSync(plistPath)) return;
2140
- const plist = fs.readFileSync(plistPath, "utf8");
2255
+ const plistPath = path$1.join(iosAppDir, "Info.plist");
2256
+ if (!fs$1.existsSync(plistPath)) return;
2257
+ const plist = fs$1.readFileSync(plistPath, "utf8");
2141
2258
  const updated = USAGE_DESCRIPTIONS.reduce((current, [key, description]) => {
2142
2259
  if (current.includes(`<key>${key}</key>`)) return current;
2143
2260
  return current.replace("</dict>\n</plist>", `\t<key>${key}</key>\n\t<string>${description}</string>\n</dict>\n</plist>`);
2144
2261
  }, plist);
2145
- if (updated !== plist) fs.writeFileSync(plistPath, updated);
2262
+ if (updated !== plist) fs$1.writeFileSync(plistPath, updated);
2146
2263
  }
2147
2264
  //#endregion
2148
2265
  //#region src/utils/apply-splash-screen-app-delegate.ts
@@ -2151,9 +2268,9 @@ const REACT_IMPORT = "import React\n";
2151
2268
  function applySplashScreenAppDelegate(root) {
2152
2269
  const iosAppDir = findIosAppDir(root);
2153
2270
  if (iosAppDir === void 0) return;
2154
- const appDelegatePath = path.join(iosAppDir, "AppDelegate.swift");
2155
- if (!fs.existsSync(appDelegatePath)) return;
2156
- const source = fs.readFileSync(appDelegatePath, "utf8");
2271
+ const appDelegatePath = path$1.join(iosAppDir, "AppDelegate.swift");
2272
+ if (!fs$1.existsSync(appDelegatePath)) return;
2273
+ const source = fs$1.readFileSync(appDelegatePath, "utf8");
2157
2274
  if (source.includes("RNBootSplash.initWithStoryboard(")) return;
2158
2275
  if (!source.includes(REACT_IMPORT)) return;
2159
2276
  const updated = source.includes("import RNBootSplash") ? source : source.replace(REACT_IMPORT, `${REACT_IMPORT}import RNBootSplash\n`);
@@ -2162,13 +2279,13 @@ function applySplashScreenAppDelegate(root) {
2162
2279
  }
2163
2280
 
2164
2281
  `);
2165
- fs.writeFileSync(appDelegatePath, withInit);
2282
+ fs$1.writeFileSync(appDelegatePath, withInit);
2166
2283
  }
2167
2284
  //#endregion
2168
2285
  //#region src/utils/apply-splash-screen-main-activity.ts
2169
2286
  function findMainActivityKt(dir) {
2170
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2171
- const entryPath = path.join(dir, entry.name);
2287
+ for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
2288
+ const entryPath = path$1.join(dir, entry.name);
2172
2289
  if (entry.isDirectory()) {
2173
2290
  const found = findMainActivityKt(entryPath);
2174
2291
  if (found !== void 0) return found;
@@ -2180,32 +2297,32 @@ function findMainActivityKt(dir) {
2180
2297
  const ONCREATE_OPEN = /override fun onCreate\(savedInstanceState: Bundle\?\) \{\n/;
2181
2298
  const REACT_ACTIVITY_IMPORT = "import com.facebook.react.ReactActivity\n";
2182
2299
  function applySplashScreenMainActivity(root) {
2183
- const javaDir = path.join(root, "android/app/src/main/java");
2184
- if (!fs.existsSync(javaDir)) return;
2300
+ const javaDir = path$1.join(root, "android/app/src/main/java");
2301
+ if (!fs$1.existsSync(javaDir)) return;
2185
2302
  const mainActivityPath = findMainActivityKt(javaDir);
2186
2303
  if (mainActivityPath === void 0) return;
2187
- const source = fs.readFileSync(mainActivityPath, "utf8");
2304
+ const source = fs$1.readFileSync(mainActivityPath, "utf8");
2188
2305
  if (source.includes("RNBootSplash.init(")) return;
2189
2306
  if (!source.includes(REACT_ACTIVITY_IMPORT)) return;
2190
2307
  let updated = source.includes("import com.zoontek.rnbootsplash.RNBootSplash") ? source : source.replace(REACT_ACTIVITY_IMPORT, `${REACT_ACTIVITY_IMPORT}import com.zoontek.rnbootsplash.RNBootSplash\n`);
2191
2308
  if (!updated.includes("import android.os.Bundle")) updated = updated.replace(REACT_ACTIVITY_IMPORT, `import android.os.Bundle\n${REACT_ACTIVITY_IMPORT}`);
2192
2309
  updated = ONCREATE_OPEN.test(updated) ? updated.replace(ONCREATE_OPEN, (match) => `${match} RNBootSplash.init(this, R.style.BootTheme)\n`) : updated.replace("class MainActivity : ReactActivity() {\n", "class MainActivity : ReactActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n RNBootSplash.init(this, R.style.BootTheme)\n super.onCreate(savedInstanceState)\n }\n");
2193
- fs.writeFileSync(mainActivityPath, updated);
2310
+ fs$1.writeFileSync(mainActivityPath, updated);
2194
2311
  }
2195
2312
  //#endregion
2196
2313
  //#region src/utils/apply-splash-screen-manifest.ts
2197
2314
  const MAIN_ACTIVITY_BLOCK = /<activity\b[^>]*android:name="\.MainActivity"[^>]*>/;
2198
2315
  function applySplashScreenManifest(root) {
2199
- const manifestPath = path.join(root, "android/app/src/main/AndroidManifest.xml");
2200
- if (!fs.existsSync(manifestPath)) return;
2201
- const manifest = fs.readFileSync(manifestPath, "utf8");
2316
+ const manifestPath = path$1.join(root, "android/app/src/main/AndroidManifest.xml");
2317
+ if (!fs$1.existsSync(manifestPath)) return;
2318
+ const manifest = fs$1.readFileSync(manifestPath, "utf8");
2202
2319
  const match = manifest.match(MAIN_ACTIVITY_BLOCK);
2203
2320
  if (match === null) return;
2204
2321
  const block = match[0];
2205
2322
  if (block.includes("@style/BootTheme")) return;
2206
2323
  if (!block.includes("android:theme=\"@style/AppTheme\"")) return;
2207
2324
  const updatedBlock = block.replace("android:theme=\"@style/AppTheme\"", "android:theme=\"@style/BootTheme\"");
2208
- fs.writeFileSync(manifestPath, manifest.replace(block, updatedBlock));
2325
+ fs$1.writeFileSync(manifestPath, manifest.replace(block, updatedBlock));
2209
2326
  }
2210
2327
  //#endregion
2211
2328
  //#region src/utils/apply-splash-screen-styles.ts
@@ -2216,22 +2333,22 @@ const BOOT_THEME_STYLE = ` <style name="BootTheme" parent="Theme.BootSplash">
2216
2333
  </style>
2217
2334
  `;
2218
2335
  function applySplashScreenStyles(root) {
2219
- const stylesPath = path.join(root, "android/app/src/main/res/values/styles.xml");
2220
- if (!fs.existsSync(stylesPath)) return;
2221
- const styles = fs.readFileSync(stylesPath, "utf8");
2336
+ const stylesPath = path$1.join(root, "android/app/src/main/res/values/styles.xml");
2337
+ if (!fs$1.existsSync(stylesPath)) return;
2338
+ const styles = fs$1.readFileSync(stylesPath, "utf8");
2222
2339
  if (styles.includes("name=\"BootTheme\"")) return;
2223
2340
  if (!styles.includes("</resources>")) return;
2224
- fs.writeFileSync(stylesPath, styles.replace("</resources>", `${BOOT_THEME_STYLE}</resources>`));
2341
+ fs$1.writeFileSync(stylesPath, styles.replace("</resources>", `${BOOT_THEME_STYLE}</resources>`));
2225
2342
  }
2226
2343
  //#endregion
2227
2344
  //#region src/add-layers.ts
2228
2345
  function applyDependencyOnlyLayer(root, layerName) {
2229
- renderTemplate(path.join(templatesRoot, "layers", layerName, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
2346
+ renderTemplate(path$1.join(templatesRoot, "layers", layerName, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
2230
2347
  }
2231
2348
  function applyExpoModulesLayer(root) {
2232
2349
  applyDependencyOnlyLayer(root, "expo-modules");
2233
- if (fs.existsSync(path.join(root, "android/app/src/main/AndroidManifest.xml"))) applyExpoModulesAndroidManifest(root);
2234
- if (fs.existsSync(path.join(root, "ios"))) {
2350
+ if (fs$1.existsSync(path$1.join(root, "android/app/src/main/AndroidManifest.xml"))) applyExpoModulesAndroidManifest(root);
2351
+ if (fs$1.existsSync(path$1.join(root, "ios"))) {
2235
2352
  applyExpoModulesInfoPlist(root);
2236
2353
  applyExpoModulesPodfilePlatform(root);
2237
2354
  applyExpoModulesPodfileAutolinking(root);
@@ -2248,14 +2365,14 @@ function applySplashScreenLayer(root, framework) {
2248
2365
  applyBootsplashLogo(root, templatesRoot, framework);
2249
2366
  }
2250
2367
  async function applyTestingLayer(root, framework, confirmOverwrite) {
2251
- const testingLayerRoot = path.join(templatesRoot, "layers", "testing");
2368
+ const testingLayerRoot = path$1.join(templatesRoot, "layers", "testing");
2252
2369
  const fragmentName = framework === "angular" ? "angular" : "base";
2253
- renderTemplate(path.join(testingLayerRoot, "fragment", fragmentName, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
2254
- const detoxConfigPath = path.join(root, "detox.config.js");
2255
- const e2eDirPath = path.join(root, "e2e");
2256
- if ((fs.existsSync(detoxConfigPath) || fs.existsSync(e2eDirPath)) && !await confirmOverwrite()) return false;
2257
- renderTemplate(path.join(testingLayerRoot, "detox.config.js"), detoxConfigPath);
2258
- renderTemplate(path.join(testingLayerRoot, "e2e"), e2eDirPath, { hasTypescript: true });
2370
+ renderTemplate(path$1.join(testingLayerRoot, "fragment", fragmentName, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
2371
+ const detoxConfigPath = path$1.join(root, "detox.config.js");
2372
+ const e2eDirPath = path$1.join(root, "e2e");
2373
+ if ((fs$1.existsSync(detoxConfigPath) || fs$1.existsSync(e2eDirPath)) && !await confirmOverwrite()) return false;
2374
+ renderTemplate(path$1.join(testingLayerRoot, "detox.config.js"), detoxConfigPath);
2375
+ renderTemplate(path$1.join(testingLayerRoot, "e2e"), e2eDirPath, { hasTypescript: true });
2259
2376
  return true;
2260
2377
  }
2261
2378
  async function addLayersToApp(options) {
@@ -2330,6 +2447,591 @@ function getCommand(packageManager, script) {
2330
2447
  return packageManager === "npm" ? `npm run ${script}` : `${packageManager} ${script}`;
2331
2448
  }
2332
2449
  //#endregion
2450
+ //#region src/grant-bundles.ts
2451
+ var import_src = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
2452
+ const fs = __require("node:fs");
2453
+ const path = __require("node:path");
2454
+ const BEGIN_TAG = "SYMBIOTE-EXPO-LINK:BEGIN";
2455
+ const END_TAG = "SYMBIOTE-EXPO-LINK:END";
2456
+ const REGION_NOTE = "generated by symbiote-expo-link - do not edit, regenerated on every install";
2457
+ const REACT_ANDROID_ANCHOR = "implementation(\"com.facebook.react:react-android\")";
2458
+ const MODULES_MAP_ANCHOR = "override fun getModulesMap(): Map<Class<out Module>, String?> = mapOf(";
2459
+ const MODULES_PROVIDER_CLASS_ANCHOR = ": ModulesProvider {";
2460
+ const MANIFEST_FILENAME = "native-link.json";
2461
+ function isDebug() {
2462
+ return process.env.DEBUG === "1" || process.env.DEBUG === "true" || globalThis.__SYMBIOTE_DEBUG__ === true;
2463
+ }
2464
+ function dlog(...args) {
2465
+ if (isDebug()) console.log("[symbiote-expo-link]", ...args);
2466
+ }
2467
+ function warn(message) {
2468
+ console.warn(`[symbiote-expo-link] ${message}`);
2469
+ }
2470
+ function writeFileAtomic(filePath, content) {
2471
+ const tmpPath = `${filePath}.symbiote-expo-link.${process.pid}.tmp`;
2472
+ fs.writeFileSync(tmpPath, content);
2473
+ fs.renameSync(tmpPath, filePath);
2474
+ }
2475
+ function findAppRoot() {
2476
+ const candidates = [process.cwd(), process.env.INIT_CWD].filter(Boolean);
2477
+ for (const candidate of candidates) {
2478
+ if (fs.existsSync(path.join(candidate, "android", "app", "build.gradle"))) return candidate;
2479
+ if (findInfoPlistFile(candidate)) return candidate;
2480
+ }
2481
+ return null;
2482
+ }
2483
+ function findInfoPlistFile(appRoot) {
2484
+ const iosRoot = path.join(appRoot, "ios");
2485
+ if (!fs.existsSync(iosRoot)) return null;
2486
+ const skip = /Pods|build|DerivedData|Tests/i;
2487
+ const stack = [iosRoot];
2488
+ while (stack.length > 0) {
2489
+ const dir = stack.pop();
2490
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2491
+ if (skip.test(entry.name)) continue;
2492
+ const full = path.join(dir, entry.name);
2493
+ if (entry.isDirectory()) stack.push(full);
2494
+ else if (entry.name === "Info.plist") return full;
2495
+ }
2496
+ }
2497
+ return null;
2498
+ }
2499
+ function findMainApplicationFile(appRoot) {
2500
+ const javaRoot = path.join(appRoot, "android", "app", "src", "main", "java");
2501
+ if (!fs.existsSync(javaRoot)) return null;
2502
+ const stack = [javaRoot];
2503
+ while (stack.length > 0) {
2504
+ const dir = stack.pop();
2505
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2506
+ const full = path.join(dir, entry.name);
2507
+ if (entry.isDirectory()) stack.push(full);
2508
+ else if (entry.name === "MainApplication.kt") return full;
2509
+ }
2510
+ }
2511
+ return null;
2512
+ }
2513
+ function readManifest(packageDir, packageName) {
2514
+ const manifestPath = path.join(packageDir, MANIFEST_FILENAME);
2515
+ if (!fs.existsSync(manifestPath)) return null;
2516
+ try {
2517
+ return {
2518
+ packageName,
2519
+ manifest: JSON.parse(fs.readFileSync(manifestPath, "utf8"))
2520
+ };
2521
+ } catch (error) {
2522
+ warn(`skipping ${packageName}: its ${MANIFEST_FILENAME} is not valid JSON (${error.message})`);
2523
+ return null;
2524
+ }
2525
+ }
2526
+ function collectManifests(appRoot) {
2527
+ const modulesRoot = path.join(appRoot, "node_modules");
2528
+ if (!fs.existsSync(modulesRoot)) {
2529
+ warn(`no node_modules in ${appRoot}, nothing to link`);
2530
+ return [];
2531
+ }
2532
+ const found = [];
2533
+ for (const entry of fs.readdirSync(modulesRoot, { withFileTypes: true })) {
2534
+ if (entry.name.startsWith(".")) continue;
2535
+ if (entry.name.startsWith("@")) {
2536
+ const scopeDir = path.join(modulesRoot, entry.name);
2537
+ for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
2538
+ const entryFound = readManifest(path.join(scopeDir, scoped.name), `${entry.name}/${scoped.name}`);
2539
+ if (entryFound) found.push(entryFound);
2540
+ }
2541
+ continue;
2542
+ }
2543
+ const entryFound = readManifest(path.join(modulesRoot, entry.name), entry.name);
2544
+ if (entryFound) found.push(entryFound);
2545
+ }
2546
+ found.sort((a, b) => a.packageName < b.packageName ? -1 : a.packageName > b.packageName ? 1 : 0);
2547
+ dlog(`found ${found.length} linkable package(s): ${found.map((f) => f.packageName).join(", ")}`);
2548
+ return found;
2549
+ }
2550
+ function writeRegion({ filePath, content, name, indent, bodyLines, findAnchorIndex, anchorLabel }) {
2551
+ const lines = content.split("\n");
2552
+ const block = [
2553
+ `${indent}// ${BEGIN_TAG} ${name} (${REGION_NOTE})`,
2554
+ ...bodyLines.map((line) => `${indent}${line}`),
2555
+ `${indent}// ${END_TAG} ${name}`
2556
+ ];
2557
+ const beginIndex = lines.findIndex((line) => line.includes(`${BEGIN_TAG} ${name}`));
2558
+ if (beginIndex !== -1) {
2559
+ const endIndex = lines.findIndex((line, i) => i > beginIndex && line.includes(`${END_TAG} ${name}`));
2560
+ if (endIndex === -1) {
2561
+ warn(`${filePath} has "${BEGIN_TAG} ${name}" but no matching "${END_TAG} ${name}". Restore the end marker (or delete the whole block) and reinstall.`);
2562
+ return null;
2563
+ }
2564
+ lines.splice(beginIndex, endIndex - beginIndex + 1, ...block);
2565
+ return lines.join("\n");
2566
+ }
2567
+ const anchorIndex = findAnchorIndex(lines);
2568
+ if (anchorIndex === -1) {
2569
+ warn(`cannot create the ${name} block in ${filePath}: ${anchorLabel}`);
2570
+ return null;
2571
+ }
2572
+ lines.splice(anchorIndex + 1, 0, ...block);
2573
+ return lines.join("\n");
2574
+ }
2575
+ function applyIfChanged(filePath, content, nextContent) {
2576
+ if (nextContent === null || nextContent === content) return;
2577
+ writeFileAtomic(filePath, nextContent);
2578
+ dlog(`updated ${filePath}`);
2579
+ }
2580
+ function patchBuildGradle(appRoot, entries) {
2581
+ const gradlePath = path.join(appRoot, "android", "app", "build.gradle");
2582
+ if (!fs.existsSync(gradlePath)) {
2583
+ dlog("android/app/build.gradle not found, skipping");
2584
+ return;
2585
+ }
2586
+ const primaryProjectNames = entries.filter((entry) => entry.manifest.android && entry.manifest.android.gradleProjectName).map((entry) => entry.manifest.android.gradleProjectName);
2587
+ const serviceProjectNames = androidServicesOf(entries).map((service) => service.gradleProjectName);
2588
+ const deps = [.../* @__PURE__ */ new Set([...primaryProjectNames, ...serviceProjectNames])].sort().map((name) => `implementation project(':${name}')`);
2589
+ const content = fs.readFileSync(gradlePath, "utf8");
2590
+ applyIfChanged(gradlePath, content, writeRegion({
2591
+ filePath: gradlePath,
2592
+ content,
2593
+ name: "DEPENDENCIES",
2594
+ indent: " ",
2595
+ bodyLines: deps,
2596
+ findAnchorIndex: (lines) => lines.findIndex((line) => line.includes(REACT_ANDROID_ANCHOR)),
2597
+ anchorLabel: `no \`${REACT_ANDROID_ANCHOR}\` line to anchor below`
2598
+ }));
2599
+ }
2600
+ function androidModulesOf(entries) {
2601
+ return entries.flatMap((entry) => entry.manifest.android && Array.isArray(entry.manifest.android.modules) ? entry.manifest.android.modules : []);
2602
+ }
2603
+ function androidServicesOf(entries) {
2604
+ return entries.flatMap((entry) => entry.manifest.android && Array.isArray(entry.manifest.android.services) ? entry.manifest.android.services : []);
2605
+ }
2606
+ function patchMainApplication(appRoot, entries) {
2607
+ const filePath = findMainApplicationFile(appRoot);
2608
+ if (!filePath) {
2609
+ dlog("MainApplication.kt not found, skipping");
2610
+ return;
2611
+ }
2612
+ const modules = androidModulesOf(entries);
2613
+ const services = androidServicesOf(entries);
2614
+ const imports = [.../* @__PURE__ */ new Set([
2615
+ ...modules.map((module$1) => `import ${module$1.importPath}`),
2616
+ ...services.map((service) => `import ${service.importPath}`),
2617
+ "import expo.modules.kotlin.services.Service"
2618
+ ])].sort();
2619
+ const mapEntries = [...new Set(modules.map((module$2) => `${module$2.className}::class.java to "${module$2.nativeName}",`))].sort();
2620
+ const serviceEntries = [...new Set(services.map((service) => ` ${service.className}::class.java,`))].sort();
2621
+ const content = fs.readFileSync(filePath, "utf8");
2622
+ const withImports = writeRegion({
2623
+ filePath,
2624
+ content,
2625
+ name: "IMPORTS",
2626
+ indent: "",
2627
+ bodyLines: imports,
2628
+ findAnchorIndex: (lines) => {
2629
+ let lastImport = -1;
2630
+ for (let i = 0; i < lines.length; i += 1) if (lines[i].trim().startsWith("import ")) lastImport = i;
2631
+ return lastImport;
2632
+ },
2633
+ anchorLabel: "no import statements to anchor below"
2634
+ });
2635
+ if (withImports === null) return;
2636
+ const withModules = writeRegion({
2637
+ filePath,
2638
+ content: withImports,
2639
+ name: "MODULES-MAP",
2640
+ indent: " ",
2641
+ bodyLines: mapEntries,
2642
+ findAnchorIndex: (lines) => lines.findIndex((line) => line.includes(MODULES_MAP_ANCHOR)),
2643
+ anchorLabel: "no `getModulesMap()` declaration to anchor below"
2644
+ });
2645
+ if (withModules === null) return;
2646
+ applyIfChanged(filePath, content, writeRegion({
2647
+ filePath,
2648
+ content: withModules,
2649
+ name: "SERVICES",
2650
+ indent: " ",
2651
+ bodyLines: [
2652
+ "override fun getServices(): List<Class<out Service>> = listOf(",
2653
+ ...serviceEntries,
2654
+ ")"
2655
+ ],
2656
+ findAnchorIndex: (lines) => lines.findIndex((line) => line.includes(MODULES_PROVIDER_CLASS_ANCHOR)),
2657
+ anchorLabel: "no `ExpoModulesProvider : ModulesProvider` class declaration to anchor below"
2658
+ }));
2659
+ }
2660
+ function escapeXml(value) {
2661
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2662
+ }
2663
+ function escapeXmlAttribute(value) {
2664
+ return escapeXml(value).replace(/"/g, "&quot;");
2665
+ }
2666
+ function findTag(content, tagName) {
2667
+ const match = new RegExp(`<${tagName}(?=[\\s>/])`).exec(content);
2668
+ if (!match) return null;
2669
+ let quote = null;
2670
+ for (let i = match.index; i < content.length; i += 1) {
2671
+ const char = content[i];
2672
+ if (quote !== null) {
2673
+ if (char === quote) quote = null;
2674
+ continue;
2675
+ }
2676
+ if (char === "\"" || char === "'") quote = char;
2677
+ else if (char === ">") return {
2678
+ start: match.index,
2679
+ end: i
2680
+ };
2681
+ }
2682
+ return null;
2683
+ }
2684
+ function findApplicationTag(content) {
2685
+ return findTag(content, "application");
2686
+ }
2687
+ function findAllTags(content, tagName) {
2688
+ const tags = [];
2689
+ const re = new RegExp(`<${tagName}(?=[\\s>/])`, "g");
2690
+ let match = re.exec(content);
2691
+ while (match) {
2692
+ let quote = null;
2693
+ for (let i = match.index; i < content.length; i += 1) {
2694
+ const char = content[i];
2695
+ if (quote !== null) {
2696
+ if (char === quote) quote = null;
2697
+ continue;
2698
+ }
2699
+ if (char === "\"" || char === "'") quote = char;
2700
+ else if (char === ">") {
2701
+ tags.push({
2702
+ start: match.index,
2703
+ end: i
2704
+ });
2705
+ re.lastIndex = i + 1;
2706
+ break;
2707
+ }
2708
+ }
2709
+ match = re.exec(content);
2710
+ }
2711
+ return tags;
2712
+ }
2713
+ function findMainActivityTag(content) {
2714
+ for (const tag of findAllTags(content, "activity")) {
2715
+ const name = readXmlAttribute(content.slice(tag.start, tag.end), "android:name");
2716
+ if (name && /(^|\.)MainActivity$/.test(name)) return tag;
2717
+ }
2718
+ return null;
2719
+ }
2720
+ function readXmlAttribute(tagText, name) {
2721
+ const match = new RegExp(`\\s${name}\\s*=\\s*"([^"]*)"`).exec(tagText);
2722
+ return match ? match[1] : null;
2723
+ }
2724
+ function patchAndroidManifest(appRoot, entries) {
2725
+ const wanted = /* @__PURE__ */ new Map();
2726
+ for (const entry of entries) {
2727
+ const attributes = entry.manifest.android && entry.manifest.android.manifestApplicationAttributes;
2728
+ if (!attributes) continue;
2729
+ for (const [name, value] of Object.entries(attributes)) if (!wanted.has(name)) wanted.set(name, value);
2730
+ }
2731
+ if (wanted.size === 0) return;
2732
+ const manifestPath = path.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
2733
+ if (!fs.existsSync(manifestPath)) {
2734
+ dlog("AndroidManifest.xml not found, skipping application attributes");
2735
+ return;
2736
+ }
2737
+ const content = fs.readFileSync(manifestPath, "utf8");
2738
+ let next = content;
2739
+ for (const [name, value] of [...wanted].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
2740
+ if (!/^[\w.:-]+$/.test(name)) {
2741
+ warn(`skipping the manifest attribute "${name}": not a plain XML attribute name`);
2742
+ continue;
2743
+ }
2744
+ const tag = findApplicationTag(next);
2745
+ if (!tag) {
2746
+ warn(`${manifestPath} has no <application> element, skipping ${name}`);
2747
+ return;
2748
+ }
2749
+ const tagText = next.slice(tag.start, tag.end);
2750
+ const escaped = escapeXmlAttribute(value);
2751
+ const existing = readXmlAttribute(tagText, name);
2752
+ if (existing !== null) {
2753
+ if (existing !== escaped) warn(`${name} in ${manifestPath} reads "${existing}" but the package manifest says "${value}". Keeping the file as-is; edit it by hand if you want the manifest's.`);
2754
+ continue;
2755
+ }
2756
+ const lastNewline = tagText.lastIndexOf("\n");
2757
+ const separator = lastNewline === -1 ? " " : `\n${/^[ \t]*/.exec(tagText.slice(lastNewline + 1))[0]}`;
2758
+ const insertAt = next[tag.end - 1] === "/" ? tag.end - 1 : tag.end;
2759
+ next = `${next.slice(0, insertAt)}${separator}${name}="${escaped}"${next.slice(insertAt)}`;
2760
+ }
2761
+ applyIfChanged(manifestPath, content, next);
2762
+ }
2763
+ function patchMainActivityConfigChanges(appRoot, entries) {
2764
+ const wanted = /* @__PURE__ */ new Set();
2765
+ for (const entry of entries) {
2766
+ const values = entry.manifest.android && entry.manifest.android.mainActivityConfigChanges;
2767
+ if (!values) continue;
2768
+ for (const value of values) wanted.add(value);
2769
+ }
2770
+ if (wanted.size === 0) return;
2771
+ const manifestPath = path.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
2772
+ if (!fs.existsSync(manifestPath)) {
2773
+ dlog("AndroidManifest.xml not found, skipping MainActivity configChanges");
2774
+ return;
2775
+ }
2776
+ const content = fs.readFileSync(manifestPath, "utf8");
2777
+ const tag = findMainActivityTag(content);
2778
+ if (!tag) {
2779
+ warn(`${manifestPath} has no MainActivity <activity> element, skipping configChanges`);
2780
+ return;
2781
+ }
2782
+ const tagText = content.slice(tag.start, tag.end);
2783
+ const existingValue = readXmlAttribute(tagText, "android:configChanges");
2784
+ const existingTokens = existingValue ? existingValue.split("|").map((token) => token.trim()).filter(Boolean) : [];
2785
+ const existingSet = new Set(existingTokens);
2786
+ const missing = [...wanted].filter((token) => !existingSet.has(token)).sort();
2787
+ if (missing.length === 0) return;
2788
+ const merged = [...existingTokens, ...missing].join("|");
2789
+ let next;
2790
+ if (existingValue !== null) {
2791
+ const valueMatch = /android:configChanges\s*=\s*"/.exec(tagText);
2792
+ const valueStart = valueMatch.index + valueMatch[0].length;
2793
+ const valueEnd = tagText.indexOf("\"", valueStart);
2794
+ const nextTagText = tagText.slice(0, valueStart) + merged + tagText.slice(valueEnd);
2795
+ next = content.slice(0, tag.start) + nextTagText + content.slice(tag.end);
2796
+ } else {
2797
+ const lastNewline = tagText.lastIndexOf("\n");
2798
+ const separator = lastNewline === -1 ? " " : `\n${/^[ \t]*/.exec(tagText.slice(lastNewline + 1))[0]}`;
2799
+ const selfClosing = content[tag.end - 1] === "/";
2800
+ const insertAt = selfClosing ? tag.end - 1 : tag.end;
2801
+ next = `${content.slice(0, insertAt).replace(/[ \t]+$/, "")}${separator}android:configChanges="${merged}"${selfClosing ? " " : ""}${content.slice(insertAt)}`;
2802
+ }
2803
+ applyIfChanged(manifestPath, content, next);
2804
+ }
2805
+ function readManifestPermissions(content) {
2806
+ return new Set([...content.matchAll(/<uses-permission\s[^>]*android:name="([^"]+)"/g)].map((match) => match[1]));
2807
+ }
2808
+ function patchAndroidManifestPermissions(appRoot, entries) {
2809
+ const wanted = /* @__PURE__ */ new Set();
2810
+ for (const entry of entries) {
2811
+ const permissions = entry.manifest.android && entry.manifest.android.manifestPermissions;
2812
+ if (!permissions) continue;
2813
+ for (const permission of permissions) wanted.add(permission);
2814
+ }
2815
+ if (wanted.size === 0) return;
2816
+ const manifestPath = path.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
2817
+ if (!fs.existsSync(manifestPath)) {
2818
+ dlog("AndroidManifest.xml not found, skipping permissions");
2819
+ return;
2820
+ }
2821
+ const content = fs.readFileSync(manifestPath, "utf8");
2822
+ const existing = readManifestPermissions(content);
2823
+ const missing = [...wanted].filter((permission) => !existing.has(permission)).sort();
2824
+ if (missing.length === 0) return;
2825
+ const tag = findTag(content, "manifest");
2826
+ if (!tag) {
2827
+ warn(`${manifestPath} has no <manifest> element, skipping permissions`);
2828
+ return;
2829
+ }
2830
+ const lines = missing.map((permission) => ` <uses-permission android:name="${permission}" />`).join("\n");
2831
+ applyIfChanged(manifestPath, content, `${content.slice(0, tag.end + 1)}\n${lines}${content.slice(tag.end + 1)}`);
2832
+ }
2833
+ function readManifestServiceNames(content) {
2834
+ return new Set([...content.matchAll(/<service\s[^>]*android:name="([^"]+)"/g)].map((match) => match[1]));
2835
+ }
2836
+ function buildServiceXml(service, indent) {
2837
+ const attrs = [`android:name="${escapeXmlAttribute(service.name)}"`, `android:exported="${service.exported === true ? "true" : "false"}"`];
2838
+ if (service.foregroundServiceType) attrs.push(`android:foregroundServiceType="${escapeXmlAttribute(service.foregroundServiceType)}"`);
2839
+ const openTag = `${indent}<service ${attrs.join(" ")}`;
2840
+ if (!service.intentFilterActions || service.intentFilterActions.length === 0) return `${openTag} />`;
2841
+ return `${openTag}>\n${indent} <intent-filter>\n${service.intentFilterActions.map((action) => `${indent} <action android:name="${escapeXmlAttribute(action)}" />`).join("\n")}\n${indent} </intent-filter>\n${indent}</service>`;
2842
+ }
2843
+ function patchAndroidManifestServices(appRoot, entries) {
2844
+ const wanted = /* @__PURE__ */ new Map();
2845
+ for (const entry of entries) {
2846
+ const services = entry.manifest.android && entry.manifest.android.manifestServices;
2847
+ if (!services) continue;
2848
+ for (const service of services) if (!wanted.has(service.name)) wanted.set(service.name, service);
2849
+ }
2850
+ if (wanted.size === 0) return;
2851
+ const manifestPath = path.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
2852
+ if (!fs.existsSync(manifestPath)) {
2853
+ dlog("AndroidManifest.xml not found, skipping services");
2854
+ return;
2855
+ }
2856
+ const content = fs.readFileSync(manifestPath, "utf8");
2857
+ const existing = readManifestServiceNames(content);
2858
+ const missing = [...wanted.values()].filter((service) => !existing.has(service.name)).sort((a, b) => a.name < b.name ? -1 : 1);
2859
+ if (missing.length === 0) return;
2860
+ const tag = findApplicationTag(content);
2861
+ if (!tag) {
2862
+ warn(`${manifestPath} has no <application> element, skipping services`);
2863
+ return;
2864
+ }
2865
+ if (content[tag.end - 1] === "/") {
2866
+ warn(`${manifestPath}'s <application> is self-closing, skipping services`);
2867
+ return;
2868
+ }
2869
+ const lastNewline = content.lastIndexOf("\n", tag.start);
2870
+ const indent = `${/^[ \t]*/.exec(content.slice(lastNewline + 1, tag.start))[0]} `;
2871
+ const blocks = missing.map((service) => buildServiceXml(service, indent)).join("\n");
2872
+ const insertAt = tag.end + 1;
2873
+ applyIfChanged(manifestPath, content, `${content.slice(0, insertAt)}\n${blocks}${content.slice(insertAt)}`);
2874
+ }
2875
+ function readPlistString(content, key) {
2876
+ const keyIndex = content.indexOf(`<key>${key}</key>`);
2877
+ if (keyIndex === -1) return null;
2878
+ const match = /<string>([\s\S]*?)<\/string>/.exec(content.slice(keyIndex));
2879
+ return match ? match[1] : null;
2880
+ }
2881
+ function patchInfoPlist(appRoot, entries) {
2882
+ const wanted = /* @__PURE__ */ new Map();
2883
+ for (const entry of entries) {
2884
+ const keys = entry.manifest.ios && entry.manifest.ios.infoPlistKeys;
2885
+ if (!keys) continue;
2886
+ for (const [key, description] of Object.entries(keys)) if (!wanted.has(key)) wanted.set(key, description);
2887
+ }
2888
+ if (wanted.size === 0) return;
2889
+ const plistPath = findInfoPlistFile(appRoot);
2890
+ if (!plistPath) {
2891
+ dlog("Info.plist not found, skipping iOS permission strings");
2892
+ return;
2893
+ }
2894
+ const content = fs.readFileSync(plistPath, "utf8");
2895
+ let next = content;
2896
+ for (const [key, description] of [...wanted].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
2897
+ const escaped = escapeXml(description);
2898
+ const existing = readPlistString(next, key);
2899
+ if (existing !== null) {
2900
+ if (existing !== escaped) warn(`${key} in ${plistPath} reads "${existing}" but the package manifest says "${description}". Keeping the file as-is; edit it by hand if you want the manifest's.`);
2901
+ continue;
2902
+ }
2903
+ const plistCloseIndex = next.lastIndexOf("</plist>");
2904
+ const dictCloseIndex = next.lastIndexOf("</dict>", plistCloseIndex === -1 ? void 0 : plistCloseIndex);
2905
+ if (dictCloseIndex === -1) {
2906
+ warn(`${plistPath} has no closing </dict>, skipping ${key}`);
2907
+ continue;
2908
+ }
2909
+ const block = `\t<key>${key}</key>\n\t<string>${escaped}</string>\n`;
2910
+ next = next.slice(0, dictCloseIndex) + block + next.slice(dictCloseIndex);
2911
+ }
2912
+ applyIfChanged(plistPath, content, next);
2913
+ }
2914
+ function readPlistArray(content, key) {
2915
+ const keyTag = `<key>${key}</key>`;
2916
+ const keyIndex = content.indexOf(keyTag);
2917
+ if (keyIndex === -1) return null;
2918
+ const arrayOpenIndex = content.indexOf("<array>", keyIndex + keyTag.length);
2919
+ const arrayCloseIndex = arrayOpenIndex === -1 ? -1 : content.indexOf("</array>", arrayOpenIndex);
2920
+ if (arrayOpenIndex === -1 || arrayCloseIndex === -1) return null;
2921
+ return {
2922
+ arrayCloseIndex,
2923
+ items: [...content.slice(arrayOpenIndex + 7, arrayCloseIndex).matchAll(/<string>([\s\S]*?)<\/string>/g)].map((match) => match[1])
2924
+ };
2925
+ }
2926
+ function patchInfoPlistArrays(appRoot, entries) {
2927
+ const wanted = /* @__PURE__ */ new Map();
2928
+ for (const entry of entries) {
2929
+ const arrayKeys = entry.manifest.ios && entry.manifest.ios.infoPlistArrayKeys;
2930
+ if (!arrayKeys) continue;
2931
+ for (const [key, items] of Object.entries(arrayKeys)) {
2932
+ if (!wanted.has(key)) wanted.set(key, /* @__PURE__ */ new Set());
2933
+ for (const item of items) wanted.get(key).add(item);
2934
+ }
2935
+ }
2936
+ if (wanted.size === 0) return;
2937
+ const plistPath = findInfoPlistFile(appRoot);
2938
+ if (!plistPath) {
2939
+ dlog("Info.plist not found, skipping iOS array keys");
2940
+ return;
2941
+ }
2942
+ const content = fs.readFileSync(plistPath, "utf8");
2943
+ let next = content;
2944
+ for (const [key, itemsSet] of [...wanted].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
2945
+ const wantedItems = [...itemsSet];
2946
+ const existing = readPlistArray(next, key);
2947
+ if (existing === null) {
2948
+ const plistCloseIndex = next.lastIndexOf("</plist>");
2949
+ const dictCloseIndex = next.lastIndexOf("</dict>", plistCloseIndex === -1 ? void 0 : plistCloseIndex);
2950
+ if (dictCloseIndex === -1) {
2951
+ warn(`${plistPath} has no closing </dict>, skipping ${key}`);
2952
+ continue;
2953
+ }
2954
+ const block = `\t<key>${key}</key>\n\t<array>\n${wantedItems.map((item) => `\t\t<string>${escapeXml(item)}</string>`).join("\n")}\n\t</array>\n`;
2955
+ next = next.slice(0, dictCloseIndex) + block + next.slice(dictCloseIndex);
2956
+ continue;
2957
+ }
2958
+ const missing = wantedItems.filter((item) => !existing.items.includes(item));
2959
+ if (missing.length === 0) continue;
2960
+ const missingLines = missing.map((item) => `\t\t<string>${escapeXml(item)}</string>\n`).join("");
2961
+ next = next.slice(0, existing.arrayCloseIndex) + missingLines + next.slice(existing.arrayCloseIndex);
2962
+ }
2963
+ applyIfChanged(plistPath, content, next);
2964
+ }
2965
+ function linkApp(explicitAppRoot) {
2966
+ const appRoot = explicitAppRoot || findAppRoot();
2967
+ if (!appRoot) {
2968
+ warn("no React Native app here (no android/app/build.gradle, no ios/**/Info.plist), skipping");
2969
+ return;
2970
+ }
2971
+ const entries = collectManifests(appRoot);
2972
+ patchBuildGradle(appRoot, entries);
2973
+ patchMainApplication(appRoot, entries);
2974
+ patchAndroidManifest(appRoot, entries);
2975
+ patchAndroidManifestPermissions(appRoot, entries);
2976
+ patchAndroidManifestServices(appRoot, entries);
2977
+ patchMainActivityConfigChanges(appRoot, entries);
2978
+ patchInfoPlist(appRoot, entries);
2979
+ patchInfoPlistArrays(appRoot, entries);
2980
+ }
2981
+ module.exports = {
2982
+ linkApp,
2983
+ collectManifests,
2984
+ findAppRoot,
2985
+ findInfoPlistFile,
2986
+ findMainApplicationFile,
2987
+ patchBuildGradle,
2988
+ patchMainApplication,
2989
+ patchAndroidManifest,
2990
+ patchAndroidManifestPermissions,
2991
+ patchAndroidManifestServices,
2992
+ patchMainActivityConfigChanges,
2993
+ patchInfoPlist,
2994
+ patchInfoPlistArrays
2995
+ };
2996
+ })))();
2997
+ function discoveredBundlesFromLayers(layers) {
2998
+ return layers.flatMap((layer) => (layer.optionalManifestBundles ?? []).map((bundle) => ({
2999
+ packageName: layer.symbiotePackage,
3000
+ bundle
3001
+ })));
3002
+ }
3003
+ function discoverOptionalBundles(appRoot) {
3004
+ const discovered = [];
3005
+ for (const entry of (0, import_src.collectManifests)(appRoot)) {
3006
+ const bundles = entry.manifest.android?.optionalManifestBundles;
3007
+ if (!bundles) continue;
3008
+ for (const bundle of bundles) discovered.push({
3009
+ packageName: entry.packageName,
3010
+ bundle
3011
+ });
3012
+ }
3013
+ return discovered;
3014
+ }
3015
+ function applyBundle(appRoot, bundle) {
3016
+ const entries = [{
3017
+ packageName: `grant:${bundle.id}`,
3018
+ manifest: { android: {
3019
+ manifestPermissions: bundle.manifestPermissions,
3020
+ manifestServices: bundle.manifestServices
3021
+ } }
3022
+ }];
3023
+ (0, import_src.patchAndroidManifestPermissions)(appRoot, entries);
3024
+ (0, import_src.patchAndroidManifestServices)(appRoot, entries);
3025
+ }
3026
+ function packageShortName(packageName) {
3027
+ const slashIndex = packageName.lastIndexOf("/");
3028
+ return slashIndex === -1 ? packageName : packageName.slice(slashIndex + 1);
3029
+ }
3030
+ function filterBundlesForLayer(discovered, layer) {
3031
+ if (layer === void 0) return [...discovered];
3032
+ return discovered.filter((entry) => packageShortName(entry.packageName) === layer);
3033
+ }
3034
+ //#endregion
2333
3035
  //#region src/detect-package-manager.ts
2334
3036
  const USER_AGENT_PREFIXES = [
2335
3037
  ["pnpm", "pnpm"],
@@ -2433,6 +3135,15 @@ async function resolveOverwrite(targetDir, force) {
2433
3135
  assertNotCancelled(answer);
2434
3136
  return answer;
2435
3137
  }
3138
+ async function resolveGitInit() {
3139
+ if (!isInteractive()) return false;
3140
+ const answer = await confirm({
3141
+ message: "Initialize a git repository?",
3142
+ initialValue: true
3143
+ });
3144
+ assertNotCancelled(answer);
3145
+ return answer;
3146
+ }
2436
3147
  async function resolveStyling(styling) {
2437
3148
  if (styling !== void 0) return styling;
2438
3149
  if (!isInteractive()) return "css";
@@ -2581,6 +3292,10 @@ const ADD_LAYER_OPTIONS = [
2581
3292
  hint: layer.symbiotePackage
2582
3293
  }))
2583
3294
  ];
3295
+ const ADD_LAYER_NAME_SET = new Set(ADD_LAYER_OPTIONS.map((option) => option.value));
3296
+ function isAddLayerName(value) {
3297
+ return ADD_LAYER_NAME_SET.has(value);
3298
+ }
2584
3299
  function explicitLayersFromFlags(flags) {
2585
3300
  const explicit = [];
2586
3301
  if (flags.hasNavigation) explicit.push("navigation");
@@ -2622,7 +3337,22 @@ async function resolveAddLayers(flags, alreadyAdded, hasForce = false) {
2622
3337
  required: false
2623
3338
  });
2624
3339
  assertNotCancelled(answer);
2625
- return withImpliedExpoModules(dropAlreadyAdded(answer, alreadyAdded, hasForce));
3340
+ return withImpliedExpoModules(dropAlreadyAdded(answer.filter(isAddLayerName), alreadyAdded, hasForce));
3341
+ }
3342
+ async function resolveGrantSelection(candidates) {
3343
+ if (!isInteractive()) throw new CliUsageError("Not running in an interactive terminal — \"grant\" needs a real choice for each policy-sensitive bundle, so it cannot run non-interactively.");
3344
+ const answer = await multiselect({
3345
+ message: "Which policy-sensitive capabilities should this app request?",
3346
+ options: candidates.map((entry, index) => ({
3347
+ value: String(index),
3348
+ label: entry.bundle.label,
3349
+ hint: entry.packageName
3350
+ })),
3351
+ required: false
3352
+ });
3353
+ assertNotCancelled(answer);
3354
+ const selectedIndexes = new Set(answer);
3355
+ return candidates.filter((_entry, index) => selectedIndexes.has(String(index)));
2626
3356
  }
2627
3357
  async function resolvePackageManager(packageManager) {
2628
3358
  const detected = packageManager ?? detectPackageManagerFromUserAgent();
@@ -2649,6 +3379,11 @@ async function resolvePackageManager(packageManager) {
2649
3379
  return answer;
2650
3380
  }
2651
3381
  //#endregion
3382
+ //#region src/commands/grant-summary.ts
3383
+ function printGrantedNotes(granted) {
3384
+ for (const entry of granted) note(`${entry.bundle.warning}\n\nNext: ${entry.bundle.nextSteps}`, entry.bundle.label);
3385
+ }
3386
+ //#endregion
2652
3387
  //#region src/commands/add.ts
2653
3388
  function resolveFrameworkForAdd(explicit, cwd = process.cwd()) {
2654
3389
  const detected = detectSymbioteFrameworkFromDependencies(readCwdDependencies(cwd));
@@ -2690,20 +3425,70 @@ async function runAdd(parsed) {
2690
3425
  const allSkips = [...alreadyPresentSkips, ...result.skippedLayers];
2691
3426
  if (allSkips.length > 0) lines.push("", `Skipped (already present, left untouched): ${allSkips.join(", ")}.`);
2692
3427
  if (result.appliedLayers.includes("navigation")) lines.push("", "Wire it up in your App entry:", import_picocolors.default.dim(` import { Stack } from '@symbiote-native/navigation/${framework}';`));
3428
+ const grantable = expoPackagesWithOptionalManifestBundles(new Set(result.appliedLayers));
3429
+ if (grantable.length > 0) {
3430
+ const candidates = discoveredBundlesFromLayers(grantable);
3431
+ let granted = [];
3432
+ let asked = false;
3433
+ try {
3434
+ granted = await resolveGrantSelection(candidates);
3435
+ asked = true;
3436
+ } catch (error) {
3437
+ if (!(error instanceof CliUsageError)) throw error;
3438
+ }
3439
+ if (granted.length > 0) {
3440
+ for (const entry of granted) applyBundle(cwd, entry.bundle);
3441
+ printGrantedNotes(granted);
3442
+ lines.push("", `Granted: ${granted.map((entry) => entry.bundle.label).join(", ")}.`);
3443
+ } else if (!asked) lines.push("", `${grantable.map((layer) => layer.label).join(", ")} also offer${grantable.length === 1 ? "s" : ""} optional, policy-sensitive Android permissions — after installing:`, ...grantable.map((layer) => import_picocolors.default.dim(` npx @symbiote-native/cli grant ${layer.id}`)));
3444
+ }
2693
3445
  outro(lines.length > 0 ? lines.join("\n") : "Nothing changed.");
2694
3446
  }
2695
3447
  //#endregion
3448
+ //#region src/commands/grant.ts
3449
+ async function runGrant(parsed) {
3450
+ intro("@symbiote-native/cli grant");
3451
+ const appRoot = (0, import_src.findAppRoot)();
3452
+ if (!appRoot) throw new NotSymbioteAppError();
3453
+ const candidates = filterBundlesForLayer(discoverOptionalBundles(appRoot), parsed.layer);
3454
+ if (candidates.length === 0) {
3455
+ outro(parsed.layer === void 0 ? "Nothing to grant — no installed package declares an optional manifest bundle." : `Nothing to grant for "${parsed.layer}" — it declares no optional manifest bundle, or isn't installed.`);
3456
+ return;
3457
+ }
3458
+ const selected = await resolveGrantSelection(candidates);
3459
+ if (selected.length === 0) {
3460
+ outro("Nothing granted.");
3461
+ return;
3462
+ }
3463
+ for (const entry of selected) applyBundle(appRoot, entry.bundle);
3464
+ printGrantedNotes(selected);
3465
+ outro(`Granted: ${selected.map((entry) => entry.bundle.label).join(", ")}.`);
3466
+ }
3467
+ //#endregion
3468
+ //#region src/utils/git-init.ts
3469
+ function tryGitInit(root) {
3470
+ try {
3471
+ execFileSync("git", ["init"], {
3472
+ cwd: root,
3473
+ stdio: "ignore"
3474
+ });
3475
+ return true;
3476
+ } catch {
3477
+ return false;
3478
+ }
3479
+ }
3480
+ //#endregion
2696
3481
  //#region src/commands/new.ts
2697
3482
  async function runNew(parsed) {
2698
3483
  intro("@symbiote-native/cli new");
2699
3484
  const appName = await resolveAppName(parsed.appName);
2700
- const targetDir = path.resolve(process.cwd(), appName);
3485
+ const targetDir = path$1.resolve(process.cwd(), appName);
2701
3486
  if (!isEmptyDir(targetDir)) {
2702
3487
  if (!await resolveOverwrite(targetDir, parsed.hasForce)) {
2703
3488
  cancel("Cancelled.");
2704
3489
  process.exit(1);
2705
3490
  }
2706
- fs.rmSync(targetDir, {
3491
+ fs$1.rmSync(targetDir, {
2707
3492
  recursive: true,
2708
3493
  force: true
2709
3494
  });
@@ -2729,23 +3514,38 @@ async function runNew(parsed) {
2729
3514
  styling,
2730
3515
  packageManager
2731
3516
  });
2732
- const hasGitDir = fs.existsSync(path.join(root, ".git"));
2733
- outro([
3517
+ const hasGitDir = fs$1.existsSync(path$1.join(root, ".git"));
3518
+ const lines = [
2734
3519
  `Scaffolded "${appName}" at ${root}.`,
2735
3520
  "",
2736
3521
  ...appName === "." ? [] : [import_picocolors.default.bold(import_picocolors.default.green(` cd ${appName}`))],
2737
3522
  import_picocolors.default.bold(import_picocolors.default.green(` ${getCommand(packageManager, "install")}`)),
2738
- import_picocolors.default.bold(import_picocolors.default.green(` ${getCommand(packageManager, "dev")}`)),
2739
- ...hasGitDir ? [] : [
2740
- "",
2741
- "Not a git repository yet:",
2742
- import_picocolors.default.bold(import_picocolors.default.green(" git init && git add -A && git commit -m \"initial commit\""))
2743
- ]
2744
- ].join("\n"));
3523
+ import_picocolors.default.bold(import_picocolors.default.green(` ${getCommand(packageManager, "dev")}`))
3524
+ ];
3525
+ if (!hasGitDir && await resolveGitInit() && tryGitInit(root)) lines.push("", import_picocolors.default.dim("Initialized an empty git repository."));
3526
+ else if (!hasGitDir) lines.push("", "Not a git repository yet:", import_picocolors.default.bold(import_picocolors.default.green(" git init && git add -A && git commit -m \"initial commit\"")));
3527
+ const grantable = expoPackagesWithOptionalManifestBundles(expoPackages);
3528
+ if (grantable.length > 0) {
3529
+ const candidates = discoveredBundlesFromLayers(grantable);
3530
+ let granted = [];
3531
+ let asked = false;
3532
+ try {
3533
+ granted = await resolveGrantSelection(candidates);
3534
+ asked = true;
3535
+ } catch (error) {
3536
+ if (!(error instanceof CliUsageError)) throw error;
3537
+ }
3538
+ if (granted.length > 0) {
3539
+ for (const entry of granted) applyBundle(root, entry.bundle);
3540
+ printGrantedNotes(granted);
3541
+ lines.push("", `Granted: ${granted.map((entry) => entry.bundle.label).join(", ")}.`);
3542
+ } else if (!asked) lines.push("", `${grantable.map((layer) => layer.label).join(", ")} also offer${grantable.length === 1 ? "s" : ""} optional, policy-sensitive Android permissions — after installing:`, ...grantable.map((layer) => import_picocolors.default.dim(` npx @symbiote-native/cli grant ${layer.id}`)));
3543
+ }
3544
+ outro(lines.join("\n"));
2745
3545
  }
2746
3546
  //#endregion
2747
3547
  //#region package.json
2748
- var version = "0.1.2";
3548
+ var version = "0.2.1";
2749
3549
  //#endregion
2750
3550
  //#region src/index.ts
2751
3551
  function printHelp() {
@@ -2761,6 +3561,10 @@ Usage:
2761
3561
  npx @symbiote-native/cli add [--navigation] [--expo-modules] [--testing] [--splash-screen] [--slider]
2762
3562
  [--<expo-package> ...] [--force] [--pm npm|pnpm|yarn]
2763
3563
  (run inside an existing @symbiote-native/* app)
3564
+ npx @symbiote-native/cli grant [layer]
3565
+ interactively opt into policy-sensitive Android permissions/services
3566
+ (e.g. background location, audio recording) an installed package offers;
3567
+ [layer] narrows to one package's short name, e.g. "grant location"
2764
3568
 
2765
3569
  npx @symbiote-native/cli --version print the CLI version
2766
3570
  npx @symbiote-native/cli --help print this message
@@ -2778,6 +3582,9 @@ async function main() {
2778
3582
  case "add":
2779
3583
  await runAdd(parsed);
2780
3584
  return;
3585
+ case "grant":
3586
+ await runGrant(parsed);
3587
+ return;
2781
3588
  case "help":
2782
3589
  printHelp();
2783
3590
  return;