erne-universal 0.5.0 → 0.5.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.
Files changed (36) hide show
  1. package/.cursorrules +1 -1
  2. package/.windsurfrules +1 -1
  3. package/AGENTS.md +1 -1
  4. package/GEMINI.md +1 -1
  5. package/agents/architect.md +1 -1
  6. package/agents/variants/architect/monorepo.md +1 -1
  7. package/agents/variants/architect/redux.md +1 -1
  8. package/dashboard/public/agents.js +1 -0
  9. package/dashboard/public/canvas.js +1 -0
  10. package/dashboard/public/overlay.js +7 -2
  11. package/dashboard/public/styles.css +2 -5
  12. package/dashboard/server.js +64 -3
  13. package/docs/agents.md +2 -0
  14. package/docs/getting-started.md +5 -1
  15. package/lib/claude-md.js +16 -0
  16. package/lib/dashboard.js +5 -1
  17. package/lib/generate.js +34 -9
  18. package/lib/init.js +121 -103
  19. package/package.json +7 -2
  20. package/rules/common/accessibility.md +70 -0
  21. package/rules/common/patterns.md +1 -1
  22. package/rules/common/performance.md +3 -3
  23. package/rules/common/security.md +1 -1
  24. package/skills/coding-standards/SKILL.md +3 -3
  25. package/docs/ANALYSIS/ERNE_ANALYSIS.md +0 -396
  26. package/docs/ANALYSIS/ERNE_UNIVERSAL_ANALYSIS.md +0 -517
  27. package/docs/superpowers/plans/2026-03-10-erne-plan-1-infrastructure-hooks.md +0 -3973
  28. package/docs/superpowers/plans/2026-03-10-erne-plan-2-content-layer.md +0 -4496
  29. package/docs/superpowers/plans/2026-03-10-erne-plan-3-skills-knowledge-base.md +0 -1952
  30. package/docs/superpowers/plans/2026-03-10-erne-plan-4-install-cli-distribution.md +0 -1624
  31. package/docs/superpowers/plans/2026-03-11-adaptive-init.md +0 -2043
  32. package/docs/superpowers/plans/2026-03-11-agent-dashboard.md +0 -1537
  33. package/docs/superpowers/plans/2026-03-11-dashboard-detail-and-history.md +0 -1322
  34. package/docs/superpowers/specs/2026-03-10-everything-react-native-expo-design.md +0 -581
  35. package/docs/superpowers/specs/2026-03-11-adaptive-init-design.md +0 -437
  36. package/docs/superpowers/specs/2026-03-11-agent-dashboard-design.md +0 -275
package/.cursorrules CHANGED
@@ -32,7 +32,7 @@ You are an expert React Native and Expo developer working in an ERNE-powered pro
32
32
  - Never hardcode secrets — use env vars
33
33
  - Validate all deep link parameters
34
34
  - SSL pinning for sensitive APIs
35
- - `expo-secure-store` for tokens, never AsyncStorage
35
+ - Use secure storage for tokens (expo-secure-store, react-native-keychain, etc.)
36
36
 
37
37
  # State Management
38
38
  - Zustand: UI state, preferences, navigation state
package/.windsurfrules CHANGED
@@ -32,7 +32,7 @@ You are an expert React Native and Expo developer working in an ERNE-powered pro
32
32
  - Never hardcode secrets — use env vars
33
33
  - Validate all deep link parameters
34
34
  - SSL pinning for sensitive APIs
35
- - `expo-secure-store` for tokens, never AsyncStorage
35
+ - Use secure storage for tokens (expo-secure-store, react-native-keychain, etc.)
36
36
 
37
37
  # State Management
38
38
  - Zustand: UI state, preferences, navigation state
package/AGENTS.md CHANGED
@@ -40,7 +40,7 @@ You are an expert React Native and Expo developer. Follow these conventions stri
40
40
  - Environment variables for all secrets (never hardcode)
41
41
  - Validate and sanitize deep link parameters
42
42
  - SSL certificate pinning for sensitive endpoints
43
- - `expo-secure-store` for tokens never use AsyncStorage for secrets
43
+ - Use secure storage for tokens (expo-secure-store, react-native-keychain, etc.)
44
44
  - Enable ProGuard/R8 for Android release builds
45
45
 
46
46
  ### Git Workflow
package/GEMINI.md CHANGED
@@ -34,7 +34,7 @@ You are an expert React Native and Expo developer working in an ERNE-powered pro
34
34
  - Never hardcode secrets — use env vars
