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.
- package/.cursorrules +1 -1
- package/.windsurfrules +1 -1
- package/AGENTS.md +1 -1
- package/GEMINI.md +1 -1
- package/agents/architect.md +1 -1
- package/agents/variants/architect/monorepo.md +1 -1
- package/agents/variants/architect/redux.md +1 -1
- package/dashboard/public/agents.js +1 -0
- package/dashboard/public/canvas.js +1 -0
- package/dashboard/public/overlay.js +7 -2
- package/dashboard/public/styles.css +2 -5
- package/dashboard/server.js +64 -3
- package/docs/agents.md +2 -0
- package/docs/getting-started.md +5 -1
- package/lib/claude-md.js +16 -0
- package/lib/dashboard.js +5 -1
- package/lib/generate.js +34 -9
- package/lib/init.js +121 -103
- package/package.json +7 -2
- package/rules/common/accessibility.md +70 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/performance.md +3 -3
- package/rules/common/security.md +1 -1
- package/skills/coding-standards/SKILL.md +3 -3
- package/docs/ANALYSIS/ERNE_ANALYSIS.md +0 -396
- package/docs/ANALYSIS/ERNE_UNIVERSAL_ANALYSIS.md +0 -517
- package/docs/superpowers/plans/2026-03-10-erne-plan-1-infrastructure-hooks.md +0 -3973
- package/docs/superpowers/plans/2026-03-10-erne-plan-2-content-layer.md +0 -4496
- package/docs/superpowers/plans/2026-03-10-erne-plan-3-skills-knowledge-base.md +0 -1952
- package/docs/superpowers/plans/2026-03-10-erne-plan-4-install-cli-distribution.md +0 -1624
- package/docs/superpowers/plans/2026-03-11-adaptive-init.md +0 -2043
- package/docs/superpowers/plans/2026-03-11-agent-dashboard.md +0 -1537
- package/docs/superpowers/plans/2026-03-11-dashboard-detail-and-history.md +0 -1322
- package/docs/superpowers/specs/2026-03-10-everything-react-native-expo-design.md +0 -581
- package/docs/superpowers/specs/2026-03-11-adaptive-init-design.md +0 -437
- 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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
package/agents/architect.md
CHANGED
|
@@ -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)
|
|
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**:
|
|
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
|
|
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
|
|
|
@@ -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 ' +
|
|
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
|
-
|
|
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:
|
|
87
|
-
|
|
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); }
|
package/dashboard/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
|
package/docs/getting-started.md
CHANGED
|
@@ -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/ #
|
|
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.
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
223
|
+
// ─── Step 4: Generate config ───
|
|
224
|
+
console.log('\n Step 4: Generating configuration...\n');
|
|
210
225
|
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
const erneRoot = path.resolve(__dirname, '..');
|
|
227
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
213
228
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
234
|
+
const { ruleLayers, mcpCount } = generateConfig(erneRoot, claudeDir, detection, profile, enabledMcp);
|
|
220
235
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
243
|
+
// Handle CLAUDE.md
|
|
244
|
+
const claudeMdResult = handleClaudeMd(cwd, detection, profile, ruleLayers);
|
|
230
245
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
253
|
+
console.log('\n Done! Run /plan to start your first feature.\n');
|
|
254
|
+
} finally {
|
|
255
|
+
if (rl) rl.close();
|
|
256
|
+
}
|
|
239
257
|
};
|