erne-universal 0.10.8 → 0.10.10

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/lib/audit.js CHANGED
@@ -869,6 +869,31 @@ function checkPermissions(cwd, deps, findings, strengths) {
869
869
  const declaredIosKeys = new Set(Object.keys(infoPlist));
870
870
  const declaredAndroidPerms = new Set(androidPermissions);
871
871
 
872
+ // Expo plugins automatically add permissions at build time
873
+ // Map plugin names to the permissions they auto-configure
874
+ const plugins = expo?.plugins || [];
875
+ const pluginNames = new Set(plugins.map(p => Array.isArray(p) ? p[0] : p));
876
+ const PLUGIN_PERMISSIONS = {
877
+ 'expo-camera': { iosKeys: ['NSCameraUsageDescription', 'NSMicrophoneUsageDescription'], androidPerms: ['android.permission.CAMERA'] },
878
+ 'expo-location': { iosKeys: ['NSLocationWhenInUseUsageDescription', 'NSLocationAlwaysUsageDescription', 'NSLocationAlwaysAndWhenInUseUsageDescription'], androidPerms: ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION'] },
879
+ 'expo-av': { iosKeys: ['NSMicrophoneUsageDescription'], androidPerms: ['android.permission.RECORD_AUDIO'] },
880
+ 'expo-audio': { iosKeys: ['NSMicrophoneUsageDescription'], androidPerms: ['android.permission.RECORD_AUDIO'] },
881
+ 'expo-media-library': { iosKeys: ['NSPhotoLibraryUsageDescription', 'NSPhotoLibraryAddUsageDescription'], androidPerms: ['android.permission.READ_MEDIA_IMAGES'] },
882
+ 'expo-image-picker': { iosKeys: ['NSCameraUsageDescription', 'NSPhotoLibraryUsageDescription'], androidPerms: ['android.permission.CAMERA'] },
883
+ 'expo-contacts': { iosKeys: ['NSContactsUsageDescription'], androidPerms: ['android.permission.READ_CONTACTS'] },
884
+ 'expo-calendar': { iosKeys: ['NSCalendarsUsageDescription'], androidPerms: ['android.permission.READ_CALENDAR'] },
885
+ 'expo-notifications': { iosKeys: [], androidPerms: [] },
886
+ };
887
+
888
+ // Add plugin-provided permissions to declared sets
889
+ for (const pluginName of pluginNames) {
890
+ const pluginPerms = PLUGIN_PERMISSIONS[pluginName];
891
+ if (pluginPerms) {
892
+ for (const key of pluginPerms.iosKeys) declaredIosKeys.add(key);
893
+ for (const perm of pluginPerms.androidPerms) declaredAndroidPerms.add(perm);
894
+ }
895
+ }
896
+
872
897
  let missingCount = 0;
873
898
  let installedPermCount = 0;
874
899
  const usedIosKeys = new Set();
@@ -884,27 +909,27 @@ function checkPermissions(cwd, deps, findings, strengths) {
884
909
  if (api.iosKey) usedIosKeys.add(api.iosKey);
885
910
  if (api.androidPerm) usedAndroidPerms.add(api.androidPerm);
886
911
 
887
- // Check iOS plist declaration
912
+ // Check iOS plist declaration (manual infoPlist OR plugin-provided)
888
913
  if (api.iosKey && appConfig && !declaredIosKeys.has(api.iosKey)) {
889
914
  missingCount++;
890
915
  findings.push({
891
916
  severity: SEVERITY.critical,
892
917
  category: CATEGORY.permissions,
893
918
  title: `Missing iOS permission: ${api.iosKey}`,
894
- detail: `${installed.join(', ')} requires ${name} permission but ${api.iosKey} is not declared in app.json ios.infoPlist.`,
895
- fix: `Add "${api.iosKey}" to app.json > expo > ios > infoPlist`,
919
+ detail: `${installed.join(', ')} requires ${name} permission but ${api.iosKey} is not declared in app.json ios.infoPlist and no matching Expo plugin found.`,
920
+ fix: `Add "${api.iosKey}" to app.json > expo > ios > infoPlist, or add the corresponding Expo plugin`,
896
921
  });
897
922
  }
898
923
 
899
- // Check Android permission declaration
924
+ // Check Android permission declaration (manual OR plugin-provided)
900
925
  if (api.androidPerm && appConfig && androidPermissions.length > 0 && !declaredAndroidPerms.has(api.androidPerm)) {
901
926
  missingCount++;
902
927
  findings.push({
903
928
  severity: SEVERITY.critical,
904
929
  category: CATEGORY.permissions,
905
930
  title: `Missing Android permission: ${api.androidPerm}`,
906
- detail: `${installed.join(', ')} requires ${name} permission but ${api.androidPerm} is not declared in app.json android.permissions.`,
907
- fix: `Add "${api.androidPerm}" to app.json > expo > android > permissions`,
931
+ detail: `${installed.join(', ')} requires ${name} permission but ${api.androidPerm} is not declared in app.json android.permissions and no matching Expo plugin found.`,
932
+ fix: `Add "${api.androidPerm}" to app.json > expo > android > permissions, or add the corresponding Expo plugin`,
908
933
  });
909
934
  }
910
935
  }
package/lib/generate.js CHANGED
@@ -333,20 +333,51 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
333
333
  fs.writeFileSync(destHooksPath, JSON.stringify(merged, null, 2));
334
334
  }
335
335
 
336
- // 7. Copy MCP configs
336
+ // 7. Copy MCP configs + generate .mcp.json for Claude Code
337
337
  if (mcpSelections && mcpSelections.length > 0) {
338
338
  const mcpSrc = path.join(erneRoot, 'mcp-configs');
339
339
  const mcpDest = path.join(targetDir, 'mcp');
340
+ const mcpServers = {};
340
341
  if (fs.existsSync(mcpSrc)) {
341
342
  fs.mkdirSync(mcpDest, { recursive: true });
342
343
  for (const sel of mcpSelections) {
343
- const srcFile = path.join(mcpSrc, sel);
344
+ // Try with and without .json extension
345
+ const srcFile = fs.existsSync(path.join(mcpSrc, sel + '.json'))
346
+ ? path.join(mcpSrc, sel + '.json')
347
+ : path.join(mcpSrc, sel);
344
348
  if (fs.existsSync(srcFile)) {
345
- fs.copyFileSync(srcFile, path.join(mcpDest, sel));
349
+ fs.copyFileSync(srcFile, path.join(mcpDest, sel + '.json'));
346
350
  mcpCount++;
351
+ // Parse config for .mcp.json generation
352
+ try {
353
+ const mcpConfig = JSON.parse(fs.readFileSync(srcFile, 'utf8'));
354
+ const serverEntry = {};
355
+ if (mcpConfig.command) serverEntry.command = mcpConfig.command;
356
+ if (mcpConfig.args) serverEntry.args = mcpConfig.args;
357
+ if (mcpConfig.env) serverEntry.env = mcpConfig.env;
358
+ if (mcpConfig.url) serverEntry.url = mcpConfig.url;
359
+ mcpServers[sel] = serverEntry;
360
+ } catch { /* skip invalid json */ }
347
361
  }
348
362
  }
349
363
  }
364
+ // Write .mcp.json at project root for Claude Code
365
+ if (Object.keys(mcpServers).length > 0) {
366
+ const projectRoot = path.dirname(targetDir); // targetDir is .claude/, project root is parent
367
+ const mcpJsonPath = path.join(projectRoot, '.mcp.json');
368
+ let existingMcp = {};
369
+ if (fs.existsSync(mcpJsonPath)) {
370
+ try { existingMcp = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8')); } catch { /* fresh */ }
371
+ }
372
+ if (!existingMcp.mcpServers) existingMcp.mcpServers = {};
373
+ // Merge — don't overwrite existing servers
374
+ for (const [name, config] of Object.entries(mcpServers)) {
375
+ if (!existingMcp.mcpServers[name]) {
376
+ existingMcp.mcpServers[name] = config;
377
+ }
378
+ }
379
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(existingMcp, null, 2) + '\n');
380
+ }
350
381
  }
351
382
 
352
383
  // 8. Write settings.json with full detection profile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erne-universal",
3
- "version": "0.10.8",
3
+ "version": "0.10.10",
4
4
  "description": "Complete AI coding agent harness for React Native and Expo development",
5
5
  "keywords": [
6
6
  "react-native",