35
35
  - Validate all deep link parameters
36
36
  - SSL pinning for sensitive APIs
37
- - `expo-secure-store` for tokens, never AsyncStorage
37
+ - Use secure storage for tokens (expo-secure-store, react-native-keychain, etc.)
38
38
 
39
39
  ## State Management
40
40
  - Zustand: UI state, preferences, navigation state
@@ -13,7 +13,7 @@ Design feature architectures, navigation flows, and system structure for React N
13
13
 
14
14
  - **Feature decomposition**: Break complex features into implementable units with clear interfaces
15
15
  - **Navigation design**: Design Expo Router file-based layouts, tab structures, modal patterns, deep linking
16
- - **State management selection**: Recommend Zustand (client), TanStack Query (server), or Jotai (atomic) based on requirements
16
+ - **State management selection**: Recommend Zustand (client) + TanStack Query (server), or Redux Toolkit for complex state requirements
17
17
  - **API layer planning**: Design data fetching patterns, caching strategies, optimistic updates
18
18
  - **Monorepo structure**: Organize shared packages, platform-specific code, config management
19
19
 
@@ -13,7 +13,7 @@ Design feature architectures, navigation flows, and system structure for React N
13
13
 
14
14
  - **Feature decomposition**: Break complex features into implementable units with clear interfaces
15
15
  - **Navigation design**: Design Expo Router file-based layouts, tab structures, modal patterns, deep linking
16
- - **State management selection**: Zustand for client state, TanStack Query for server state — no other state management libraries
16
+ - **State management selection**: Recommend appropriate state management for the project (Zustand, Redux Toolkit, etc.) with TanStack Query for server state
17
17
  - **API layer planning**: Design data fetching patterns, caching strategies, optimistic updates
18
18
  - **Monorepo architecture**: Design workspace structure, shared packages, cross-package boundaries
19
19
 
@@ -13,7 +13,7 @@ Design feature architectures, navigation flows, and system structure for React N
13
13
 
14
14
  - **Feature decomposition**: Break complex features into implementable units with clear interfaces
15
15
  - **Navigation design**: Design navigation flows, tab structures, modal patterns, deep linking
16
- - **State management**: Redux Toolkit for all state `createSlice` for domain state, `createAsyncThunk` for async operations, `createSelector` for derived data
16
+ - **State management**: Redux Toolkit for state management. Use RTK Query for server state, or Redux Saga for complex async flows. `createSlice` for domain state, `createSelector` for derived data.
17
17
  - **API layer planning**: Design data fetching patterns with RTK createAsyncThunk, caching with RTK Query (optional), optimistic updates in reducers
18
18
  - **Monorepo structure**: Organize shared packages, platform-specific code, config management
19
19
 
@@ -393,6 +393,7 @@
393
393
  };
394
394
 
