erne-universal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +92 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/agents/architect.md +64 -0
- package/agents/code-reviewer.md +72 -0
- package/agents/expo-config-resolver.md +77 -0
- package/agents/native-bridge-builder.md +98 -0
- package/agents/performance-profiler.md +89 -0
- package/agents/tdd-guide.md +86 -0
- package/agents/ui-designer.md +100 -0
- package/agents/upgrade-assistant.md +106 -0
- package/bin/cli.js +55 -0
- package/commands/animate.md +70 -0
- package/commands/build-fix.md +57 -0
- package/commands/code-review.md +51 -0
- package/commands/component.md +93 -0
- package/commands/debug.md +74 -0
- package/commands/deploy.md +82 -0
- package/commands/learn.md +56 -0
- package/commands/native-module.md +51 -0
- package/commands/navigate.md +69 -0
- package/commands/perf.md +68 -0
- package/commands/plan.md +49 -0
- package/commands/quality-gate.md +80 -0
- package/commands/retrospective.md +70 -0
- package/commands/setup-device.md +99 -0
- package/commands/tdd.md +51 -0
- package/commands/upgrade.md +78 -0
- package/contexts/dev.md +29 -0
- package/contexts/review.md +32 -0
- package/contexts/vibe.md +44 -0
- package/docs/agents.md +41 -0
- package/docs/commands.md +53 -0
- package/docs/creating-skills.md +63 -0
- package/docs/getting-started.md +60 -0
- package/docs/hooks-profiles.md +73 -0
- package/docs/superpowers/plans/2026-03-10-erne-plan-1-infrastructure-hooks.md +3973 -0
- package/docs/superpowers/plans/2026-03-10-erne-plan-2-content-layer.md +4496 -0
- package/docs/superpowers/plans/2026-03-10-erne-plan-3-skills-knowledge-base.md +1952 -0
- package/docs/superpowers/plans/2026-03-10-erne-plan-4-install-cli-distribution.md +1624 -0
- package/docs/superpowers/specs/2026-03-10-everything-react-native-expo-design.md +581 -0
- package/examples/claude-md-bare-rn.md +46 -0
- package/examples/claude-md-expo-managed.md +45 -0
- package/examples/eas-json-standard.json +41 -0
- package/hooks/hooks.json +113 -0
- package/hooks/profiles/minimal.json +9 -0
- package/hooks/profiles/standard.json +17 -0
- package/hooks/profiles/strict.json +22 -0
- package/install.sh +50 -0
- package/mcp-configs/agent-device.json +10 -0
- package/mcp-configs/appstore-connect.json +15 -0
- package/mcp-configs/expo-api.json +13 -0
- package/mcp-configs/figma.json +13 -0
- package/mcp-configs/firebase.json +14 -0
- package/mcp-configs/github.json +13 -0
- package/mcp-configs/memory.json +13 -0
- package/mcp-configs/play-console.json +14 -0
- package/mcp-configs/sentry.json +15 -0
- package/mcp-configs/supabase.json +14 -0
- package/package.json +50 -0
- package/rules/bare-rn/coding-style.md +62 -0
- package/rules/bare-rn/patterns.md +54 -0
- package/rules/bare-rn/security.md +58 -0
- package/rules/bare-rn/testing.md +78 -0
- package/rules/common/coding-style.md +50 -0
- package/rules/common/development-workflow.md +55 -0
- package/rules/common/git-workflow.md +40 -0
- package/rules/common/navigation.md +56 -0
- package/rules/common/patterns.md +59 -0
- package/rules/common/performance.md +55 -0
- package/rules/common/security.md +64 -0
- package/rules/common/state-management.md +86 -0
- package/rules/common/testing.md +61 -0
- package/rules/expo/coding-style.md +54 -0
- package/rules/expo/patterns.md +71 -0
- package/rules/expo/security.md +41 -0
- package/rules/expo/testing.md +68 -0
- package/rules/native-android/coding-style.md +81 -0
- package/rules/native-android/patterns.md +77 -0
- package/rules/native-android/security.md +80 -0
- package/rules/native-android/testing.md +94 -0
- package/rules/native-ios/coding-style.md +72 -0
- package/rules/native-ios/patterns.md +72 -0
- package/rules/native-ios/security.md +59 -0
- package/rules/native-ios/testing.md +79 -0
- package/schemas/hooks.schema.json +34 -0
- package/schemas/plugin.schema.json +55 -0
- package/scripts/hooks/accessibility-check.js +117 -0
- package/scripts/hooks/bundle-size-check.js +31 -0
- package/scripts/hooks/check-console-log.js +37 -0
- package/scripts/hooks/check-expo-config.js +40 -0
- package/scripts/hooks/check-platform-specific.js +40 -0
- package/scripts/hooks/check-reanimated-worklet.js +51 -0
- package/scripts/hooks/continuous-learning-observer.js +24 -0
- package/scripts/hooks/evaluate-session.js +26 -0
- package/scripts/hooks/lib/hook-utils.js +52 -0
- package/scripts/hooks/native-compat-check.js +42 -0
- package/scripts/hooks/performance-budget.js +57 -0
- package/scripts/hooks/post-edit-format.js +38 -0
- package/scripts/hooks/post-edit-typecheck.js +31 -0
- package/scripts/hooks/pre-commit-lint.js +44 -0
- package/scripts/hooks/pre-edit-test-gate.js +68 -0
- package/scripts/hooks/run-with-flags.js +93 -0
- package/scripts/hooks/security-scan.js +65 -0
- package/scripts/hooks/session-start.js +77 -0
- package/scripts/lint-content.js +62 -0
- package/scripts/validate-all.js +137 -0
- package/skills/coding-standards/SKILL.md +88 -0
- package/skills/continuous-learning-v2/SKILL.md +61 -0
- package/skills/continuous-learning-v2/agent-prompts/pattern-analyzer.md +51 -0
- package/skills/continuous-learning-v2/agent-prompts/skill-generator.md +74 -0
- package/skills/continuous-learning-v2/config.json +25 -0
- package/skills/continuous-learning-v2/hook-templates/evaluate-session.cjs.template +69 -0
- package/skills/continuous-learning-v2/hook-templates/observer-hook.cjs.template +54 -0
- package/skills/continuous-learning-v2/scripts/analyze-patterns.js +50 -0
- package/skills/continuous-learning-v2/scripts/extract-session-patterns.js +54 -0
- package/skills/continuous-learning-v2/scripts/validate-content.js +88 -0
- package/skills/native-module-scaffold/SKILL.md +118 -0
- package/skills/performance-optimization/SKILL.md +103 -0
- package/skills/security-review/SKILL.md +99 -0
- package/skills/tdd-workflow/SKILL.md +142 -0
- package/skills/upgrade-workflow/SKILL.md +140 -0
|
@@ -0,0 +1,1624 @@
|
|
|
1
|
+
# ERNE Plan 4: Install CLI & Distribution
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-03-10
|
|
4
|
+
**Spec:** `docs/superpowers/specs/2026-03-10-everything-react-native-expo-design.md`
|
|
5
|
+
**Depends on:** Plans 1–3 (all content files must exist before install can link them)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This plan covers the **installer CLI** (`npx erne-universal init`), **project scaffolding**, **CI/CD**, **website**, **testing**, and **distribution packaging**. After this plan, ERNE is a shippable npm package.
|
|
12
|
+
|
|
13
|
+
**Total files:** ~18 new files
|
|
14
|
+
**Tasks:** 8 across 3 chunks + verification
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Chunk 1: Package Scaffolding & CLI Installer
|
|
19
|
+
|
|
20
|
+
### Task 1: Package Foundation (4 files)
|
|
21
|
+
|
|
22
|
+
#### File 1.1: `package.json`
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"name": "erne-universal",
|
|
27
|
+
"version": "0.1.0",
|
|
28
|
+
"description": "Complete AI coding agent harness for React Native and Expo development",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"react-native",
|
|
31
|
+
"expo",
|
|
32
|
+
"claude-code",
|
|
33
|
+
"ai-agents",
|
|
34
|
+
"mobile-development",
|
|
35
|
+
"coding-assistant"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/JubaKitiashvili/everything-react-native-expo"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://erne.dev",
|
|
43
|
+
"bin": {
|
|
44
|
+
"erne": "./bin/cli.js"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"bin/",
|
|
48
|
+
"agents/",
|
|
49
|
+
"commands/",
|
|
50
|
+
"rules/",
|
|
51
|
+
"skills/",
|
|
52
|
+
"hooks/",
|
|
53
|
+
"contexts/",
|
|
54
|
+
"mcp-configs/",
|
|
55
|
+
"scripts/",
|
|
56
|
+
"examples/",
|
|
57
|
+
"schemas/",
|
|
58
|
+
"docs/",
|
|
59
|
+
"install.sh",
|
|
60
|
+
"LICENSE",
|
|
61
|
+
"README.md"
|
|
62
|
+
],
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"test": "node --test tests/",
|
|
68
|
+
"lint": "node scripts/lint-content.js",
|
|
69
|
+
"validate": "node scripts/validate-all.js",
|
|
70
|
+
"prepublishOnly": "npm run validate"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Notes:**
|
|
77
|
+
- Zero runtime dependencies — CLI uses only Node.js built-ins (`fs`, `path`, `readline`)
|
|
78
|
+
- `bin.erne` allows `npx erne-universal init` to work
|
|
79
|
+
- `files` array controls what gets published to npm (excludes tests, website, .github)
|
|
80
|
+
- `engines.node >= 18` — required for `node --test`, `fs.cpSync`, `readline/promises`
|
|
81
|
+
|
|
82
|
+
#### File 1.2: `bin/cli.js`
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
#!/usr/bin/env node
|
|
86
|
+
// bin/cli.js — ERNE CLI entry point
|
|
87
|
+
// Usage: npx erne-universal <command>
|
|
88
|
+
// Commands:
|
|
89
|
+
// init — Interactive project setup
|
|
90
|
+
// update — Update ERNE to latest version
|
|
91
|
+
// version — Show installed version
|
|
92
|
+
|
|
93
|
+
'use strict';
|
|
94
|
+
|
|
95
|
+
const { resolve, join } = require('path');
|
|
96
|
+
|
|
97
|
+
const COMMANDS = {
|
|
98
|
+
init: () => require('../lib/init'),
|
|
99
|
+
update: () => require('../lib/update'),
|
|
100
|
+
version: () => {
|
|
101
|
+
const pkg = require('../package.json');
|
|
102
|
+
console.log(`erne v${pkg.version}`);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
},
|
|
105
|
+
help: () => {
|
|
106
|
+
console.log(`
|
|
107
|
+
erne — AI coding agent harness for React Native & Expo
|
|
108
|
+
|
|
109
|
+
Usage:
|
|
110
|
+
npx erne-universal <command>
|
|
111
|
+
|
|
112
|
+
Commands:
|
|
113
|
+
init Set up ERNE in your project
|
|
114
|
+
update Update to the latest version
|
|
115
|
+
version Show installed version
|
|
116
|
+
help Show this help message
|
|
117
|
+
|
|
118
|
+
Website: https://erne.dev
|
|
119
|
+
`);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const command = process.argv[2] || 'help';
|
|
125
|
+
|
|
126
|
+
if (!COMMANDS[command]) {
|
|
127
|
+
console.error(`Unknown command: ${command}`);
|
|
128
|
+
console.error('Run "npx erne-universal help" for available commands.');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Execute command module
|
|
133
|
+
const run = COMMANDS[command]();
|
|
134
|
+
if (typeof run === 'function') {
|
|
135
|
+
run().catch(err => {
|
|
136
|
+
console.error('Error:', err.message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Notes:**
|
|
143
|
+
- Shebang line for npx execution
|
|
144
|
+
- Lazy-loads command modules to keep startup fast
|
|
145
|
+
- `help` as default command when no args given
|
|
146
|
+
- No external dependencies — only `require('path')` and sibling modules
|
|
147
|
+
|
|
148
|
+
#### File 1.3: `LICENSE`
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
MIT License
|
|
152
|
+
|
|
153
|
+
Copyright (c) 2026 ERNE Contributors
|
|
154
|
+
|
|
155
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
156
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
157
|
+
in the Software without restriction, including without limitation the rights
|
|
158
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
159
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
160
|
+
furnished to do so, subject to the following conditions:
|
|
161
|
+
|
|
162
|
+
The above copyright notice and this permission notice shall be included in all
|
|
163
|
+
copies or substantial portions of the Software.
|
|
164
|
+
|
|
165
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
166
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
167
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
168
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
169
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
170
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
171
|
+
SOFTWARE.
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### File 1.4: `README.md`
|
|
175
|
+
|
|
176
|
+
````markdown
|
|
177
|
+
# everything-react-native-expo (ERNE)
|
|
178
|
+
|
|
179
|
+
Complete AI coding agent harness for React Native and Expo development.
|
|
180
|
+
|
|
181
|
+
## Quick Start
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npx erne-universal init
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This will:
|
|
188
|
+
1. Detect your project type (Expo managed, bare RN, or monorepo)
|
|
189
|
+
2. Let you choose a hook profile (minimal / standard / strict)
|
|
190
|
+
3. Select MCP integrations (simulator control, GitHub, etc.)
|
|
191
|
+
4. Generate your `.claude/` configuration
|
|
192
|
+
|
|
193
|
+
## What's Included
|
|
194
|
+
|
|
195
|
+
| Component | Count |
|
|
196
|
+
|-----------|-------|
|
|
197
|
+
| Agents | 8 specialized AI agents |
|
|
198
|
+
| Commands | 16 slash commands |
|
|
199
|
+
| Rule layers | 5 (common, expo, bare-rn, native-ios, native-android) |
|
|
200
|
+
| Hook profiles | 3 (minimal, standard, strict) |
|
|
201
|
+
| Skills | 8 reusable knowledge modules |
|
|
202
|
+
| Contexts | 3 behavior modes (dev, review, vibe) |
|
|
203
|
+
| MCP configs | 10 server integrations |
|
|
204
|
+
|
|
205
|
+
## Agents
|
|
206
|
+
|
|
207
|
+
- **architect** — System design and project structure
|
|
208
|
+
- **code-reviewer** — Code quality and best practices
|
|
209
|
+
- **tdd-guide** — Test-driven development workflow
|
|
210
|
+
- **performance-profiler** — Performance diagnostics
|
|
211
|
+
- **native-bridge-builder** — Native module development
|
|
212
|
+
- **expo-config-resolver** — Expo configuration issues
|
|
213
|
+
- **ui-designer** — UI/UX implementation
|
|
214
|
+
- **upgrade-assistant** — Version migration
|
|
215
|
+
|
|
216
|
+
## Hook Profiles
|
|
217
|
+
|
|
218
|
+
| Profile | Use Case |
|
|
219
|
+
|---------|----------|
|
|
220
|
+
| minimal | Fast iteration, vibe coding |
|
|
221
|
+
| standard | Balanced quality + speed (recommended) |
|
|
222
|
+
| strict | Production-grade enforcement |
|
|
223
|
+
|
|
224
|
+
Change profile: Edit `hookProfile` in `.claude/settings.json` or use `/vibe` context.
|
|
225
|
+
|
|
226
|
+
## Commands
|
|
227
|
+
|
|
228
|
+
Core: `/plan`, `/code-review`, `/tdd`, `/build-fix`, `/perf`, `/upgrade`, `/native-module`, `/navigate`
|
|
229
|
+
|
|
230
|
+
Extended: `/animate`, `/deploy`, `/component`, `/debug`, `/quality-gate`
|
|
231
|
+
|
|
232
|
+
Learning: `/learn`, `/retrospective`, `/setup-device`
|
|
233
|
+
|
|
234
|
+
## Documentation
|
|
235
|
+
|
|
236
|
+
- [Getting Started](docs/getting-started.md)
|
|
237
|
+
- [Agents Guide](docs/agents.md)
|
|
238
|
+
- [Commands Reference](docs/commands.md)
|
|
239
|
+
- [Hooks & Profiles](docs/hooks-profiles.md)
|
|
240
|
+
- [Creating Skills](docs/creating-skills.md)
|
|
241
|
+
|
|
242
|
+
## Links
|
|
243
|
+
|
|
244
|
+
- Website: [erne.dev](https://erne.dev)
|
|
245
|
+
- npm: [erne-universal](https://www.npmjs.com/package/erne-universal)
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|
|
250
|
+
````
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### Task 2: Installer Core Logic (2 files)
|
|
255
|
+
|
|
256
|
+
#### File 2.1: `lib/init.js`
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
// lib/init.js — Interactive project initializer
|
|
260
|
+
// Implements the 4-step install flow from spec Section 6
|
|
261
|
+
|
|
262
|
+
'use strict';
|
|
263
|
+
|
|
264
|
+
const fs = require('fs');
|
|
265
|
+
const path = require('path');
|
|
266
|
+
const readline = require('readline/promises');
|
|
267
|
+
const { stdin, stdout } = require('process');
|
|
268
|
+
|
|
269
|
+
module.exports = async function init() {
|
|
270
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
271
|
+
const cwd = process.cwd();
|
|
272
|
+
|
|
273
|
+
console.log('\n erne — Setting up AI agent harness for React Native & Expo\n');
|
|
274
|
+
|
|
275
|
+
// ─── Step 1: Detect project type ───
|
|
276
|
+
console.log(' Step 1: Scanning project...');
|
|
277
|
+
const detection = detectProject(cwd);
|
|
278
|
+
printDetection(detection);
|
|
279
|
+
|
|
280
|
+
if (!detection.isRNProject) {
|
|
281
|
+
console.log('\n ⚠ No React Native project detected in current directory.');
|
|
282
|
+
const proceed = await rl.question(' Continue anyway? (y/N) ');
|
|
283
|
+
if (proceed.toLowerCase() !== 'y') {
|
|
284
|
+
console.log(' Aborted.');
|
|
285
|
+
rl.close();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Step 2: Choose hook profile ───
|
|
291
|
+
console.log('\n Step 2: Select hook profile:\n');
|
|
292
|
+
console.log(' (a) minimal — fast iteration, minimal checks');
|
|
293
|
+
console.log(' (b) standard — balanced quality + speed [recommended]');
|
|
294
|
+
console.log(' (c) strict — production-grade enforcement');
|
|
295
|
+
console.log();
|
|
296
|
+
|
|
297
|
+
let profileChoice = await rl.question(' Profile (a/b/c) [b]: ');
|
|
298
|
+
profileChoice = profileChoice.toLowerCase() || 'b';
|
|
299
|
+
const profileMap = { a: 'minimal', b: 'standard', c: 'strict' };
|
|
300
|
+
const profile = profileMap[profileChoice] || 'standard';
|
|
301
|
+
|
|
302
|
+
// ─── Step 3: Select MCP integrations ───
|
|
303
|
+
console.log('\n Step 3: MCP server integrations:\n');
|
|
304
|
+
|
|
305
|
+
const mcpSelections = {};
|
|
306
|
+
|
|
307
|
+
// Recommended servers
|
|
308
|
+
console.log(' Recommended:');
|
|
309
|
+
const agentDevice = await rl.question(' [Y/n] agent-device — Control iOS Simulator & Android Emulator: ');
|
|
310
|
+
mcpSelections['agent-device'] = agentDevice.toLowerCase() !== 'n';
|
|
311
|
+
|
|
312
|
+
const github = await rl.question(' [Y/n] GitHub — PR management, issue tracking: ');
|
|
313
|
+
mcpSelections['github'] = github.toLowerCase() !== 'n';
|
|
314
|
+
|
|
315
|
+
// Optional servers
|
|
316
|
+
console.log('\n Optional (press Enter to skip):');
|
|
317
|
+
const optionalServers = [
|
|
318
|
+
{ key: 'supabase', label: 'Supabase — Database & auth' },
|
|
319
|
+
{ key: 'firebase', label: 'Firebase — Analytics & push' },
|
|
320
|
+
{ key: 'figma', label: 'Figma — Design token sync' },
|
|
321
|
+
{ key: 'sentry', label: 'Sentry — Error tracking' },
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
for (const server of optionalServers) {
|
|
325
|
+
const answer = await rl.question(` [y/N] ${server.label}: `);
|
|
326
|
+
mcpSelections[server.key] = answer.toLowerCase() === 'y';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
rl.close();
|
|
330
|
+
|
|
331
|
+
// ─── Step 4: Generate config ───
|
|
332
|
+
console.log('\n Step 4: Generating configuration...\n');
|
|
333
|
+
|
|
334
|
+
const erneRoot = path.resolve(__dirname, '..');
|
|
335
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
336
|
+
|
|
337
|
+
// Ensure .claude/ exists
|
|
338
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
339
|
+
|
|
340
|
+
// Copy agents
|
|
341
|
+
copyDir(path.join(erneRoot, 'agents'), path.join(claudeDir, 'agents'));
|
|
342
|
+
console.log(' ✓ .claude/agents/ (8 agents)');
|
|
343
|
+
|
|
344
|
+
// Copy commands
|
|
345
|
+
copyDir(path.join(erneRoot, 'commands'), path.join(claudeDir, 'commands'));
|
|
346
|
+
console.log(' ✓ .claude/commands/ (16 commands)');
|
|
347
|
+
|
|
348
|
+
// Copy applicable rules
|
|
349
|
+
const ruleLayers = determineRuleLayers(detection);
|
|
350
|
+
const rulesTarget = path.join(claudeDir, 'rules');
|
|
351
|
+
fs.mkdirSync(rulesTarget, { recursive: true });
|
|
352
|
+
for (const layer of ruleLayers) {
|
|
353
|
+
copyDir(path.join(erneRoot, 'rules', layer), path.join(rulesTarget, layer));
|
|
354
|
+
}
|
|
355
|
+
console.log(` ✓ .claude/rules/ (layers: ${ruleLayers.join(', ')})`);
|
|
356
|
+
|
|
357
|
+
// Copy selected hook profile
|
|
358
|
+
const hooksSource = path.join(erneRoot, 'hooks');
|
|
359
|
+
const hooksTarget = path.join(claudeDir);
|
|
360
|
+
const profileSource = path.join(hooksSource, 'profiles', `${profile}.json`);
|
|
361
|
+
const masterHooks = JSON.parse(fs.readFileSync(path.join(hooksSource, 'hooks.json'), 'utf8'));
|
|
362
|
+
const profileHooks = JSON.parse(fs.readFileSync(profileSource, 'utf8'));
|
|
363
|
+
const mergedHooks = mergeHookProfile(masterHooks, profileHooks, profile);
|
|
364
|
+
fs.writeFileSync(path.join(hooksTarget, 'hooks.json'), JSON.stringify(mergedHooks, null, 2));
|
|
365
|
+
console.log(` ✓ .claude/hooks.json (${profile} profile)`);
|
|
366
|
+
|
|
367
|
+
// Copy hook scripts
|
|
368
|
+
const scriptsTarget = path.join(claudeDir, 'scripts', 'hooks');
|
|
369
|
+
copyDir(path.join(erneRoot, 'scripts', 'hooks'), scriptsTarget);
|
|
370
|
+
console.log(' ✓ .claude/scripts/hooks/ (hook implementations)');
|
|
371
|
+
|
|
372
|
+
// Copy contexts
|
|
373
|
+
copyDir(path.join(erneRoot, 'contexts'), path.join(claudeDir, 'contexts'));
|
|
374
|
+
console.log(' ✓ .claude/contexts/ (3 contexts)');
|
|
375
|
+
|
|
376
|
+
// Copy selected MCP configs
|
|
377
|
+
const mcpTarget = path.join(claudeDir, 'mcp-configs');
|
|
378
|
+
fs.mkdirSync(mcpTarget, { recursive: true });
|
|
379
|
+
let mcpCount = 0;
|
|
380
|
+
for (const [key, enabled] of Object.entries(mcpSelections)) {
|
|
381
|
+
if (enabled) {
|
|
382
|
+
const src = path.join(erneRoot, 'mcp-configs', `${key}.json`);
|
|
383
|
+
if (fs.existsSync(src)) {
|
|
384
|
+
fs.copyFileSync(src, path.join(mcpTarget, `${key}.json`));
|
|
385
|
+
mcpCount++;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
console.log(` ✓ .claude/mcp-configs/ (${mcpCount} servers)`);
|
|
390
|
+
|
|
391
|
+
// Copy skills
|
|
392
|
+
copyDir(path.join(erneRoot, 'skills'), path.join(claudeDir, 'skills'));
|
|
393
|
+
console.log(' ✓ .claude/skills/ (8 skills)');
|
|
394
|
+
|
|
395
|
+
// Generate CLAUDE.md
|
|
396
|
+
const claudeMd = generateClaudeMd(detection, profile, ruleLayers);
|
|
397
|
+
fs.writeFileSync(path.join(cwd, 'CLAUDE.md'), claudeMd);
|
|
398
|
+
console.log(' ✓ CLAUDE.md (with correct rule imports)');
|
|
399
|
+
|
|
400
|
+
// Generate settings.json
|
|
401
|
+
const settings = {
|
|
402
|
+
hookProfile: profile,
|
|
403
|
+
erneVersion: require('../package.json').version,
|
|
404
|
+
detectedProject: detection.type,
|
|
405
|
+
installedAt: new Date().toISOString()
|
|
406
|
+
};
|
|
407
|
+
fs.writeFileSync(
|
|
408
|
+
path.join(claudeDir, 'settings.json'),
|
|
409
|
+
JSON.stringify(settings, null, 2)
|
|
410
|
+
);
|
|
411
|
+
console.log(' ✓ .claude/settings.json');
|
|
412
|
+
|
|
413
|
+
console.log('\n Done! Run /plan to start your first feature.\n');
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
// ─── Helper functions ───
|
|
418
|
+
|
|
419
|
+
function detectProject(cwd) {
|
|
420
|
+
const result = {
|
|
421
|
+
isRNProject: false,
|
|
422
|
+
type: 'unknown',
|
|
423
|
+
hasExpo: false,
|
|
424
|
+
hasBareRN: false,
|
|
425
|
+
hasIOS: false,
|
|
426
|
+
hasAndroid: false,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Check for app.json / app.config.js / app.config.ts (Expo)
|
|
430
|
+
const expoConfigs = ['app.json', 'app.config.js', 'app.config.ts'];
|
|
431
|
+
result.hasExpo = expoConfigs.some(f => fs.existsSync(path.join(cwd, f)));
|
|
432
|
+
|
|
433
|
+
// Check for ios/ directory with Swift files
|
|
434
|
+
const iosDir = path.join(cwd, 'ios');
|
|
435
|
+
if (fs.existsSync(iosDir) && fs.statSync(iosDir).isDirectory()) {
|
|
436
|
+
result.hasIOS = hasFilesWithExtension(iosDir, '.swift');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Check for android/ directory with Kotlin files
|
|
440
|
+
const androidDir = path.join(cwd, 'android');
|
|
441
|
+
if (fs.existsSync(androidDir) && fs.statSync(androidDir).isDirectory()) {
|
|
442
|
+
result.hasAndroid = hasFilesWithExtension(androidDir, '.kt');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Check for bare RN indicators
|
|
446
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
447
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
448
|
+
try {
|
|
449
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
450
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
451
|
+
if (deps['react-native']) {
|
|
452
|
+
result.isRNProject = true;
|
|
453
|
+
result.hasBareRN = !result.hasExpo;
|
|
454
|
+
}
|
|
455
|
+
if (deps['expo']) {
|
|
456
|
+
result.isRNProject = true;
|
|
457
|
+
result.hasExpo = true;
|
|
458
|
+
}
|
|
459
|
+
} catch { /* ignore parse errors */ }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Determine type
|
|
463
|
+
if (result.hasExpo) result.type = 'expo-managed';
|
|
464
|
+
else if (result.hasBareRN) result.type = 'bare-rn';
|
|
465
|
+
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function hasFilesWithExtension(dir, ext) {
|
|
470
|
+
try {
|
|
471
|
+
const entries = fs.readdirSync(dir, { recursive: true });
|
|
472
|
+
return entries.some(entry => entry.endsWith(ext));
|
|
473
|
+
} catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function printDetection(detection) {
|
|
479
|
+
const ok = (msg) => console.log(` ✓ ${msg}`);
|
|
480
|
+
const no = (msg) => console.log(` ✗ ${msg}`);
|
|
481
|
+
|
|
482
|
+
if (detection.hasExpo) ok('Expo config found → Expo managed workflow');
|
|
483
|
+
else no('No Expo config detected');
|
|
484
|
+
|
|
485
|
+
if (detection.hasBareRN) ok('Bare React Native project detected');
|
|
486
|
+
|
|
487
|
+
if (detection.hasIOS) ok('ios/ contains Swift files → iOS native rules enabled');
|
|
488
|
+
else no('No iOS native code found');
|
|
489
|
+
|
|
490
|
+
if (detection.hasAndroid) ok('android/ contains Kotlin files → Android native rules enabled');
|
|
491
|
+
else no('No Android native code found');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function determineRuleLayers(detection) {
|
|
495
|
+
const layers = ['common'];
|
|
496
|
+
if (detection.hasExpo) layers.push('expo');
|
|
497
|
+
if (detection.hasBareRN) layers.push('bare-rn');
|
|
498
|
+
if (detection.hasIOS) layers.push('native-ios');
|
|
499
|
+
if (detection.hasAndroid) layers.push('native-android');
|
|
500
|
+
return layers;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function mergeHookProfile(masterHooks, profileHooks, profileName) {
|
|
504
|
+
// Filter master hooks to only include those enabled in the profile
|
|
505
|
+
const enabledEvents = profileHooks.enabledEvents || [];
|
|
506
|
+
const result = {};
|
|
507
|
+
|
|
508
|
+
for (const [event, hooks] of Object.entries(masterHooks)) {
|
|
509
|
+
if (event === '_meta') {
|
|
510
|
+
result._meta = { ...masterHooks._meta, activeProfile: profileName };
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (Array.isArray(hooks)) {
|
|
515
|
+
result[event] = hooks.filter(hook => {
|
|
516
|
+
// Include hook if the profile enables its event
|
|
517
|
+
// or if the hook has no profile restriction
|
|
518
|
+
const hookProfiles = hook.profiles || ['minimal', 'standard', 'strict'];
|
|
519
|
+
return hookProfiles.includes(profileName);
|
|
520
|
+
});
|
|
521
|
+
// Remove empty arrays
|
|
522
|
+
if (result[event].length === 0) delete result[event];
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function generateClaudeMd(detection, profile, ruleLayers) {
|
|
530
|
+
const lines = [
|
|
531
|
+
'# Project Configuration (ERNE)',
|
|
532
|
+
'',
|
|
533
|
+
`Hook profile: ${profile}`,
|
|
534
|
+
`Project type: ${detection.type}`,
|
|
535
|
+
'',
|
|
536
|
+
'## Rules',
|
|
537
|
+
'',
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
for (const layer of ruleLayers) {
|
|
541
|
+
lines.push(`@import .claude/rules/${layer}/`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
lines.push('', '## Skills', '', '@import .claude/skills/', '');
|
|
545
|
+
|
|
546
|
+
return lines.join('\n');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function copyDir(src, dest) {
|
|
550
|
+
if (!fs.existsSync(src)) return;
|
|
551
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Notes:**
|
|
556
|
+
- 4-step interactive flow matching spec Section 6 exactly
|
|
557
|
+
- Project detection: checks `app.json`, `expo` in deps, `ios/` Swift, `android/` Kotlin
|
|
558
|
+
- `readline/promises` for async interactive prompts (Node 18+)
|
|
559
|
+
- `fs.cpSync` for directory copying (Node 16.7+, stable in 18+)
|
|
560
|
+
- Hook profile merging filters master hooks.json by profile flags
|
|
561
|
+
- Generated CLAUDE.md uses `@import` for rule layer inclusion
|
|
562
|
+
- Zero external dependencies
|
|
563
|
+
|
|
564
|
+
#### File 2.2: `lib/update.js`
|
|
565
|
+
|
|
566
|
+
```javascript
|
|
567
|
+
// lib/update.js — Update ERNE to latest version
|
|
568
|
+
// Usage: npx erne-universal update
|
|
569
|
+
|
|
570
|
+
'use strict';
|
|
571
|
+
|
|
572
|
+
const { execSync } = require('child_process');
|
|
573
|
+
const fs = require('fs');
|
|
574
|
+
const path = require('path');
|
|
575
|
+
|
|
576
|
+
module.exports = async function update() {
|
|
577
|
+
const cwd = process.cwd();
|
|
578
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
579
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
580
|
+
|
|
581
|
+
console.log('\n erne — Checking for updates...\n');
|
|
582
|
+
|
|
583
|
+
// Check if ERNE is installed in this project
|
|
584
|
+
if (!fs.existsSync(settingsPath)) {
|
|
585
|
+
console.log(' ⚠ ERNE not found in this project.');
|
|
586
|
+
console.log(' Run "npx erne-universal init" to set up.');
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
591
|
+
console.log(` Current version: ${settings.erneVersion}`);
|
|
592
|
+
|
|
593
|
+
// Fetch latest version from npm
|
|
594
|
+
let latestVersion;
|
|
595
|
+
try {
|
|
596
|
+
latestVersion = execSync('npm view erne-universal version', { encoding: 'utf8' }).trim();
|
|
597
|
+
} catch {
|
|
598
|
+
console.log(' ⚠ Could not check npm for latest version.');
|
|
599
|
+
console.log(' Check https://erne.dev for updates.');
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
console.log(` Latest version: ${latestVersion}`);
|
|
604
|
+
|
|
605
|
+
if (settings.erneVersion === latestVersion) {
|
|
606
|
+
console.log('\n Already up to date!\n');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
console.log(`\n Updating ${settings.erneVersion} → ${latestVersion}...`);
|
|
611
|
+
|
|
612
|
+
// Re-run init with preserved settings
|
|
613
|
+
// The init command detects existing settings and preserves user choices
|
|
614
|
+
console.log(' Running: npx erne-universal@latest init');
|
|
615
|
+
console.log(' Your profile and MCP selections will be preserved.\n');
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
execSync(`npx erne-universal@${latestVersion} init`, {
|
|
619
|
+
stdio: 'inherit',
|
|
620
|
+
cwd,
|
|
621
|
+
});
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.error(' Update failed:', err.message);
|
|
624
|
+
console.error(' Manual update: npm install -g erne-universal@latest && erne init');
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Notes:**
|
|
630
|
+
- Checks current version from `.claude/settings.json`
|
|
631
|
+
- Compares against npm registry latest
|
|
632
|
+
- Re-runs `init` at latest version (preserves user's profile/MCP choices)
|
|
633
|
+
- Falls back gracefully if npm is unreachable
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
### Task 3: Shell Installer (1 file)
|
|
638
|
+
|
|
639
|
+
#### File 3.1: `install.sh`
|
|
640
|
+
|
|
641
|
+
```bash
|
|
642
|
+
#!/bin/bash
|
|
643
|
+
# install.sh — ERNE installer for Claude Code / Cursor / Windsurf
|
|
644
|
+
# Usage: curl -fsSL https://erne.dev/install.sh | bash
|
|
645
|
+
# Or: git clone <repo> && cd everything-react-native-expo && ./install.sh
|
|
646
|
+
|
|
647
|
+
set -euo pipefail
|
|
648
|
+
|
|
649
|
+
ERNE_VERSION="0.1.0"
|
|
650
|
+
REPO_URL="https://github.com/JubaKitiashvili/everything-react-native-expo"
|
|
651
|
+
|
|
652
|
+
echo ""
|
|
653
|
+
echo " erne v${ERNE_VERSION} — AI agent harness for React Native & Expo"
|
|
654
|
+
echo ""
|
|
655
|
+
|
|
656
|
+
# Check prerequisites
|
|
657
|
+
command -v node >/dev/null 2>&1 || {
|
|
658
|
+
echo " ✗ Node.js is required. Install from https://nodejs.org/"
|
|
659
|
+
exit 1
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
|
663
|
+
if [ "$NODE_VERSION" -lt 18 ]; then
|
|
664
|
+
echo " ✗ Node.js 18+ required. Current: $(node -v)"
|
|
665
|
+
exit 1
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
echo " ✓ Node.js $(node -v) detected"
|
|
669
|
+
|
|
670
|
+
# Check for npm
|
|
671
|
+
command -v npm >/dev/null 2>&1 || {
|
|
672
|
+
echo " ✗ npm is required."
|
|
673
|
+
exit 1
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
# Determine install method
|
|
677
|
+
if [ -f "package.json" ]; then
|
|
678
|
+
echo " ✓ package.json found — installing locally"
|
|
679
|
+
echo ""
|
|
680
|
+
|
|
681
|
+
# Use npx to run init
|
|
682
|
+
npx erne-universal init
|
|
683
|
+
else
|
|
684
|
+
echo " ⚠ No package.json found in current directory."
|
|
685
|
+
echo " Navigate to your React Native project first."
|
|
686
|
+
echo ""
|
|
687
|
+
echo " Usage:"
|
|
688
|
+
echo " cd your-rn-project"
|
|
689
|
+
echo " npx erne-universal init"
|
|
690
|
+
exit 1
|
|
691
|
+
fi
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
**Notes:**
|
|
695
|
+
- Works both as curl-pipe-bash from `erne.dev` and as local `./install.sh`
|
|
696
|
+
- Checks Node.js 18+ prerequisite
|
|
697
|
+
- Delegates to `npx erne-universal init` for actual installation
|
|
698
|
+
- `set -euo pipefail` for robust error handling
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## Chunk 2: CI/CD, Testing & Validation
|
|
703
|
+
|
|
704
|
+
### Task 4: GitHub Actions & Contributing (3 files)
|
|
705
|
+
|
|
706
|
+
#### File 4.1: `.github/workflows/ci.yml`
|
|
707
|
+
|
|
708
|
+
```yaml
|
|
709
|
+
name: CI
|
|
710
|
+
|
|
711
|
+
on:
|
|
712
|
+
push:
|
|
713
|
+
branches: [main]
|
|
714
|
+
pull_request:
|
|
715
|
+
branches: [main]
|
|
716
|
+
|
|
717
|
+
jobs:
|
|
718
|
+
validate:
|
|
719
|
+
runs-on: ubuntu-latest
|
|
720
|
+
strategy:
|
|
721
|
+
matrix:
|
|
722
|
+
node-version: [18, 20, 22]
|
|
723
|
+
|
|
724
|
+
steps:
|
|
725
|
+
- uses: actions/checkout@v4
|
|
726
|
+
|
|
727
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
728
|
+
uses: actions/setup-node@v4
|
|
729
|
+
with:
|
|
730
|
+
node-version: ${{ matrix.node-version }}
|
|
731
|
+
|
|
732
|
+
- name: Validate content files
|
|
733
|
+
run: npm run validate
|
|
734
|
+
|
|
735
|
+
- name: Run tests
|
|
736
|
+
run: npm test
|
|
737
|
+
|
|
738
|
+
- name: Check CLI runs
|
|
739
|
+
run: node bin/cli.js version
|
|
740
|
+
|
|
741
|
+
- name: Verify file structure
|
|
742
|
+
run: |
|
|
743
|
+
echo "Checking required directories..."
|
|
744
|
+
for dir in agents commands rules skills hooks contexts mcp-configs scripts schemas docs examples; do
|
|
745
|
+
if [ ! -d "$dir" ]; then
|
|
746
|
+
echo "FAIL: Missing directory: $dir"
|
|
747
|
+
exit 1
|
|
748
|
+
fi
|
|
749
|
+
done
|
|
750
|
+
echo "All directories present."
|
|
751
|
+
|
|
752
|
+
echo "Checking agent count..."
|
|
753
|
+
AGENT_COUNT=$(ls -1 agents/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
754
|
+
if [ "$AGENT_COUNT" -ne 8 ]; then
|
|
755
|
+
echo "FAIL: Expected 8 agents, found $AGENT_COUNT"
|
|
756
|
+
exit 1
|
|
757
|
+
fi
|
|
758
|
+
echo "OK: $AGENT_COUNT agents"
|
|
759
|
+
|
|
760
|
+
echo "Checking command count..."
|
|
761
|
+
CMD_COUNT=$(ls -1 commands/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
762
|
+
if [ "$CMD_COUNT" -ne 16 ]; then
|
|
763
|
+
echo "FAIL: Expected 16 commands, found $CMD_COUNT"
|
|
764
|
+
exit 1
|
|
765
|
+
fi
|
|
766
|
+
echo "OK: $CMD_COUNT commands"
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Notes:**
|
|
770
|
+
- Tests on Node 18, 20, 22 (current LTS range)
|
|
771
|
+
- Validates content file frontmatter, runs unit tests, checks CLI, verifies file counts
|
|
772
|
+
- Runs on push to main and PRs
|
|
773
|
+
|
|
774
|
+
#### File 4.2: `.github/CONTRIBUTING.md`
|
|
775
|
+
|
|
776
|
+
```markdown
|
|
777
|
+
# Contributing to ERNE
|
|
778
|
+
|
|
779
|
+
## Project Structure
|
|
780
|
+
|
|
781
|
+
- `agents/` — AI agent definitions (8 `.md` files)
|
|
782
|
+
- `commands/` — Slash command definitions (16 `.md` files)
|
|
783
|
+
- `rules/` — Coding rules organized by layer
|
|
784
|
+
- `skills/` — Reusable knowledge modules
|
|
785
|
+
- `hooks/` — Git/development hooks with profile system
|
|
786
|
+
- `contexts/` — Behavior mode definitions
|
|
787
|
+
- `mcp-configs/` — MCP server configuration templates
|
|
788
|
+
- `scripts/hooks/` — Hook implementation scripts (CJS)
|
|
789
|
+
- `lib/` — CLI logic (init, update)
|
|
790
|
+
- `tests/` — Test suites
|
|
791
|
+
|
|
792
|
+
## Content File Format
|
|
793
|
+
|
|
794
|
+
Agent, command, and rule files use YAML frontmatter:
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
name: agent-name
|
|
798
|
+
description: What the agent does
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
Content body in markdown.
|
|
802
|
+
|
|
803
|
+
## Hook Scripts
|
|
804
|
+
|
|
805
|
+
All hook scripts use CommonJS (`.js` or `.cjs`). No ES modules.
|
|
806
|
+
|
|
807
|
+
## Testing
|
|
808
|
+
|
|
809
|
+
npm test # Run all tests
|
|
810
|
+
npm run validate # Validate content files
|
|
811
|
+
npm run lint # Lint content
|
|
812
|
+
|
|
813
|
+
## Pull Request Process
|
|
814
|
+
|
|
815
|
+
1. Fork the repository
|
|
816
|
+
2. Create a feature branch
|
|
817
|
+
3. Make changes following existing patterns
|
|
818
|
+
4. Run `npm run validate && npm test`
|
|
819
|
+
5. Submit PR with description of changes
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
#### File 4.3: `.github/ISSUE_TEMPLATE/bug_report.md`
|
|
823
|
+
|
|
824
|
+
```markdown
|
|
825
|
+
---
|
|
826
|
+
name: Bug Report
|
|
827
|
+
about: Report a problem with ERNE
|
|
828
|
+
labels: bug
|
|
829
|
+
---
|
|
830
|
+
|
|
831
|
+
## Description
|
|
832
|
+
|
|
833
|
+
Brief description of the issue.
|
|
834
|
+
|
|
835
|
+
## Environment
|
|
836
|
+
|
|
837
|
+
- ERNE version:
|
|
838
|
+
- Node.js version:
|
|
839
|
+
- OS:
|
|
840
|
+
- Claude Code version:
|
|
841
|
+
- Project type: (Expo managed / bare RN / monorepo)
|
|
842
|
+
|
|
843
|
+
## Steps to Reproduce
|
|
844
|
+
|
|
845
|
+
1.
|
|
846
|
+
2.
|
|
847
|
+
3.
|
|
848
|
+
|
|
849
|
+
## Expected Behavior
|
|
850
|
+
|
|
851
|
+
## Actual Behavior
|
|
852
|
+
|
|
853
|
+
## Additional Context
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
### Task 5: Validation & Lint Scripts (2 files)
|
|
859
|
+
|
|
860
|
+
#### File 5.1: `scripts/validate-all.js`
|
|
861
|
+
|
|
862
|
+
```javascript
|
|
863
|
+
#!/usr/bin/env node
|
|
864
|
+
// scripts/validate-all.js — Validate all ERNE content files
|
|
865
|
+
// Checks: frontmatter format, JSON validity, required fields, file counts
|
|
866
|
+
|
|
867
|
+
'use strict';
|
|
868
|
+
|
|
869
|
+
const fs = require('fs');
|
|
870
|
+
const path = require('path');
|
|
871
|
+
|
|
872
|
+
let errors = 0;
|
|
873
|
+
let warnings = 0;
|
|
874
|
+
let checked = 0;
|
|
875
|
+
|
|
876
|
+
function error(msg) { errors++; console.error(` ✗ ${msg}`); }
|
|
877
|
+
function warn(msg) { warnings++; console.warn(` ⚠ ${msg}`); }
|
|
878
|
+
function ok(msg) { console.log(` ✓ ${msg}`); }
|
|
879
|
+
|
|
880
|
+
// ─── Validate frontmatter in .md files ───
|
|
881
|
+
function validateFrontmatter(filePath, requiredFields) {
|
|
882
|
+
checked++;
|
|
883
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
884
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
885
|
+
|
|
886
|
+
if (!match) {
|
|
887
|
+
error(`${filePath}: Missing frontmatter`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const frontmatter = match[1];
|
|
892
|
+
for (const field of requiredFields) {
|
|
893
|
+
if (!frontmatter.includes(`${field}:`)) {
|
|
894
|
+
error(`${filePath}: Missing required field '${field}'`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ─── Validate JSON files ───
|
|
900
|
+
function validateJson(filePath) {
|
|
901
|
+
checked++;
|
|
902
|
+
try {
|
|
903
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
904
|
+
JSON.parse(content);
|
|
905
|
+
} catch (e) {
|
|
906
|
+
error(`${filePath}: Invalid JSON — ${e.message}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ─── Validate directory file counts ───
|
|
911
|
+
function validateCount(dir, ext, expected, label) {
|
|
912
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith(ext));
|
|
913
|
+
if (files.length !== expected) {
|
|
914
|
+
error(`${label}: Expected ${expected} files, found ${files.length}`);
|
|
915
|
+
} else {
|
|
916
|
+
ok(`${label}: ${files.length} files`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ─── Main validation ───
|
|
921
|
+
console.log('\n ERNE Content Validation\n');
|
|
922
|
+
|
|
923
|
+
// Agents
|
|
924
|
+
console.log(' Agents:');
|
|
925
|
+
validateCount('agents', '.md', 8, 'agents/');
|
|
926
|
+
const agentFiles = fs.readdirSync('agents').filter(f => f.endsWith('.md'));
|
|
927
|
+
for (const f of agentFiles) {
|
|
928
|
+
validateFrontmatter(path.join('agents', f), ['name', 'description']);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Commands
|
|
932
|
+
console.log(' Commands:');
|
|
933
|
+
validateCount('commands', '.md', 16, 'commands/');
|
|
934
|
+
const cmdFiles = fs.readdirSync('commands').filter(f => f.endsWith('.md'));
|
|
935
|
+
for (const f of cmdFiles) {
|
|
936
|
+
validateFrontmatter(path.join('commands', f), ['name', 'description']);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Rules
|
|
940
|
+
console.log(' Rules:');
|
|
941
|
+
const ruleLayers = ['common', 'expo', 'bare-rn', 'native-ios', 'native-android'];
|
|
942
|
+
for (const layer of ruleLayers) {
|
|
943
|
+
const layerDir = path.join('rules', layer);
|
|
944
|
+
if (!fs.existsSync(layerDir)) {
|
|
945
|
+
error(`rules/${layer}/: Missing directory`);
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
const ruleFiles = fs.readdirSync(layerDir).filter(f => f.endsWith('.md'));
|
|
949
|
+
ok(`rules/${layer}/: ${ruleFiles.length} files`);
|
|
950
|
+
for (const f of ruleFiles) {
|
|
951
|
+
validateFrontmatter(path.join(layerDir, f), ['description']);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Hook profiles
|
|
956
|
+
console.log(' Hooks:');
|
|
957
|
+
validateJson('hooks/hooks.json');
|
|
958
|
+
for (const profile of ['minimal', 'standard', 'strict']) {
|
|
959
|
+
validateJson(path.join('hooks', 'profiles', `${profile}.json`));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// MCP configs
|
|
963
|
+
console.log(' MCP Configs:');
|
|
964
|
+
const mcpFiles = fs.readdirSync('mcp-configs').filter(f => f.endsWith('.json'));
|
|
965
|
+
ok(`mcp-configs/: ${mcpFiles.length} files`);
|
|
966
|
+
for (const f of mcpFiles) {
|
|
967
|
+
validateJson(path.join('mcp-configs', f));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Contexts
|
|
971
|
+
console.log(' Contexts:');
|
|
972
|
+
validateCount('contexts', '.md', 3, 'contexts/');
|
|
973
|
+
|
|
974
|
+
// Skills
|
|
975
|
+
console.log(' Skills:');
|
|
976
|
+
const skillDirs = fs.readdirSync('skills', { withFileTypes: true })
|
|
977
|
+
.filter(d => d.isDirectory())
|
|
978
|
+
.map(d => d.name);
|
|
979
|
+
ok(`skills/: ${skillDirs.length} skill directories`);
|
|
980
|
+
for (const dir of skillDirs) {
|
|
981
|
+
const skillMd = path.join('skills', dir, 'SKILL.md');
|
|
982
|
+
if (!fs.existsSync(skillMd)) {
|
|
983
|
+
error(`skills/${dir}/: Missing SKILL.md`);
|
|
984
|
+
} else {
|
|
985
|
+
checked++;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Schemas
|
|
990
|
+
console.log(' Schemas:');
|
|
991
|
+
validateJson('schemas/hooks.schema.json');
|
|
992
|
+
validateJson('schemas/plugin.schema.json');
|
|
993
|
+
|
|
994
|
+
// Summary
|
|
995
|
+
console.log(`\n Checked ${checked} files: ${errors} errors, ${warnings} warnings\n`);
|
|
996
|
+
|
|
997
|
+
if (errors > 0) {
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
#### File 5.2: `scripts/lint-content.js`
|
|
1003
|
+
|
|
1004
|
+
```javascript
|
|
1005
|
+
#!/usr/bin/env node
|
|
1006
|
+
// scripts/lint-content.js — Lint ERNE content files for style consistency
|
|
1007
|
+
// Checks: trailing whitespace, consistent headings, max line length in frontmatter
|
|
1008
|
+
|
|
1009
|
+
'use strict';
|
|
1010
|
+
|
|
1011
|
+
const fs = require('fs');
|
|
1012
|
+
const path = require('path');
|
|
1013
|
+
|
|
1014
|
+
let issues = 0;
|
|
1015
|
+
|
|
1016
|
+
function lint(filePath) {
|
|
1017
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
1018
|
+
const lines = content.split('\n');
|
|
1019
|
+
|
|
1020
|
+
// Check for trailing whitespace
|
|
1021
|
+
lines.forEach((line, i) => {
|
|
1022
|
+
if (line !== line.trimEnd() && line.trim().length > 0) {
|
|
1023
|
+
console.log(` ${filePath}:${i + 1}: trailing whitespace`);
|
|
1024
|
+
issues++;
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// Check frontmatter has no empty description
|
|
1029
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1030
|
+
if (fmMatch) {
|
|
1031
|
+
const fm = fmMatch[1];
|
|
1032
|
+
if (fm.includes('description:') && fm.match(/description:\s*$/m)) {
|
|
1033
|
+
console.log(` ${filePath}: empty description in frontmatter`);
|
|
1034
|
+
issues++;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Check file ends with newline
|
|
1039
|
+
if (content.length > 0 && !content.endsWith('\n')) {
|
|
1040
|
+
console.log(` ${filePath}: missing trailing newline`);
|
|
1041
|
+
issues++;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function lintDir(dir, ext) {
|
|
1046
|
+
if (!fs.existsSync(dir)) return;
|
|
1047
|
+
const entries = fs.readdirSync(dir, { recursive: true });
|
|
1048
|
+
for (const entry of entries) {
|
|
1049
|
+
const fullPath = path.join(dir, entry);
|
|
1050
|
+
if (fullPath.endsWith(ext) && fs.statSync(fullPath).isFile()) {
|
|
1051
|
+
lint(fullPath);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
console.log('\n ERNE Content Lint\n');
|
|
1057
|
+
|
|
1058
|
+
lintDir('agents', '.md');
|
|
1059
|
+
lintDir('commands', '.md');
|
|
1060
|
+
lintDir('rules', '.md');
|
|
1061
|
+
lintDir('contexts', '.md');
|
|
1062
|
+
lintDir('skills', '.md');
|
|
1063
|
+
lintDir('docs', '.md');
|
|
1064
|
+
|
|
1065
|
+
console.log(`\n ${issues} issues found\n`);
|
|
1066
|
+
if (issues > 0) process.exit(1);
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
---
|
|
1070
|
+
|
|
1071
|
+
### Task 6: Unit Tests (2 files)
|
|
1072
|
+
|
|
1073
|
+
#### File 6.1: `tests/cli.test.js`
|
|
1074
|
+
|
|
1075
|
+
```javascript
|
|
1076
|
+
// tests/cli.test.js — CLI entry point tests
|
|
1077
|
+
// Uses Node.js built-in test runner (node --test)
|
|
1078
|
+
|
|
1079
|
+
'use strict';
|
|
1080
|
+
|
|
1081
|
+
const { describe, it } = require('node:test');
|
|
1082
|
+
const assert = require('node:assert/strict');
|
|
1083
|
+
const { execSync } = require('child_process');
|
|
1084
|
+
const path = require('path');
|
|
1085
|
+
|
|
1086
|
+
const CLI_PATH = path.resolve(__dirname, '..', 'bin', 'cli.js');
|
|
1087
|
+
|
|
1088
|
+
describe('CLI', () => {
|
|
1089
|
+
it('shows version', () => {
|
|
1090
|
+
const output = execSync(`node ${CLI_PATH} version`, { encoding: 'utf8' });
|
|
1091
|
+
assert.match(output, /erne v\d+\.\d+\.\d+/);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
it('shows help', () => {
|
|
1095
|
+
const output = execSync(`node ${CLI_PATH} help`, { encoding: 'utf8' });
|
|
1096
|
+
assert.ok(output.includes('erne'));
|
|
1097
|
+
assert.ok(output.includes('init'));
|
|
1098
|
+
assert.ok(output.includes('update'));
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it('shows help for no arguments', () => {
|
|
1102
|
+
const output = execSync(`node ${CLI_PATH}`, { encoding: 'utf8' });
|
|
1103
|
+
assert.ok(output.includes('erne'));
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it('errors on unknown command', () => {
|
|
1107
|
+
assert.throws(() => {
|
|
1108
|
+
execSync(`node ${CLI_PATH} nonexistent`, { encoding: 'utf8' });
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
#### File 6.2: `tests/detection.test.js`
|
|
1115
|
+
|
|
1116
|
+
```javascript
|
|
1117
|
+
// tests/detection.test.js — Project detection logic tests
|
|
1118
|
+
// Tests the detectProject function used in init flow
|
|
1119
|
+
|
|
1120
|
+
'use strict';
|
|
1121
|
+
|
|
1122
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
1123
|
+
const assert = require('node:assert/strict');
|
|
1124
|
+
const fs = require('fs');
|
|
1125
|
+
const path = require('path');
|
|
1126
|
+
const os = require('os');
|
|
1127
|
+
|
|
1128
|
+
// Create temp directory for each test
|
|
1129
|
+
let tmpDir;
|
|
1130
|
+
|
|
1131
|
+
beforeEach(() => {
|
|
1132
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'erne-test-'));
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
afterEach(() => {
|
|
1136
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// Extract detectProject for testing
|
|
1140
|
+
// We test the detection logic by creating mock project structures
|
|
1141
|
+
// and running the init module's detection against them
|
|
1142
|
+
|
|
1143
|
+
describe('Project Detection', () => {
|
|
1144
|
+
it('detects Expo managed project', () => {
|
|
1145
|
+
// Create app.json
|
|
1146
|
+
fs.writeFileSync(
|
|
1147
|
+
path.join(tmpDir, 'app.json'),
|
|
1148
|
+
JSON.stringify({ expo: { name: 'test' } })
|
|
1149
|
+
);
|
|
1150
|
+
// Create package.json with expo dep
|
|
1151
|
+
fs.writeFileSync(
|
|
1152
|
+
path.join(tmpDir, 'package.json'),
|
|
1153
|
+
JSON.stringify({
|
|
1154
|
+
dependencies: { expo: '~51.0.0', 'react-native': '0.74.0' }
|
|
1155
|
+
})
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
const result = detectInDir(tmpDir);
|
|
1159
|
+
assert.equal(result.type, 'expo-managed');
|
|
1160
|
+
assert.equal(result.hasExpo, true);
|
|
1161
|
+
assert.equal(result.isRNProject, true);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
it('detects bare React Native project', () => {
|
|
1165
|
+
fs.writeFileSync(
|
|
1166
|
+
path.join(tmpDir, 'package.json'),
|
|
1167
|
+
JSON.stringify({
|
|
1168
|
+
dependencies: { 'react-native': '0.74.0' }
|
|
1169
|
+
})
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
const result = detectInDir(tmpDir);
|
|
1173
|
+
assert.equal(result.type, 'bare-rn');
|
|
1174
|
+
assert.equal(result.hasBareRN, true);
|
|
1175
|
+
assert.equal(result.hasExpo, false);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it('detects iOS native code', () => {
|
|
1179
|
+
const iosDir = path.join(tmpDir, 'ios');
|
|
1180
|
+
fs.mkdirSync(iosDir, { recursive: true });
|
|
1181
|
+
fs.writeFileSync(path.join(iosDir, 'AppDelegate.swift'), '');
|
|
1182
|
+
fs.writeFileSync(
|
|
1183
|
+
path.join(tmpDir, 'package.json'),
|
|
1184
|
+
JSON.stringify({ dependencies: { 'react-native': '0.74.0' } })
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
const result = detectInDir(tmpDir);
|
|
1188
|
+
assert.equal(result.hasIOS, true);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
it('detects Android native code', () => {
|
|
1192
|
+
const androidDir = path.join(tmpDir, 'android');
|
|
1193
|
+
fs.mkdirSync(androidDir, { recursive: true });
|
|
1194
|
+
fs.writeFileSync(path.join(androidDir, 'MainActivity.kt'), '');
|
|
1195
|
+
fs.writeFileSync(
|
|
1196
|
+
path.join(tmpDir, 'package.json'),
|
|
1197
|
+
JSON.stringify({ dependencies: { 'react-native': '0.74.0' } })
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
const result = detectInDir(tmpDir);
|
|
1201
|
+
assert.equal(result.hasAndroid, true);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it('returns unknown for non-RN project', () => {
|
|
1205
|
+
fs.writeFileSync(
|
|
1206
|
+
path.join(tmpDir, 'package.json'),
|
|
1207
|
+
JSON.stringify({ dependencies: { express: '4.0.0' } })
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
const result = detectInDir(tmpDir);
|
|
1211
|
+
assert.equal(result.type, 'unknown');
|
|
1212
|
+
assert.equal(result.isRNProject, false);
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// Simple project detector mirroring lib/init.js logic
|
|
1217
|
+
function detectInDir(cwd) {
|
|
1218
|
+
const result = {
|
|
1219
|
+
isRNProject: false,
|
|
1220
|
+
type: 'unknown',
|
|
1221
|
+
hasExpo: false,
|
|
1222
|
+
hasBareRN: false,
|
|
1223
|
+
hasIOS: false,
|
|
1224
|
+
hasAndroid: false,
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
const expoConfigs = ['app.json', 'app.config.js', 'app.config.ts'];
|
|
1228
|
+
result.hasExpo = expoConfigs.some(f => fs.existsSync(path.join(cwd, f)));
|
|
1229
|
+
|
|
1230
|
+
const iosDir = path.join(cwd, 'ios');
|
|
1231
|
+
if (fs.existsSync(iosDir) && fs.statSync(iosDir).isDirectory()) {
|
|
1232
|
+
try {
|
|
1233
|
+
const entries = fs.readdirSync(iosDir, { recursive: true });
|
|
1234
|
+
result.hasIOS = entries.some(e => e.endsWith('.swift'));
|
|
1235
|
+
} catch { result.hasIOS = false; }
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const androidDir = path.join(cwd, 'android');
|
|
1239
|
+
if (fs.existsSync(androidDir) && fs.statSync(androidDir).isDirectory()) {
|
|
1240
|
+
try {
|
|
1241
|
+
const entries = fs.readdirSync(androidDir, { recursive: true });
|
|
1242
|
+
result.hasAndroid = entries.some(e => e.endsWith('.kt'));
|
|
1243
|
+
} catch { result.hasAndroid = false; }
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
1247
|
+
if (fs.existsSync(pkgPath)) {
|
|
1248
|
+
try {
|
|
1249
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1250
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1251
|
+
if (deps['react-native']) {
|
|
1252
|
+
result.isRNProject = true;
|
|
1253
|
+
result.hasBareRN = !result.hasExpo;
|
|
1254
|
+
}
|
|
1255
|
+
if (deps['expo']) {
|
|
1256
|
+
result.isRNProject = true;
|
|
1257
|
+
result.hasExpo = true;
|
|
1258
|
+
}
|
|
1259
|
+
} catch { /* ignore */ }
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (result.hasExpo) result.type = 'expo-managed';
|
|
1263
|
+
else if (result.hasBareRN) result.type = 'bare-rn';
|
|
1264
|
+
|
|
1265
|
+
return result;
|
|
1266
|
+
}
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
**Notes:**
|
|
1270
|
+
- Uses Node.js built-in test runner (`node:test`) — no test framework dependency
|
|
1271
|
+
- CLI tests verify `version`, `help`, and error handling
|
|
1272
|
+
- Detection tests create real temp directories with mock project structures
|
|
1273
|
+
- Tests clean up temp dirs after each test
|
|
1274
|
+
|
|
1275
|
+
---
|
|
1276
|
+
|
|
1277
|
+
## Chunk 3: Website & Final Packaging
|
|
1278
|
+
|
|
1279
|
+
### Task 7: Landing Page (1 file)
|
|
1280
|
+
|
|
1281
|
+
#### File 7.1: `website/index.html`
|
|
1282
|
+
|
|
1283
|
+
```html
|
|
1284
|
+
<!DOCTYPE html>
|
|
1285
|
+
<html lang="en">
|
|
1286
|
+
<head>
|
|
1287
|
+
<meta charset="UTF-8">
|
|
1288
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1289
|
+
<title>ERNE — AI Agent Harness for React Native & Expo</title>
|
|
1290
|
+
<meta name="description" content="Complete AI coding agent harness for React Native and Expo development. 8 agents, 16 commands, 5 rule layers, 3 hook profiles.">
|
|
1291
|
+
<style>
|
|
1292
|
+
:root {
|
|
1293
|
+
--bg: #0a0a0a;
|
|
1294
|
+
--fg: #e5e5e5;
|
|
1295
|
+
--accent: #3b82f6;
|
|
1296
|
+
--muted: #737373;
|
|
1297
|
+
--code-bg: #1a1a1a;
|
|
1298
|
+
--border: #262626;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1302
|
+
|
|
1303
|
+
body {
|
|
1304
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1305
|
+
background: var(--bg);
|
|
1306
|
+
color: var(--fg);
|
|
1307
|
+
line-height: 1.6;
|
|
1308
|
+
min-height: 100vh;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.container { max-width: 800px; margin: 0 auto; padding: 0 24px; }
|
|
1312
|
+
|
|
1313
|
+
header {
|
|
1314
|
+
padding: 80px 0 40px;
|
|
1315
|
+
text-align: center;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
h1 {
|
|
1319
|
+
font-size: 3rem;
|
|
1320
|
+
font-weight: 700;
|
|
1321
|
+
letter-spacing: -0.02em;
|
|
1322
|
+
margin-bottom: 16px;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.tagline {
|
|
1326
|
+
font-size: 1.25rem;
|
|
1327
|
+
color: var(--muted);
|
|
1328
|
+
max-width: 500px;
|
|
1329
|
+
margin: 0 auto 40px;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.install-box {
|
|
1333
|
+
background: var(--code-bg);
|
|
1334
|
+
border: 1px solid var(--border);
|
|
1335
|
+
border-radius: 8px;
|
|
1336
|
+
padding: 16px 24px;
|
|
1337
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
1338
|
+
font-size: 1rem;
|
|
1339
|
+
display: inline-block;
|
|
1340
|
+
margin-bottom: 8px;
|
|
1341
|
+
cursor: pointer;
|
|
1342
|
+
transition: border-color 0.2s;
|
|
1343
|
+
}
|
|
1344
|
+
.install-box:hover { border-color: var(--accent); }
|
|
1345
|
+
.install-box .prompt { color: var(--muted); }
|
|
1346
|
+
.install-box .cmd { color: var(--accent); }
|
|
1347
|
+
|
|
1348
|
+
.copy-hint {
|
|
1349
|
+
font-size: 0.85rem;
|
|
1350
|
+
color: var(--muted);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
section { padding: 60px 0; }
|
|
1354
|
+
|
|
1355
|
+
h2 {
|
|
1356
|
+
font-size: 1.5rem;
|
|
1357
|
+
font-weight: 600;
|
|
1358
|
+
margin-bottom: 24px;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.stats {
|
|
1362
|
+
display: grid;
|
|
1363
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
1364
|
+
gap: 16px;
|
|
1365
|
+
margin-bottom: 40px;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
.stat {
|
|
1369
|
+
background: var(--code-bg);
|
|
1370
|
+
border: 1px solid var(--border);
|
|
1371
|
+
border-radius: 8px;
|
|
1372
|
+
padding: 20px;
|
|
1373
|
+
text-align: center;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.stat-number {
|
|
1377
|
+
font-size: 2rem;
|
|
1378
|
+
font-weight: 700;
|
|
1379
|
+
color: var(--accent);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
.stat-label {
|
|
1383
|
+
font-size: 0.85rem;
|
|
1384
|
+
color: var(--muted);
|
|
1385
|
+
margin-top: 4px;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
.profiles {
|
|
1389
|
+
display: grid;
|
|
1390
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1391
|
+
gap: 16px;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.profile {
|
|
1395
|
+
background: var(--code-bg);
|
|
1396
|
+
border: 1px solid var(--border);
|
|
1397
|
+
border-radius: 8px;
|
|
1398
|
+
padding: 20px;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
.profile h3 {
|
|
1402
|
+
font-size: 1.1rem;
|
|
1403
|
+
margin-bottom: 8px;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
.profile p {
|
|
1407
|
+
color: var(--muted);
|
|
1408
|
+
font-size: 0.9rem;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.links {
|
|
1412
|
+
display: flex;
|
|
1413
|
+
gap: 16px;
|
|
1414
|
+
justify-content: center;
|
|
1415
|
+
margin-top: 40px;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.links a {
|
|
1419
|
+
color: var(--accent);
|
|
1420
|
+
text-decoration: none;
|
|
1421
|
+
font-size: 0.95rem;
|
|
1422
|
+
}
|
|
1423
|
+
.links a:hover { text-decoration: underline; }
|
|
1424
|
+
|
|
1425
|
+
footer {
|
|
1426
|
+
border-top: 1px solid var(--border);
|
|
1427
|
+
padding: 40px 0;
|
|
1428
|
+
text-align: center;
|
|
1429
|
+
color: var(--muted);
|
|
1430
|
+
font-size: 0.85rem;
|
|
1431
|
+
}
|
|
1432
|
+
</style>
|
|
1433
|
+
</head>
|
|
1434
|
+
<body>
|
|
1435
|
+
<div class="container">
|
|
1436
|
+
<header>
|
|
1437
|
+
<h1>ERNE</h1>
|
|
1438
|
+
<p class="tagline">Complete AI coding agent harness for React Native and Expo development</p>
|
|
1439
|
+
<div class="install-box" onclick="navigator.clipboard.writeText('npx erne-universal init')">
|
|
1440
|
+
<span class="prompt">$ </span><span class="cmd">npx erne-universal init</span>
|
|
1441
|
+
</div>
|
|
1442
|
+
<p class="copy-hint">Click to copy</p>
|
|
1443
|
+
</header>
|
|
1444
|
+
|
|
1445
|
+
<section>
|
|
1446
|
+
<div class="stats">
|
|
1447
|
+
<div class="stat">
|
|
1448
|
+
<div class="stat-number">8</div>
|
|
1449
|
+
<div class="stat-label">AI Agents</div>
|
|
1450
|
+
</div>
|
|
1451
|
+
<div class="stat">
|
|
1452
|
+
<div class="stat-number">16</div>
|
|
1453
|
+
<div class="stat-label">Commands</div>
|
|
1454
|
+
</div>
|
|
1455
|
+
<div class="stat">
|
|
1456
|
+
<div class="stat-number">5</div>
|
|
1457
|
+
<div class="stat-label">Rule Layers</div>
|
|
1458
|
+
</div>
|
|
1459
|
+
<div class="stat">
|
|
1460
|
+
<div class="stat-number">8</div>
|
|
1461
|
+
<div class="stat-label">Skills</div>
|
|
1462
|
+
</div>
|
|
1463
|
+
<div class="stat">
|
|
1464
|
+
<div class="stat-number">10</div>
|
|
1465
|
+
<div class="stat-label">MCP Configs</div>
|
|
1466
|
+
</div>
|
|
1467
|
+
<div class="stat">
|
|
1468
|
+
<div class="stat-number">3</div>
|
|
1469
|
+
<div class="stat-label">Contexts</div>
|
|
1470
|
+
</div>
|
|
1471
|
+
</div>
|
|
1472
|
+
</section>
|
|
1473
|
+
|
|
1474
|
+
<section>
|
|
1475
|
+
<h2>Hook Profiles</h2>
|
|
1476
|
+
<div class="profiles">
|
|
1477
|
+
<div class="profile">
|
|
1478
|
+
<h3>minimal</h3>
|
|
1479
|
+
<p>Fast iteration, minimal checks. Perfect for vibe coding and rapid prototyping.</p>
|
|
1480
|
+
</div>
|
|
1481
|
+
<div class="profile">
|
|
1482
|
+
<h3>standard</h3>
|
|
1483
|
+
<p>Balanced quality and speed. Recommended for most projects.</p>
|
|
1484
|
+
</div>
|
|
1485
|
+
<div class="profile">
|
|
1486
|
+
<h3>strict</h3>
|
|
1487
|
+
<p>Production-grade enforcement. Full linting, type checking, and security scans.</p>
|
|
1488
|
+
</div>
|
|
1489
|
+
</div>
|
|
1490
|
+
</section>
|
|
1491
|
+
|
|
1492
|
+
<section>
|
|
1493
|
+
<h2>Links</h2>
|
|
1494
|
+
<div class="links">
|
|
1495
|
+
<a href="https://github.com/JubaKitiashvili/everything-react-native-expo">GitHub</a>
|
|
1496
|
+
<a href="https://www.npmjs.com/package/erne-universal">npm</a>
|
|
1497
|
+
<a href="https://github.com/JubaKitiashvili/everything-react-native-expo/blob/main/docs/getting-started.md">Docs</a>
|
|
1498
|
+
</div>
|
|
1499
|
+
</section>
|
|
1500
|
+
|
|
1501
|
+
<footer>
|
|
1502
|
+
MIT License · ERNE Contributors
|
|
1503
|
+
</footer>
|
|
1504
|
+
</div>
|
|
1505
|
+
</body>
|
|
1506
|
+
</html>
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
**Notes:**
|
|
1510
|
+
- Single-page static site for `erne.dev` deployment on Vercel
|
|
1511
|
+
- Dark theme, minimal CSS, no JavaScript frameworks
|
|
1512
|
+
- Click-to-copy install command
|
|
1513
|
+
- Stats section mirrors spec Summary table
|
|
1514
|
+
- Responsive grid layout
|
|
1515
|
+
|
|
1516
|
+
---
|
|
1517
|
+
|
|
1518
|
+
### Task 8: Final Verification
|
|
1519
|
+
|
|
1520
|
+
#### 8.1: Complete File Inventory
|
|
1521
|
+
|
|
1522
|
+
Verify all Plan 4 files exist:
|
|
1523
|
+
|
|
1524
|
+
```
|
|
1525
|
+
Package Foundation (Task 1):
|
|
1526
|
+
[ ] package.json
|
|
1527
|
+
[ ] bin/cli.js
|
|
1528
|
+
[ ] LICENSE
|
|
1529
|
+
[ ] README.md
|
|
1530
|
+
|
|
1531
|
+
Installer Logic (Task 2):
|
|
1532
|
+
[ ] lib/init.js
|
|
1533
|
+
[ ] lib/update.js
|
|
1534
|
+
|
|
1535
|
+
Shell Installer (Task 3):
|
|
1536
|
+
[ ] install.sh
|
|
1537
|
+
|
|
1538
|
+
CI/CD (Task 4):
|
|
1539
|
+
[ ] .github/workflows/ci.yml
|
|
1540
|
+
[ ] .github/CONTRIBUTING.md
|
|
1541
|
+
[ ] .github/ISSUE_TEMPLATE/bug_report.md
|
|
1542
|
+
|
|
1543
|
+
Validation Scripts (Task 5):
|
|
1544
|
+
[ ] scripts/validate-all.js
|
|
1545
|
+
[ ] scripts/lint-content.js
|
|
1546
|
+
|
|
1547
|
+
Tests (Task 6):
|
|
1548
|
+
[ ] tests/cli.test.js
|
|
1549
|
+
[ ] tests/detection.test.js
|
|
1550
|
+
|
|
1551
|
+
Website (Task 7):
|
|
1552
|
+
[ ] website/index.html
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
#### 8.2: Functional Checks
|
|
1556
|
+
|
|
1557
|
+
```bash
|
|
1558
|
+
# CLI runs
|
|
1559
|
+
node bin/cli.js version
|
|
1560
|
+
node bin/cli.js help
|
|
1561
|
+
|
|
1562
|
+
# JSON valid
|
|
1563
|
+
node -e "require('./package.json')"
|
|
1564
|
+
|
|
1565
|
+
# Scripts run
|
|
1566
|
+
node scripts/validate-all.js
|
|
1567
|
+
node scripts/lint-content.js
|
|
1568
|
+
|
|
1569
|
+
# Tests pass
|
|
1570
|
+
node --test tests/
|
|
1571
|
+
|
|
1572
|
+
# install.sh is executable
|
|
1573
|
+
test -x install.sh
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
#### 8.3: npm Publish Readiness
|
|
1577
|
+
|
|
1578
|
+
```bash
|
|
1579
|
+
# Dry run package
|
|
1580
|
+
npm pack --dry-run
|
|
1581
|
+
|
|
1582
|
+
# Verify files array includes all content
|
|
1583
|
+
npm pack --dry-run 2>&1 | grep -c '.md\|.json\|.js\|.html'
|
|
1584
|
+
|
|
1585
|
+
# Verify bin field
|
|
1586
|
+
node -e "const p = require('./package.json'); console.log('bin:', p.bin)"
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
#### 8.4: Spec Cross-Reference
|
|
1590
|
+
|
|
1591
|
+
| Spec Requirement | Plan 4 Location |
|
|
1592
|
+
|-----------------|-----------------|
|
|
1593
|
+
| npm: `erne-universal` | package.json name field |
|
|
1594
|
+
| `npx erne-universal init` | bin/cli.js → lib/init.js |
|
|
1595
|
+
| 4-step install flow | lib/init.js (detect → profile → MCP → generate) |
|
|
1596
|
+
| `npx erne-universal update` | bin/cli.js → lib/update.js |
|
|
1597
|
+
| Shell installer | install.sh |
|
|
1598
|
+
| CI workflow | .github/workflows/ci.yml |
|
|
1599
|
+
| Website at erne.dev | website/index.html |
|
|
1600
|
+
| Semver versioning | package.json version field |
|
|
1601
|
+
| MIT License | LICENSE |
|
|
1602
|
+
|
|
1603
|
+
---
|
|
1604
|
+
|
|
1605
|
+
## Plan 4 Summary
|
|
1606
|
+
|
|
1607
|
+
| Chunk | Tasks | Files | Description |
|
|
1608
|
+
|-------|-------|-------|-------------|
|
|
1609
|
+
| 1 | 1–3 | 7 | Package scaffold, CLI, init/update logic, shell installer |
|
|
1610
|
+
| 2 | 4–6 | 7 | CI/CD, contributing guide, validation scripts, tests |
|
|
1611
|
+
| 3 | 7–8 | 1 + verification | Landing page, final checks |
|
|
1612
|
+
| **Total** | **8** | **~18** | **Complete distribution package** |
|
|
1613
|
+
|
|
1614
|
+
---
|
|
1615
|
+
|
|
1616
|
+
## All Plans Overview
|
|
1617
|
+
|
|
1618
|
+
| Plan | Focus | Files | Status |
|
|
1619
|
+
|------|-------|-------|--------|
|
|
1620
|
+
| Plan 1 | Core Infrastructure & Hook System | ~27 | Committed (`6099621`) |
|
|
1621
|
+
| Plan 2 | Content Layer (rules, agents, commands, contexts, MCP) | ~65 | Committed (`87e9b61`) |
|
|
1622
|
+
| Plan 3 | Skills & Knowledge Base | ~24 | Committed (`0961ec2`) |
|
|
1623
|
+
| Plan 4 | Install CLI & Distribution | ~18 | This plan |
|
|
1624
|
+
| **Total** | **Complete ERNE Package** | **~134** | |
|