395
395
  const drawAgentSprites = (ctx) => {
396
+ if (!ctx) return;
396
397
  for (const [name, sprite] of Object.entries(agentSprites)) {
397
398
  // Use moving row while walking, otherwise status row
398
399
  const row = sprite.isMoving
@@ -662,6 +662,7 @@
662
662
  };
663
663
 
664
664
  const drawOffice = (ctx, agents) => {
665
+ if (!ctx) return;
665
666
  // Background
666
667
  ctx.fillStyle = '#0a0a1a';
667
668
  ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
@@ -114,15 +114,20 @@
114
114
  }
115
115
 
116
116
  var html = '';
117
+ var validTypes = { start: 1, complete: 1, planning: 1, timeout: 1 };
117
118
  for (var i = entries.length - 1; i >= 0; i--) {
118
119
  var e = entries[i];
120
+ var dotClass = validTypes[e.type] ? e.type : 'unknown';
121
+ var typeLabel = e.type === 'complete' ? 'Completed '
122
+ : e.type === 'timeout' ? 'Timed out '
123
+ : 'Started ';
119
124
  html +=
120
125
  '<div class="history-entry">' +
121
- '<div class="history-dot ' + e.type + '"></div>' +
126
+ '<div class="history-dot ' + dotClass + '"></div>' +
122
127
  '<div class="history-info">' +
123
128
  '<div class="history-task">' + escapeHtml(e.task || 'Unknown task') + '</div>' +
124
129
  '<div class="history-time">' +
125
- (e.type === 'complete' ? 'Completed ' : 'Started ') +
130
+ typeLabel +
126
131
  History.formatRelativeTime(e.timestamp) +
127
132
  ' at ' + History.formatTime(e.timestamp) +
128
133
  '</div>' +
@@ -83,8 +83,8 @@ canvas {
83
83
  .connection-dot.live { background: var(--green); }
84
84
 
85
85
  .panel-agents {
86
- display: flex;
87
- flex-wrap: wrap;
86
+ display: grid;
87
+ grid-template-columns: repeat(5, 1fr);
88
88
  overflow-y: auto;
89
89
  scrollbar-width: thin;
90
90
  scrollbar-color: var(--border) transparent;
@@ -99,9 +99,6 @@ canvas {
99
99
  cursor: pointer;
100
100
  transition: background 0.15s;
101
101
  border-radius: 4px;
102
- min-width: 200px;
103
- flex: 1 1 200px;
104
- max-width: 300px;
105
102
  }
106
103
 
107
104
  .agent-row:hover { background: rgba(255,255,255,0.05); }
@@ -32,6 +32,38 @@ const AGENT_DEFINITIONS = [
32
32
  { name: 'feature-builder', room: 'development' },
33
33
  ];
34
34
 
35
+ // Rate limiting — 60 requests per minute per IP
36
+ const RATE_LIMIT_WINDOW_MS = 60 * 1000;
37
+ const RATE_LIMIT_MAX = 60;
38
+ const rateLimitMap = new Map();
39
+
40
+ const isRateLimited = (ip) => {
41
+ const now = Date.now();
42
+ const entry = rateLimitMap.get(ip);
43
+ if (!entry) {
44
+ rateLimitMap.set(ip, { timestamps: [now] });
45
+ return false;
46
+ }
47
+ // Prune timestamps outside the window
48
+ entry.timestamps = entry.timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
49
+ if (entry.timestamps.length >= RATE_LIMIT_MAX) {
50
+ return true;
51
+ }
52
+ entry.timestamps.push(now);
53
+ return false;
54
+ };
55
+
56
+ // Periodically clean up stale rate limit entries
57
+ setInterval(() => {
58
+ const now = Date.now();
59
+ for (const [ip, entry] of rateLimitMap) {
60
+ entry.timestamps = entry.timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
61
+ if (entry.timestamps.length === 0) {
62
+ rateLimitMap.delete(ip);
63
+ }
64
+ }
65
+ }, RATE_LIMIT_WINDOW_MS);
66
+
35
67
  const agentState = {};
36
68
 
37
69
  const initAgentState = () => {
@@ -185,6 +217,12 @@ setInterval(() => {
185
217
  if (agent.status !== 'idle' && agent.lastEvent) {
186
218
  const elapsed = now - new Date(agent.lastEvent).getTime();
187
219
  if (elapsed > AGENT_TIMEOUT_MS) {
220
+ const timeoutNow = new Date().toISOString();
221
+ addHistoryEntry(name, {
222
+ type: 'timeout',
223
+ task: agent.task,
224
+ timestamp: timeoutNow,
225
+ });
188
226
  agent.status = 'idle';
189
227
  agent.task = null;
190
228
  agent.startedAt = null;
@@ -193,14 +231,26 @@ setInterval(() => {
193
231
  }
194
232
  }
195
233
  if (changed) {
234
+ persistHistory();
196
235
  broadcastState();
197
236
  }
198
237
  }, TIMEOUT_CHECK_INTERVAL_MS);
199
238
 
239
+ const MAX_PAYLOAD_BYTES = 64 * 1024; // 64KB
240
+
200
241
  const parseBody = (req) =>
201
242
  new Promise((resolve, reject) => {
202
243
  const chunks = [];
203
- req.on('data', (chunk) => chunks.push(chunk));
244
+ let totalBytes = 0;
245
+ req.on('data', (chunk) => {
246
+ totalBytes += chunk.length;
247
+ if (totalBytes > MAX_PAYLOAD_BYTES) {
248
+ req.destroy();
249
+ reject(new RangeError('Payload too large'));
250
+ return;
251
+ }
252
+ chunks.push(chunk);
253
+ });
204
254
  req.on('end', () => {
205
255
  try {
206
256
  resolve(JSON.parse(Buffer.concat(chunks).toString()));
@@ -253,6 +303,12 @@ const server = http.createServer(async (req, res) => {
253
303
  }
254
304
 
255
305
  if (req.method === 'POST' && req.url === '/api/events') {
306
+ const clientIp = req.socket.remoteAddress || 'unknown';
307
+ if (isRateLimited(clientIp)) {
308
+ res.writeHead(429, { 'Content-Type': 'application/json' });
309
+ res.end(JSON.stringify({ error: 'Too many requests' }));
310
+ return;
311
+ }
256
312
  try {
257
313
  const body = await parseBody(req);
258
314
  const result = handleEvent(body);
@@ -265,8 +321,13 @@ const server = http.createServer(async (req, res) => {
265
321
  res.end(JSON.stringify(result));
266
322
  }
267
323
  } catch (e) {
268
- res.writeHead(400, { 'Content-Type': 'application/json' });
269
- res.end(JSON.stringify({ error: 'Invalid JSON' }));
324
+ if (e instanceof RangeError && e.message === 'Payload too large') {
325
+ res.writeHead(413, { 'Content-Type': 'application/json' });
326
+ res.end(JSON.stringify({ error: 'Payload too large' }));
327
+ } else {
328
+ res.writeHead(400, { 'Content-Type': 'application/json' });
329
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
330
+ }
270
331
  }
271
332
  return;
272
333
  }
package/docs/agents.md CHANGED
@@ -14,6 +14,8 @@ Agents are specialized AI personas with focused expertise. Each agent has specif
14
14
  | expo-config-resolver | EAS, app.config, build fixes | /build-fix, /deploy |
15
15
  | ui-designer | NativeWind, Reanimated, components | /animate, /component |
16
16
  | upgrade-assistant | Version migrations, breaking changes | /upgrade |
17
+ | senior-developer | End-to-end feature implementation, screens, hooks, API, state | /code, /feature, /plan |
18
+ | feature-builder | Focused feature units, works in parallel with senior-developer | /code, /feature, /component |
17
19
 
18
20
  ## How Agents Work
19
21
 
@@ -14,11 +14,15 @@ The installer will:
14
14
  3. Let you select MCP integrations (agent-device, GitHub, etc.)
15
15
  4. Generate all configuration files in `.claude/`
16
16
 
17
+ ### Adaptive Init System
18
+
19
+ ERNE's `init` command deep-scans your project across 15 stack dimensions — navigation, state management, styling, lists, images, forms, storage, testing, and more. Based on what it finds, it selects from 24 variant templates to generate rules, agents, and hooks tailored to your exact stack. This means a Zustand + Expo Router project gets different guidance than a Redux Toolkit + React Navigation one, with no manual configuration required.
20
+
17
21
  ## What Gets Installed
18
22
 
19
23
  ```
20
24
  .claude/
21
- agents/ # 8 specialized AI agents
25
+ agents/ # 10 specialized AI agents
22
26
  rules/ # 25 coding standard rules (layered by platform)
23
27
  commands/ # 16 slash commands
24
28
  contexts/ # 3 behavior modes (dev, review, vibe)
package/lib/claude-md.js CHANGED
@@ -297,6 +297,22 @@ function handleClaudeMd(cwd, detection, profile, ruleLayers) {
297
297
  return 'regenerated';
298
298
  }
299
299
 
300
+ // Check if ERNE section was already appended (double-run protection)
301
+ if (existingContent.includes('# ERNE Configuration') || existingContent.includes('@import .claude/rules/')) {
302
+ // Treat as regenerate: strip old ERNE section and re-append
303
+ const erneSeparator = existingContent.indexOf('\n---\n\n# ERNE Configuration');
304
+ if (erneSeparator !== -1) {
305
+ const originalContent = existingContent.substring(0, erneSeparator);
306
+ const appendSection = generateAppendSection(ruleLayers);
307
+ fs.writeFileSync(claudeMdPath, originalContent + appendSection);
308
+ return 'regenerated';
309
+ }
310
+ // If we can't find the separator cleanly, regenerate fully
311
+ const content = generateFullClaudeMd(cwd, detection, ruleLayers);
312
+ fs.writeFileSync(claudeMdPath, content);
313
+ return 'regenerated';
314
+ }
315
+
300
316
  // Scenario B: Existing non-ERNE CLAUDE.md — backup + append
301
317
  if (!fs.existsSync(backupPath)) {
302
318
  fs.writeFileSync(backupPath, existingContent);
package/lib/dashboard.js CHANGED
@@ -9,7 +9,7 @@ const path = require('path');
9
9
  const readline = require('readline/promises');
10
10
  const { stdin, stdout } = require('process');
11
11
 
12
- const HOOKS_PATH = path.resolve(__dirname, '..', 'hooks', 'hooks.json');
12
+ const HOOKS_PATH = path.join(process.cwd(), '.claude', 'hooks.json');
13
13
  const SERVER_PATH = path.resolve(__dirname, '..', 'dashboard', 'server.js');
14
14
 
15
15
  function parseArgs(argv) {
@@ -104,6 +104,10 @@ function openBrowser(url) {
104
104
  execFileSync('open', [url]);
105
105
  } else if (platform === 'linux') {
106
106
  execFileSync('xdg-open', [url]);
107
+ } else if (platform === 'win32') {
108
+ execFileSync('cmd', ['/c', 'start', url]);
109
+ } else {
110
+ console.log(' Open in browser: ' + url);
107
111
  }
108
112
  } catch {
109
113
  // Silently ignore — browser open is best-effort
package/lib/generate.js CHANGED
@@ -144,10 +144,17 @@ function selectVariant(targetPath, detection) {
144
144
 
145
145
  // ─── determineRuleLayers ───────────────────────────────────────────────────────
146
146
 
147
- function determineRuleLayers(detection) {
147
+ function determineRuleLayers(detection, cwd) {
148
148
  const layers = ['common'];
149
149
  if (detection.framework === 'expo-managed' || detection.framework === 'expo-bare') layers.push('expo');
150
150
  if (detection.framework === 'bare-rn' || detection.framework === 'expo-bare') layers.push('bare-rn');
151
+
152
+ // Add native rule layers for bare projects with native directories
153
+ if (cwd && (detection.framework === 'bare-rn' || detection.framework === 'expo-bare')) {
154
+ if (fs.existsSync(path.join(cwd, 'ios'))) layers.push('native-ios');
155
+ if (fs.existsSync(path.join(cwd, 'android'))) layers.push('native-android');
156
+ }
157
+
151
158
  return layers;
152
159
  }
153
160
 
@@ -178,15 +185,30 @@ function mergeHookProfile(masterHooks, profileHooks, profileName) {
178
185
  // ─── Helper: copyDir ───────────────────────────────────────────────────────────
179
186
 
180
187
  function copyDir(src, dest) {
181
- fs.mkdirSync(dest, { recursive: true });
182
- const entries = fs.readdirSync(src, { withFileTypes: true });
188
+ try {
189
+ fs.mkdirSync(dest, { recursive: true });
190
+ } catch (err) {
191
+ console.error(` ✗ Failed to create directory ${dest}: ${err.message}`);
192
+ return;
193
+ }
194
+ let entries;
195
+ try {
196
+ entries = fs.readdirSync(src, { withFileTypes: true });
197
+ } catch (err) {
198
+ console.error(` ✗ Failed to read ${src}: ${err.message}`);
199
+ return;
200
+ }
183
201
  for (const entry of entries) {
184
202
  const srcPath = path.join(src, entry.name);
185
203
  const destPath = path.join(dest, entry.name);
186
- if (entry.isDirectory()) {
187
- copyDir(srcPath, destPath);
188
- } else {
189
- fs.copyFileSync(srcPath, destPath);
204
+ try {
205
+ if (entry.isDirectory()) {
206
+ copyDir(srcPath, destPath);
207
+ } else {
208
+ fs.copyFileSync(srcPath, destPath);
209
+ }
210
+ } catch (err) {
211
+ console.error(` ✗ Failed to copy ${srcPath}: ${err.message}`);
190
212
  }
191
213
  }
192
214
  }
@@ -200,7 +222,9 @@ function safeDelete(filePath) {
200
222
  // ─── generateConfig ────────────────────────────────────────────────────────────
201
223
 
202
224
  function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections) {
203
- const ruleLayers = determineRuleLayers(detection);
225
+ // Derive project root (cwd) from targetDir (.claude dir is one level inside project)
226
+ const cwd = path.dirname(targetDir);
227
+ const ruleLayers = determineRuleLayers(detection, cwd);
204
228
  let mcpCount = 0;
205
229
 
206
230
  // 1. Copy universal content (commands, contexts, skills, hook scripts)
@@ -288,7 +312,7 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
288
312
 
289
313
  // 7. Copy MCP configs
290
314
  if (mcpSelections && mcpSelections.length > 0) {
291
- const mcpSrc = path.join(erneRoot, 'mcp');
315
+ const mcpSrc = path.join(erneRoot, 'mcp-configs');
292
316
  const mcpDest = path.join(targetDir, 'mcp');
293
317
  if (fs.existsSync(mcpSrc)) {
294
318
  fs.mkdirSync(mcpDest, { recursive: true });
@@ -305,6 +329,7 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
305
329
  // 8. Write settings.json with full detection profile
306
330
  const settingsPath = path.join(targetDir, 'settings.json');
307
331
  const settings = {
332
+ erneVersion: require('../package.json').version,
308
333
  detection,
309
334
  profile,
310
335
  ruleLayers,
package/lib/init.js CHANGED
@@ -64,6 +64,23 @@ function parseArgs() {
64
64
  }
65
65
  }
66
66
 
67
+ // Validate --profile value
68
+ const validProfileValues = ['minimal', 'standard', 'strict'];
69
+ if (opts.profile && !validProfileValues.includes(opts.profile)) {
70
+ console.error(`Invalid profile: "${opts.profile}". Valid profiles: ${validProfileValues.join(', ')}`);
71
+ process.exit(1);
72
+ }
73
+
74
+ // Validate --mcp values
75
+ const validMcpKeys = ['agent-device', 'github', 'supabase', 'firebase', 'figma', 'sentry'];
76
+ if (opts.mcp !== null && Array.isArray(opts.mcp)) {
77
+ const invalid = opts.mcp.filter(k => !validMcpKeys.includes(k));
78
+ if (invalid.length > 0) {
79
+ console.error(`Invalid MCP server(s): ${invalid.join(', ')}. Valid options: ${validMcpKeys.join(', ')}`);
80
+ process.exit(1);
81
+ }
82
+ }
83
+
67
84
  return opts;
68
85
  }
69
86
 
@@ -119,121 +136,122 @@ module.exports = async function init() {
119
136
 
120
137
  const cwd = process.cwd();
121
138
 
122
- console.log('\n erne — Setting up AI agent harness for React Native & Expo\n');
123
-
124
- // ─── Step 1: Detect project type ───
125
- console.log(' Scanning project...');
126
- const detection = detectProject(cwd);
127
- printDetectionReport(detection);
128
-
129
- if (!detection.isRNProject) {
130
- if (nonInteractive) {
131
- console.log('\n ⚠ No React Native project detected — continuing (non-interactive mode).');
132
- } else {
133
- console.log('\n ⚠ No React Native project detected in current directory.');
134
- const proceed = await rl.question(' Continue anyway? (y/N) ');
135
- if (proceed.toLowerCase() !== 'y') {
136
- console.log(' Aborted.');
137
- rl.close();
138
- return;
139
+ try {
140
+ console.log('\n erne — Setting up AI agent harness for React Native & Expo\n');
141
+
142
+ // ─── Step 1: Detect project type ───
143
+ console.log(' Scanning project...');
144
+ const detection = detectProject(cwd);
145
+ printDetectionReport(detection);
146
+
147
+ if (!detection.isRNProject) {
148
+ if (nonInteractive) {
149
+ console.log('\n ⚠ No React Native project detected — continuing (non-interactive mode).');
150
+ } else {
151
+ console.log('\n No React Native project detected in current directory.');
152
+ const proceed = await rl.question(' Continue anyway? (y/N) ');
153
+ if (proceed.toLowerCase() !== 'y') {
154
+ console.log(' Aborted.');
155
+ return;
156
+ }
139
157
  }
140
158
  }
141
- }
142
159
 
143
- // ─── Step 2: Choose hook profile ───
144
- let profile;
145
- const validProfiles = ['minimal', 'standard', 'strict'];
146
-
147
- if (opts.profile && validProfiles.includes(opts.profile)) {
148
- profile = opts.profile;
149
- console.log(`\n Step 2: Hook profile: ${profile} (from --profile flag)`);
150
- } else if (nonInteractive) {
151
- profile = 'standard';
152
- console.log('\n Step 2: Hook profile: standard (default)');
153
- } else {
154
- console.log('\n Step 2: Select hook profile:\n');
155
- console.log(' (a) minimal — fast iteration, minimal checks');
156
- console.log(' (b) standard — balanced quality + speed [recommended]');
157
- console.log(' (c) strict — production-grade enforcement');
158
- console.log();
159
-
160
- let profileChoice = await rl.question(' Profile (a/b/c) [b]: ');
161
- profileChoice = profileChoice.toLowerCase() || 'b';
162
- const profileMap = { a: 'minimal', b: 'standard', c: 'strict' };
163
- profile = profileMap[profileChoice] || 'standard';
164
- }
160
+ // ─── Step 2: Choose hook profile ───
161
+ let profile;
162
+ const validProfiles = ['minimal', 'standard', 'strict'];
165
163
 
166
- // ─── Step 3: Select MCP integrations ───
167
- const mcpSelections = {};
168
- const allMcpKeys = ['agent-device', 'github', 'supabase', 'firebase', 'figma', 'sentry'];
169
- const defaultMcpKeys = ['agent-device', 'github'];
170
-
171
- if (opts.noMcp) {
172
- console.log('\n Step 3: MCP servers: none (--no-mcp)');
173
- for (const key of allMcpKeys) mcpSelections[key] = false;
174
- } else if (opts.mcp !== null) {
175
- console.log(`\n Step 3: MCP servers: ${opts.mcp.join(', ') || 'none'} (from --mcp flag)`);
176
- for (const key of allMcpKeys) mcpSelections[key] = opts.mcp.includes(key);
177
- } else if (opts.yes) {
178
- console.log(`\n Step 3: MCP servers: ${defaultMcpKeys.join(', ')} (defaults)`);
179
- for (const key of allMcpKeys) mcpSelections[key] = defaultMcpKeys.includes(key);
180
- } else {
181
- console.log('\n Step 3: MCP server integrations:\n');
182
-
183
- // Recommended servers
184
- console.log(' Recommended:');
185
- const agentDevice = await rl.question(' [Y/n] agent-device — Control iOS Simulator & Android Emulator: ');
186
- mcpSelections['agent-device'] = agentDevice.toLowerCase() !== 'n';
187
-
188
- const github = await rl.question(' [Y/n] GitHub — PR management, issue tracking: ');
189
- mcpSelections['github'] = github.toLowerCase() !== 'n';
190
-
191
- // Optional servers
192
- console.log('\n Optional (press Enter to skip):');
193
- const optionalServers = [
194
- { key: 'supabase', label: 'Supabase — Database & auth' },
195
- { key: 'firebase', label: 'Firebase — Analytics & push' },
196
- { key: 'figma', label: 'Figma — Design token sync' },
197
- { key: 'sentry', label: 'Sentry — Error tracking' },
198
- ];
199
-
200
- for (const server of optionalServers) {
201
- const answer = await rl.question(` [y/N] ${server.label}: `);
202
- mcpSelections[server.key] = answer.toLowerCase() === 'y';
164
+ if (opts.profile && validProfiles.includes(opts.profile)) {
165
+ profile = opts.profile;
166
+ console.log(`\n Step 2: Hook profile: ${profile} (from --profile flag)`);
167
+ } else if (nonInteractive) {
168
+ profile = 'standard';
169
+ console.log('\n Step 2: Hook profile: standard (default)');
170
+ } else {
171
+ console.log('\n Step 2: Select hook profile:\n');
172
+ console.log(' (a) minimal fast iteration, minimal checks');
173
+ console.log(' (b) standard balanced quality + speed [recommended]');
174
+ console.log(' (c) strict — production-grade enforcement');
175
+ console.log();
176
+
177
+ let profileChoice = await rl.question(' Profile (a/b/c) [b]: ');
178
+ profileChoice = profileChoice.toLowerCase() || 'b';
179
+ const profileMap = { a: 'minimal', b: 'standard', c: 'strict' };
180
+ profile = profileMap[profileChoice] || 'standard';
203
181
  }
204
- }
205
182
 
206
- if (rl) rl.close();
183
+ // ─── Step 3: Select MCP integrations ───
184
+ const mcpSelections = {};
185
+ const allMcpKeys = ['agent-device', 'github', 'supabase', 'firebase', 'figma', 'sentry'];
186
+ const defaultMcpKeys = ['agent-device', 'github'];
187
+
188
+ if (opts.noMcp) {
189
+ console.log('\n Step 3: MCP servers: none (--no-mcp)');
190
+ for (const key of allMcpKeys) mcpSelections[key] = false;
191
+ } else if (opts.mcp !== null) {
192
+ console.log(`\n Step 3: MCP servers: ${opts.mcp.join(', ') || 'none'} (from --mcp flag)`);
193
+ for (const key of allMcpKeys) mcpSelections[key] = opts.mcp.includes(key);
194
+ } else if (opts.yes) {
195
+ console.log(`\n Step 3: MCP servers: ${defaultMcpKeys.join(', ')} (defaults)`);
196
+ for (const key of allMcpKeys) mcpSelections[key] = defaultMcpKeys.includes(key);
197
+ } else {
198
+ console.log('\n Step 3: MCP server integrations:\n');
199
+
200
+ // Recommended servers
201
+ console.log(' Recommended:');
202
+ const agentDevice = await rl.question(' [Y/n] agent-device — Control iOS Simulator & Android Emulator: ');
203
+ mcpSelections['agent-device'] = agentDevice.toLowerCase() !== 'n';
204
+
205
+ const github = await rl.question(' [Y/n] GitHub — PR management, issue tracking: ');
206
+ mcpSelections['github'] = github.toLowerCase() !== 'n';
207
+
208
+ // Optional servers
209
+ console.log('\n Optional (press Enter to skip):');
210
+ const optionalServers = [
211
+ { key: 'supabase', label: 'Supabase — Database & auth' },
212
+ { key: 'firebase', label: 'Firebase — Analytics & push' },
213
+ { key: 'figma', label: 'Figma — Design token sync' },
214
+ { key: 'sentry', label: 'Sentry — Error tracking' },
215
+ ];
216
+
217
+ for (const server of optionalServers) {
218
+ const answer = await rl.question(` [y/N] ${server.label}: `);
219
+ mcpSelections[server.key] = answer.toLowerCase() === 'y';
220
+ }
221
+ }
207
222
 
208
- // ─── Step 4: Generate config ───
209
- console.log('\n Step 4: Generating configuration...\n');
223
+ // ─── Step 4: Generate config ───
224
+ console.log('\n Step 4: Generating configuration...\n');
210
225
 
211
- const erneRoot = path.resolve(__dirname, '..');
212
- const claudeDir = path.join(cwd, '.claude');
226
+ const erneRoot = path.resolve(__dirname, '..');
227
+ const claudeDir = path.join(cwd, '.claude');
213
228
 
214
- // Convert mcpSelections object to array of enabled keys
215
- const enabledMcp = Object.entries(mcpSelections)
216
- .filter(([, enabled]) => enabled)
217
- .map(([key]) => key);
229
+ // Convert mcpSelections object to array of enabled keys
230
+ const enabledMcp = Object.entries(mcpSelections)
231
+ .filter(([, enabled]) => enabled)
232
+ .map(([key]) => key);
218
233
 
219
- const { ruleLayers, mcpCount } = generateConfig(erneRoot, claudeDir, detection, profile, enabledMcp);
234
+ const { ruleLayers, mcpCount } = generateConfig(erneRoot, claudeDir, detection, profile, enabledMcp);
220
235
 
221
- // Print what was generated
222
- console.log(` ✓ .claude/ (agents, commands, rules, skills, contexts, hooks)`);
223
- console.log(` ✓ .claude/rules/ (layers: ${ruleLayers.join(', ')})`);
224
- console.log(` ✓ .claude/hooks.json (${profile} profile)`);
225
- console.log(` ✓ .claude/mcp/ (${mcpCount} servers)`);
226
- console.log(' ✓ .claude/settings.json');
236
+ // Print what was generated
237
+ console.log(` ✓ .claude/ (agents, commands, rules, skills, contexts, hooks)`);
238
+ console.log(` ✓ .claude/rules/ (layers: ${ruleLayers.join(', ')})`);
239
+ console.log(` ✓ .claude/hooks.json (${profile} profile)`);
240
+ console.log(` ✓ .claude/mcp/ (${mcpCount} servers)`);
241
+ console.log(' ✓ .claude/settings.json');
227
242
 
228
- // Handle CLAUDE.md
229
- const claudeMdResult = handleClaudeMd(cwd, detection, profile, ruleLayers);
243
+ // Handle CLAUDE.md
244
+ const claudeMdResult = handleClaudeMd(cwd, detection, profile, ruleLayers);
230
245
 
231
- const claudeMdMessages = {
232
- appended: 'CLAUDE.md (appended — original backed up to CLAUDE.md.pre-erne)',
233
- regenerated: 'CLAUDE.md (regenerated for detected stack)',
234
- generated: 'CLAUDE.md (generated for detected stack)',
235
- };
236
- console.log(` ✓ ${claudeMdMessages[claudeMdResult]}`);
246
+ const claudeMdMessages = {
247
+ appended: 'CLAUDE.md (appended — original backed up to CLAUDE.md.pre-erne)',
248
+ regenerated: 'CLAUDE.md (regenerated for detected stack)',
249
+ generated: 'CLAUDE.md (generated for detected stack)',
250
+ };
251
+ console.log(` ✓ ${claudeMdMessages[claudeMdResult]}`);
237
252
 
238
- console.log('\n Done! Run /plan to start your first feature.\n');
253
+ console.log('\n Done! Run /plan to start your first feature.\n');
254
+ } finally {
255
+ if (rl) rl.close();
256
+ }
239
257
  };