mvagnon-agents 1.0.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/README.md +144 -0
- package/bootstrap.mjs +469 -0
- package/config/AGENTS.md +77 -0
- package/config/claudecode.settings.json +15 -0
- package/config/codex.config.toml +10 -0
- package/config/cursor.mcp.json +13 -0
- package/config/opencode.settings.json +20 -0
- package/config/rules/project.md +7 -0
- package/config/rules/react-hexagonal-architecture.md +50 -0
- package/config/skills/readme-writing/SKILL.md +97 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
```
|
|
2
|
+
__ _
|
|
3
|
+
_ ____ ____ _ __ _ _ _ ___ _ _ / /_ _ __ _ ___ _ _| |_ ___
|
|
4
|
+
| ' \ V / _` / _` | ' \/ _ \ ' \ / / _` / _` / -_) ' \ _(_-<
|
|
5
|
+
|_|_|_\_/\__,_\__, |_||_\___/_||_/_/\__,_\__, \___|_||_\__/__/
|
|
6
|
+
|___/ |___/
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
# AI Workflow
|
|
10
|
+
|
|
11
|
+
Shared configuration and conventions to bootstrap AI coding assistants on TypeScript/React projects. One repo, one command — every tool gets the same rules, skills, and MCP servers.
|
|
12
|
+
|
|
13
|
+
## Supported Tools
|
|
14
|
+
|
|
15
|
+
| Tool | Rules | Skills / Agents | Root File | MCP Config |
|
|
16
|
+
| ----------- | ------------------ | ---------------------------------------- | ----------- | -------------------- |
|
|
17
|
+
| Claude Code | `.claude/rules/` | `.claude/skills/`, `.claude/agents/` | `CLAUDE.md` | `.mcp.json` |
|
|
18
|
+
| OpenCode | `.opencode/rules/` | `.opencode/skills/`, `.opencode/agents/` | `AGENTS.md` | `opencode.json` |
|
|
19
|
+
| Cursor | `.cursor/rules/` | `.cursor/skills/`, `.cursor/agents/` | — | `.cursor/mcp.json` |
|
|
20
|
+
| Codex | — | `.agents/skills/` | `AGENTS.md` | `.codex/config.toml` |
|
|
21
|
+
|
|
22
|
+
## Tech Stack Coverage
|
|
23
|
+
|
|
24
|
+
| Category | Technologies |
|
|
25
|
+
| -------- | --------------------------------------------------------- |
|
|
26
|
+
| Frontend | React, Next.js, Vite, TypeScript, Tailwind CSS, ShadCN UI |
|
|
27
|
+
| Backend | Node.js, Fastify, Express, NestJS, Supabase |
|
|
28
|
+
| State | Zustand, TanStack Query, React Context |
|
|
29
|
+
| Testing | Vitest, Jest, React Testing Library |
|
|
30
|
+
| Tooling | ESLint, Prettier, pnpm, Docker |
|
|
31
|
+
|
|
32
|
+
## Getting Started
|
|
33
|
+
|
|
34
|
+
### Prerequisites
|
|
35
|
+
|
|
36
|
+
- Node.js >= 18
|
|
37
|
+
- pnpm or npm
|
|
38
|
+
|
|
39
|
+
### Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Run the bootstrap script with target path
|
|
49
|
+
pnpm run bootstrap ../my-project
|
|
50
|
+
|
|
51
|
+
# Or directly with node
|
|
52
|
+
node bootstrap.mjs ~/projects/my-app
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Interactive Walkthrough
|
|
56
|
+
|
|
57
|
+
The script prompts you to:
|
|
58
|
+
|
|
59
|
+
1. **Select technologies** — filter rules by stack (React, TypeScript)
|
|
60
|
+
2. **Select architecture** — optional (e.g. Hexagonal)
|
|
61
|
+
3. **Choose mode** — symlinks (recommended) or copy files
|
|
62
|
+
4. **Select target tools** — one or more (Claude Code, OpenCode, Cursor, Codex)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
AI Workflow → /Users/you/projects/my-app
|
|
66
|
+
|
|
67
|
+
◆ Select technologies
|
|
68
|
+
│ [ ] React - Components, hooks, patterns
|
|
69
|
+
│ [x] TypeScript - Conventions, testing
|
|
70
|
+
|
|
71
|
+
◆ Select custom architecture
|
|
72
|
+
│ ● None
|
|
73
|
+
|
|
74
|
+
◆ Use symlinks? Yes
|
|
75
|
+
|
|
76
|
+
◆ Select target tools
|
|
77
|
+
│ [x] Claude Code
|
|
78
|
+
│ [x] Cursor
|
|
79
|
+
│ [ ] OpenCode
|
|
80
|
+
│ [ ] Codex
|
|
81
|
+
|
|
82
|
+
◇ Claude Code Setup
|
|
83
|
+
│ Rules: 2 linked
|
|
84
|
+
│ Skills: 1 linked
|
|
85
|
+
│ Agents: 0 linked
|
|
86
|
+
│ CLAUDE.md: linked
|
|
87
|
+
│ .mcp.json: copied
|
|
88
|
+
│ .gitignore: entries added
|
|
89
|
+
|
|
90
|
+
◇ Cursor Setup
|
|
91
|
+
│ Rules: 2 linked
|
|
92
|
+
│ .cursor/mcp.json: copied
|
|
93
|
+
│ .gitignore: entries added
|
|
94
|
+
|
|
95
|
+
Done
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Project Structure
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
config/
|
|
102
|
+
├── rules/
|
|
103
|
+
│ ├── project.md # Generic project rules (always included)
|
|
104
|
+
│ └── react-hexagonal-architecture.md # Hexagonal architecture for React
|
|
105
|
+
├── skills/
|
|
106
|
+
│ └── readme-writing/ # README generation skill
|
|
107
|
+
├── agents/ # Custom agents (optional)
|
|
108
|
+
├── AGENTS.md # Master rules for all agents
|
|
109
|
+
├── claudecode.settings.json # Claude Code MCP config
|
|
110
|
+
├── opencode.settings.json # OpenCode MCP config
|
|
111
|
+
├── cursor.mcp.json # Cursor MCP config
|
|
112
|
+
└── codex.config.toml # Codex MCP config (TOML)
|
|
113
|
+
|
|
114
|
+
bootstrap.mjs # Interactive setup script
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Rule Filtering
|
|
118
|
+
|
|
119
|
+
Items without a technology or architecture prefix are considered generic and always included.
|
|
120
|
+
|
|
121
|
+
| Selection | Rules Included |
|
|
122
|
+
| ------------ | -------------------------------------- |
|
|
123
|
+
| None | Generic rules only (`project.md`) |
|
|
124
|
+
| React | Generic + `react-*.md` |
|
|
125
|
+
| TypeScript | Generic + `ts-*.md` |
|
|
126
|
+
| Hexagonal | Generic + items containing `hexagonal` |
|
|
127
|
+
| React + Both | Generic + `react-*.md` + `ts-*.md` |
|
|
128
|
+
|
|
129
|
+
## Symlinks vs Copy
|
|
130
|
+
|
|
131
|
+
| Mode | Pros | Cons |
|
|
132
|
+
| -------- | ----------------------------------- | ----------------------------- |
|
|
133
|
+
| Symlinks | Centralized updates, no duplication | Requires source repo presence |
|
|
134
|
+
| Copy | Self-contained, portable | Manual updates needed |
|
|
135
|
+
|
|
136
|
+
Items listed in `COPIED_RULES`, `COPIED_SKILLS`, or `COPIED_AGENTS` are always copied (never symlinked) to allow per-project customization.
|
|
137
|
+
|
|
138
|
+
## Manual Installation
|
|
139
|
+
|
|
140
|
+
If you prefer not to use the bootstrap script:
|
|
141
|
+
|
|
142
|
+
1. Copy the `config/` folder contents to your project
|
|
143
|
+
2. Rename files according to your target tool (see Supported Tools table)
|
|
144
|
+
3. Update `.gitignore` as needed
|
package/bootstrap.mjs
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// CONFIGURATION
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
// Technologies filter items by prefix (e.g., "react-*" matches "react-components")
|
|
15
|
+
const TECHNOLOGIES = [
|
|
16
|
+
{ value: "react", label: "React", hint: "Components, hooks, patterns" },
|
|
17
|
+
{ value: "ts", label: "TypeScript", hint: "Conventions, testing" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Architectures filter items by substring match
|
|
21
|
+
const ARCHITECTURES = [
|
|
22
|
+
{ value: "none", label: "None", hint: "No custom architecture" },
|
|
23
|
+
{ value: "hexagonal", label: "Hexagonal", hint: "Ports & adapters pattern" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Items always copied (never symlinked) to allow per-project customization
|
|
27
|
+
const COPIED_RULES = ["project"];
|
|
28
|
+
const COPIED_SKILLS = [];
|
|
29
|
+
const COPIED_AGENTS = [];
|
|
30
|
+
|
|
31
|
+
// Intermediate directory in the target project for per-project customizable resources
|
|
32
|
+
const INTERMEDIATE_DIR = ".mvagnon/agents";
|
|
33
|
+
|
|
34
|
+
// Tool definitions: directory structure, root files, config files, gitignore
|
|
35
|
+
const TOOLS = {
|
|
36
|
+
claudecode: {
|
|
37
|
+
value: "claudecode",
|
|
38
|
+
label: "Claude Code",
|
|
39
|
+
hint: "Anthropic's CLI for Claude",
|
|
40
|
+
paths: {
|
|
41
|
+
rules: ".claude/rules",
|
|
42
|
+
skills: ".claude/skills",
|
|
43
|
+
agents: ".claude/agents",
|
|
44
|
+
},
|
|
45
|
+
rootFiles: { "AGENTS.md": "CLAUDE.md" },
|
|
46
|
+
configFiles: { "claudecode.settings.json": ".mcp.json" },
|
|
47
|
+
gitignoreEntries: [".claude", "CLAUDE.md", ".mcp.json"],
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
opencode: {
|
|
51
|
+
value: "opencode",
|
|
52
|
+
label: "OpenCode",
|
|
53
|
+
hint: "Open-source AI coding assistant",
|
|
54
|
+
paths: {
|
|
55
|
+
rules: ".opencode/rules",
|
|
56
|
+
skills: ".opencode/skills",
|
|
57
|
+
agents: ".opencode/agents",
|
|
58
|
+
},
|
|
59
|
+
rootFiles: { "AGENTS.md": "AGENTS.md" },
|
|
60
|
+
configFiles: { "opencode.settings.json": "opencode.json" },
|
|
61
|
+
gitignoreEntries: [".opencode", "AGENTS.md", "opencode.json"],
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
cursor: {
|
|
65
|
+
value: "cursor",
|
|
66
|
+
label: "Cursor",
|
|
67
|
+
hint: "AI-powered code editor",
|
|
68
|
+
paths: {
|
|
69
|
+
rules: ".cursor/rules",
|
|
70
|
+
skills: ".cursor/skills",
|
|
71
|
+
agents: ".cursor/agents",
|
|
72
|
+
},
|
|
73
|
+
rootFiles: {},
|
|
74
|
+
configFiles: { "cursor.mcp.json": ".cursor/mcp.json" },
|
|
75
|
+
gitignoreEntries: [".cursor"],
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
codex: {
|
|
79
|
+
value: "codex",
|
|
80
|
+
label: "Codex",
|
|
81
|
+
hint: "OpenAI's coding agent CLI",
|
|
82
|
+
paths: {
|
|
83
|
+
skills: ".agents/skills",
|
|
84
|
+
},
|
|
85
|
+
rootFiles: { "AGENTS.md": "AGENTS.md" },
|
|
86
|
+
configFiles: { "codex.config.toml": ".codex/config.toml" },
|
|
87
|
+
gitignoreEntries: [".codex", ".agents", "AGENTS.md"],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// MAIN
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
async function main() {
|
|
96
|
+
const targetArg = process.argv[2];
|
|
97
|
+
|
|
98
|
+
if (!targetArg) {
|
|
99
|
+
console.error("Usage: ./bootstrap.sh <target-path>");
|
|
100
|
+
console.error("Example: ./bootstrap.sh ../my-project");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const targetPath = resolvePath(targetArg);
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(targetPath)) {
|
|
107
|
+
console.error(`Error: Directory not found: ${targetPath}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!fs.statSync(targetPath).isDirectory()) {
|
|
112
|
+
console.error(`Error: Path must be a directory: ${targetPath}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.clear();
|
|
117
|
+
|
|
118
|
+
const banner = [
|
|
119
|
+
" __ _ ",
|
|
120
|
+
" _ ____ ____ _ __ _ _ _ ___ _ _ / /_ _ __ _ ___ _ _| |_ ___",
|
|
121
|
+
"| ' \\ V / _` / _` | ' \\/ _ \\ ' \\ / / _` / _` / -_) ' \\ _(_-<",
|
|
122
|
+
"|_|_|_\\_/\\__,_\\__, |_||_\\___/_||_/_/\\__,_\\__, \\___|_||_\\__/__/",
|
|
123
|
+
" |___/ |___/ ",
|
|
124
|
+
];
|
|
125
|
+
console.log("\x1b[36m" + banner.join("\n") + "\x1b[0m\n");
|
|
126
|
+
|
|
127
|
+
p.intro(`AI Workflow → ${targetPath}`);
|
|
128
|
+
|
|
129
|
+
const config = await p.group(
|
|
130
|
+
{
|
|
131
|
+
techs: () =>
|
|
132
|
+
p.multiselect({
|
|
133
|
+
message: "Select technologies",
|
|
134
|
+
options: TECHNOLOGIES,
|
|
135
|
+
required: false,
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
archs: () =>
|
|
139
|
+
p.select({
|
|
140
|
+
message: "Select custom architecture",
|
|
141
|
+
options: ARCHITECTURES,
|
|
142
|
+
required: false,
|
|
143
|
+
}),
|
|
144
|
+
|
|
145
|
+
useSymlinks: () =>
|
|
146
|
+
p.confirm({
|
|
147
|
+
message:
|
|
148
|
+
"Use symlinks? (yes: `.gitignore` updated, no: config versioned)",
|
|
149
|
+
initialValue: true,
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
tools: () =>
|
|
153
|
+
p.multiselect({
|
|
154
|
+
message: "Select target tools",
|
|
155
|
+
options: Object.values(TOOLS).map((t) => ({
|
|
156
|
+
value: t.value,
|
|
157
|
+
label: t.label,
|
|
158
|
+
hint: t.hint,
|
|
159
|
+
})),
|
|
160
|
+
required: true,
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
onCancel: () => {
|
|
165
|
+
p.cancel("Setup cancelled");
|
|
166
|
+
process.exit(0);
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const selectedTechs = config.techs || [];
|
|
172
|
+
const selectedArchs = config.archs ? [config.archs] : [];
|
|
173
|
+
const useSymlinks = config.useSymlinks;
|
|
174
|
+
const selectedTools = config.tools.map((key) => TOOLS[key]);
|
|
175
|
+
const s = p.spinner();
|
|
176
|
+
const safeCopy = (src, tgt) =>
|
|
177
|
+
copyWithConfirm(src, tgt, { spinner: s, projectRoot: targetPath });
|
|
178
|
+
const linkOrCopy = useSymlinks
|
|
179
|
+
? async (src, tgt) => createSymlink(src, tgt)
|
|
180
|
+
: safeCopy;
|
|
181
|
+
|
|
182
|
+
s.start(useSymlinks ? "Creating symlinks" : "Copying files");
|
|
183
|
+
|
|
184
|
+
const mode = useSymlinks ? "linked" : "copied";
|
|
185
|
+
const summaryLines = [];
|
|
186
|
+
const processedIntermediateFiles = new Set();
|
|
187
|
+
|
|
188
|
+
for (const tool of selectedTools) {
|
|
189
|
+
const stats = { rules: 0, skills: 0, agents: 0 };
|
|
190
|
+
const { paths } = tool;
|
|
191
|
+
|
|
192
|
+
for (const dir of Object.values(paths)) {
|
|
193
|
+
fs.mkdirSync(path.join(targetPath, dir), { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (paths.rules) {
|
|
197
|
+
stats.rules = await linkMatchingItems(
|
|
198
|
+
path.join(__dirname, "config", "rules"),
|
|
199
|
+
path.join(targetPath, paths.rules),
|
|
200
|
+
selectedTechs,
|
|
201
|
+
selectedArchs,
|
|
202
|
+
linkOrCopy,
|
|
203
|
+
{
|
|
204
|
+
filterExtension: ".md",
|
|
205
|
+
copiedItems: COPIED_RULES,
|
|
206
|
+
intermediateDir: path.join(targetPath, INTERMEDIATE_DIR, "rules"),
|
|
207
|
+
processedIntermediateFiles,
|
|
208
|
+
safeCopy,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (paths.skills) {
|
|
214
|
+
stats.skills = await linkMatchingItems(
|
|
215
|
+
path.join(__dirname, "config", "skills"),
|
|
216
|
+
path.join(targetPath, paths.skills),
|
|
217
|
+
selectedTechs,
|
|
218
|
+
selectedArchs,
|
|
219
|
+
linkOrCopy,
|
|
220
|
+
{
|
|
221
|
+
directoriesOnly: true,
|
|
222
|
+
copiedItems: COPIED_SKILLS,
|
|
223
|
+
intermediateDir: path.join(targetPath, INTERMEDIATE_DIR, "skills"),
|
|
224
|
+
processedIntermediateFiles,
|
|
225
|
+
safeCopy,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (paths.agents) {
|
|
231
|
+
stats.agents = await linkMatchingItems(
|
|
232
|
+
path.join(__dirname, "config", "agents"),
|
|
233
|
+
path.join(targetPath, paths.agents),
|
|
234
|
+
selectedTechs,
|
|
235
|
+
selectedArchs,
|
|
236
|
+
linkOrCopy,
|
|
237
|
+
{
|
|
238
|
+
directoriesOnly: true,
|
|
239
|
+
copiedItems: COPIED_AGENTS,
|
|
240
|
+
intermediateDir: path.join(targetPath, INTERMEDIATE_DIR, "agents"),
|
|
241
|
+
processedIntermediateFiles,
|
|
242
|
+
safeCopy,
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const [src, dest] of Object.entries(tool.rootFiles)) {
|
|
248
|
+
const srcPath = path.join(__dirname, "config", src);
|
|
249
|
+
if (fs.existsSync(srcPath)) {
|
|
250
|
+
await linkOrCopy(srcPath, path.join(targetPath, dest));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const [src, dest] of Object.entries(tool.configFiles)) {
|
|
255
|
+
const srcPath = path.join(__dirname, "config", src);
|
|
256
|
+
if (fs.existsSync(srcPath)) {
|
|
257
|
+
const destPath = path.join(targetPath, dest);
|
|
258
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
259
|
+
await safeCopy(srcPath, destPath);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (useSymlinks) {
|
|
264
|
+
updateGitignore(targetPath, tool);
|
|
265
|
+
addGitignoreEntry(targetPath, INTERMEDIATE_DIR, "mvagnon/agents");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const toolSummary = [
|
|
269
|
+
`Rules: ${stats.rules} ${mode}`,
|
|
270
|
+
paths.skills ? `Skills: ${stats.skills} ${mode}` : null,
|
|
271
|
+
paths.agents ? `Agents: ${stats.agents} ${mode}` : null,
|
|
272
|
+
...Object.values(tool.rootFiles).map((f) => `${f}: ${mode}`),
|
|
273
|
+
...Object.values(tool.configFiles).map((f) => `${f}: copied`),
|
|
274
|
+
useSymlinks ? `.gitignore: entries added` : `.gitignore: not modified`,
|
|
275
|
+
].filter(Boolean);
|
|
276
|
+
|
|
277
|
+
summaryLines.push({ tool, lines: toolSummary });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
s.stop("Setup complete");
|
|
281
|
+
|
|
282
|
+
for (const { tool, lines } of summaryLines) {
|
|
283
|
+
p.note(lines.join("\n"), `${tool.label} Setup`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
p.note(
|
|
287
|
+
[
|
|
288
|
+
"1. Modify `project.md` to add project-specific rules;",
|
|
289
|
+
"2. Add rules, skills, agents, MCPs or plugins based on your needs for each tool.",
|
|
290
|
+
].join("\n"),
|
|
291
|
+
"Next Steps",
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
p.outro("Done");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function resolvePath(inputPath) {
|
|
298
|
+
if (inputPath.startsWith("~")) {
|
|
299
|
+
inputPath = inputPath.replace("~", process.env.HOME);
|
|
300
|
+
}
|
|
301
|
+
return path.resolve(inputPath);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isTechOrArchSpecific(name) {
|
|
305
|
+
const allTechs = TECHNOLOGIES.map((t) => t.value);
|
|
306
|
+
const allArchs = ARCHITECTURES.filter((a) => a.value !== "none").map(
|
|
307
|
+
(a) => a.value,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
allTechs.some((tech) => name.startsWith(`${tech}-`)) ||
|
|
312
|
+
allArchs.some((arch) => name.includes(arch))
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function shouldInclude(name, selectedTechs, selectedArchs) {
|
|
317
|
+
if (!isTechOrArchSpecific(name)) return true;
|
|
318
|
+
|
|
319
|
+
const matchesTech = selectedTechs.some((tech) => name.startsWith(`${tech}-`));
|
|
320
|
+
const matchesArch = selectedArchs
|
|
321
|
+
.filter((arch) => arch !== "none")
|
|
322
|
+
.some((arch) => name.includes(arch));
|
|
323
|
+
|
|
324
|
+
return matchesTech || matchesArch;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function linkMatchingItems(
|
|
328
|
+
sourceDir,
|
|
329
|
+
targetDir,
|
|
330
|
+
selectedTechs,
|
|
331
|
+
selectedArchs,
|
|
332
|
+
linkOrCopy,
|
|
333
|
+
{
|
|
334
|
+
filterExtension,
|
|
335
|
+
directoriesOnly,
|
|
336
|
+
copiedItems = [],
|
|
337
|
+
intermediateDir,
|
|
338
|
+
safeCopy,
|
|
339
|
+
processedIntermediateFiles = new Set(),
|
|
340
|
+
} = {},
|
|
341
|
+
) {
|
|
342
|
+
if (!fs.existsSync(sourceDir)) return 0;
|
|
343
|
+
|
|
344
|
+
let count = 0;
|
|
345
|
+
|
|
346
|
+
for (const entry of fs.readdirSync(sourceDir)) {
|
|
347
|
+
const fullPath = path.join(sourceDir, entry);
|
|
348
|
+
const isDir = fs.statSync(fullPath).isDirectory();
|
|
349
|
+
|
|
350
|
+
if (directoriesOnly && !isDir) continue;
|
|
351
|
+
if (filterExtension && !entry.endsWith(filterExtension)) continue;
|
|
352
|
+
|
|
353
|
+
const name = entry.replace(/\.md$/, "");
|
|
354
|
+
|
|
355
|
+
if (!shouldInclude(name, selectedTechs, selectedArchs)) continue;
|
|
356
|
+
|
|
357
|
+
const isCopied = copiedItems.includes(name) && intermediateDir;
|
|
358
|
+
|
|
359
|
+
if (isCopied) {
|
|
360
|
+
const intermediatePath = path.join(intermediateDir, entry);
|
|
361
|
+
fs.mkdirSync(intermediateDir, { recursive: true });
|
|
362
|
+
|
|
363
|
+
if (!processedIntermediateFiles.has(intermediatePath)) {
|
|
364
|
+
await safeCopy(fullPath, intermediatePath);
|
|
365
|
+
processedIntermediateFiles.add(intermediatePath);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await linkOrCopy(intermediatePath, path.join(targetDir, entry));
|
|
369
|
+
} else {
|
|
370
|
+
await linkOrCopy(fullPath, path.join(targetDir, entry));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
count++;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return count;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function copyWithConfirm(source, target, { spinner, projectRoot } = {}) {
|
|
380
|
+
if (fs.existsSync(target)) {
|
|
381
|
+
try {
|
|
382
|
+
if (!fs.lstatSync(target).isSymbolicLink()) {
|
|
383
|
+
const label = projectRoot
|
|
384
|
+
? path.relative(projectRoot, target)
|
|
385
|
+
: path.basename(target);
|
|
386
|
+
if (spinner) spinner.stop("Existing file found");
|
|
387
|
+
const overwrite = await p.confirm({
|
|
388
|
+
message: `${label} already exists. Overwrite?`,
|
|
389
|
+
initialValue: false,
|
|
390
|
+
});
|
|
391
|
+
if (p.isCancel(overwrite)) {
|
|
392
|
+
p.cancel("Setup cancelled");
|
|
393
|
+
process.exit(0);
|
|
394
|
+
}
|
|
395
|
+
if (spinner) spinner.start("Continuing setup");
|
|
396
|
+
if (!overwrite) return false;
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
// lstat failed, proceed with copy
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
copyPath(source, target);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function removePath(target) {
|
|
408
|
+
if (
|
|
409
|
+
fs.existsSync(target) ||
|
|
410
|
+
fs.lstatSync(target, { throwIfNoEntry: false })
|
|
411
|
+
) {
|
|
412
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function createSymlink(source, target) {
|
|
417
|
+
removePath(target);
|
|
418
|
+
fs.symlinkSync(source, target);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function copyPath(source, target) {
|
|
422
|
+
removePath(target);
|
|
423
|
+
|
|
424
|
+
if (fs.statSync(source).isDirectory()) {
|
|
425
|
+
fs.cpSync(source, target, { recursive: true });
|
|
426
|
+
} else {
|
|
427
|
+
fs.copyFileSync(source, target);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function updateGitignore(targetPath, tool) {
|
|
432
|
+
const gitignorePath = path.join(targetPath, ".gitignore");
|
|
433
|
+
const sectionHeader = `# ${tool.label} Configuration`;
|
|
434
|
+
let content = "";
|
|
435
|
+
|
|
436
|
+
if (fs.existsSync(gitignorePath)) {
|
|
437
|
+
content = fs.readFileSync(gitignorePath, "utf-8");
|
|
438
|
+
|
|
439
|
+
if (content.includes(sectionHeader)) return;
|
|
440
|
+
|
|
441
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
442
|
+
content += "\n";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
content += sectionHeader + "\n";
|
|
446
|
+
content += tool.gitignoreEntries.join("\n") + "\n";
|
|
447
|
+
|
|
448
|
+
fs.writeFileSync(gitignorePath, content);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function addGitignoreEntry(targetPath, entry, sectionComment) {
|
|
452
|
+
const gitignorePath = path.join(targetPath, ".gitignore");
|
|
453
|
+
let content = "";
|
|
454
|
+
|
|
455
|
+
if (fs.existsSync(gitignorePath)) {
|
|
456
|
+
content = fs.readFileSync(gitignorePath, "utf-8");
|
|
457
|
+
if (content.split("\n").some((line) => line.trim() === entry)) return;
|
|
458
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
459
|
+
content += "\n";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (sectionComment) {
|
|
463
|
+
content += `# ${sectionComment}\n`;
|
|
464
|
+
}
|
|
465
|
+
content += entry + "\n";
|
|
466
|
+
fs.writeFileSync(gitignorePath, content);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
main().catch(console.error);
|
package/config/AGENTS.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Master Rules
|
|
2
|
+
|
|
3
|
+
## Project-Specific Rules
|
|
4
|
+
|
|
5
|
+
Find the project-specific master rules here → `project.md`.
|
|
6
|
+
|
|
7
|
+
## Before Writing Code
|
|
8
|
+
|
|
9
|
+
Before writing or modifying code that uses any external library:
|
|
10
|
+
|
|
11
|
+
1. **First**, resolve the library with _Context7 MCP_ to get the up-to-date documentation;
|
|
12
|
+
2. **Only** if Context7 returns no results or insufficient info, fall back to _Exa MCP_ as a secondary source.
|
|
13
|
+
|
|
14
|
+
> **Important:** ALWAYS prefer Context7 documentation over training data as the source of truth, especially for critical features (security, billing, authentication, payments, etc.).
|
|
15
|
+
|
|
16
|
+
## While Writing Code
|
|
17
|
+
|
|
18
|
+
### Code Conventions
|
|
19
|
+
|
|
20
|
+
- ALWAYS group all imports in a single block at the top of the file, with no blank lines between them;
|
|
21
|
+
- ALWAYS organize the code into blocs with each bloc representing a distinct logic. Each bloc is separated with a line jump (example below).
|
|
22
|
+
|
|
23
|
+
> **Example (React):**
|
|
24
|
+
>
|
|
25
|
+
> ```tsx
|
|
26
|
+
> export function UserProfile({ userId }: UserProfileProps) {
|
|
27
|
+
> const { data: user, isLoading } = useUser(userId);
|
|
28
|
+
> const { mutate: updateUser } = useUpdateUser();
|
|
29
|
+
>
|
|
30
|
+
> const displayName = user?.name ?? "Anonymous";
|
|
31
|
+
> const initials = displayName.slice(0, 2).toUpperCase();
|
|
32
|
+
>
|
|
33
|
+
> const handleSave = (values: UserFormValues) => {
|
|
34
|
+
> updateUser({ id: userId, ...values });
|
|
35
|
+
> };
|
|
36
|
+
>
|
|
37
|
+
> if (isLoading) return <Skeleton />;
|
|
38
|
+
>
|
|
39
|
+
> return (
|
|
40
|
+
> <Card>
|
|
41
|
+
> <Avatar initials={initials} />
|
|
42
|
+
> <UserForm defaultValues={user} onSubmit={handleSave} />
|
|
43
|
+
> </Card>
|
|
44
|
+
> );
|
|
45
|
+
> }
|
|
46
|
+
> ```
|
|
47
|
+
>
|
|
48
|
+
> **Example (FastAPI):**
|
|
49
|
+
>
|
|
50
|
+
> ```python
|
|
51
|
+
> @router.get("/users/{user_id}")
|
|
52
|
+
> async def get_user(user_id: UUID, db: Session = Depends(get_db)) -> UserResponse:
|
|
53
|
+
> """Retrieve a user by ID."""
|
|
54
|
+
> user = await db.get(User, user_id)
|
|
55
|
+
>
|
|
56
|
+
> if not user:
|
|
57
|
+
> raise HTTPException(status_code=404, detail="User not found")
|
|
58
|
+
>
|
|
59
|
+
> return UserResponse.model_validate(user)
|
|
60
|
+
> ```
|
|
61
|
+
|
|
62
|
+
### Documentation & Logging
|
|
63
|
+
|
|
64
|
+
- Write TSDoc/docstrings ONLY for **exported** functions, classes, types, and interfaces;
|
|
65
|
+
- NEVER add comments on trivial logic (hook, constant declaration, etc.);
|
|
66
|
+
- NEVER add logs unless requested;
|
|
67
|
+
- ALWAYS comment complex logic (RegEx, etc.);
|
|
68
|
+
- Generic code (style variables, UI components, etc.) should ALWAYS be reusable.
|
|
69
|
+
|
|
70
|
+
## After Writing Code
|
|
71
|
+
|
|
72
|
+
CRITICAL: ALWAYS check the following after modifying the codebase. Iterate until complete:
|
|
73
|
+
|
|
74
|
+
- [ ] KISS (Keep It Stupid Simple) principle is applied
|
|
75
|
+
- [ ] DRY (Don't Repeat Yourself) principle is applied
|
|
76
|
+
- [ ] No logs were added without prior consent
|
|
77
|
+
- [ ] Code is properly spaced into logical blocks
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"exa": {
|
|
4
|
+
"type": "http",
|
|
5
|
+
"url": "https://mcp.exa.ai/mcp"
|
|
6
|
+
},
|
|
7
|
+
"context7": {
|
|
8
|
+
"type": "http",
|
|
9
|
+
"url": "https://mcp.context7.com/mcp",
|
|
10
|
+
"headers": {
|
|
11
|
+
"CONTEXT7_API_KEY": "ctx7sk-09be8e62-174a-424a-81ef-166f2b2c94a2"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"instructions": [".opencode/rules/*.md"],
|
|
4
|
+
"mcp": {
|
|
5
|
+
"exa": {
|
|
6
|
+
"enabled": true,
|
|
7
|
+
"type": "remote",
|
|
8
|
+
"url": "https://mcp.exa.ai/mcp",
|
|
9
|
+
"headers": {}
|
|
10
|
+
},
|
|
11
|
+
"context7": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"type": "remote",
|
|
14
|
+
"url": "https://mcp.context7.com/mcp",
|
|
15
|
+
"headers": {
|
|
16
|
+
"CONTEXT7_API_KEY": "ctx7sk-09be8e62-174a-424a-81ef-166f2b2c94a2"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# React Hexagonal Architecture
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/
|
|
7
|
+
├── application/ # UI — Can freely use React
|
|
8
|
+
│ ├── components/ # React components
|
|
9
|
+
│ ├── hooks/ # Business hooks (consume domain)
|
|
10
|
+
│ ├── pages/ # Route components
|
|
11
|
+
│ └── providers/ # Context providers
|
|
12
|
+
├── domain/ # Business — Pure TypeScript, ZERO React dependencies
|
|
13
|
+
│ ├── entities/ # Business models
|
|
14
|
+
│ ├── ports/ # Interfaces/contracts
|
|
15
|
+
│ └── lib/ # Pure functions
|
|
16
|
+
└── infrastructure/ # External — Implements ports
|
|
17
|
+
├── api/ # HTTP/GraphQL clients
|
|
18
|
+
└── config/ # Configuration, feature flags
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Layer Rules
|
|
22
|
+
|
|
23
|
+
### Domain (core)
|
|
24
|
+
|
|
25
|
+
- **No** React dependencies (no JSX, no hooks)
|
|
26
|
+
- Pure functions, testable in isolation
|
|
27
|
+
- Defines `ports` (interfaces) that infrastructure implements
|
|
28
|
+
|
|
29
|
+
### Application (UI)
|
|
30
|
+
|
|
31
|
+
- Consumes domain via custom hooks
|
|
32
|
+
- Components split by responsibility
|
|
33
|
+
- State management: `useState` → `useReducer` → Context → Zustand (progressive escalation)
|
|
34
|
+
|
|
35
|
+
### Infrastructure (adapters)
|
|
36
|
+
|
|
37
|
+
- Implements ports defined by domain
|
|
38
|
+
- Handles side effects (API, storage, analytics)
|
|
39
|
+
- Transforms external responses into domain entities
|
|
40
|
+
|
|
41
|
+
## Dependency Principle
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Application → Domain ← Infrastructure
|
|
45
|
+
↑
|
|
46
|
+
(ports/interfaces)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- Application and Infrastructure depend on Domain
|
|
50
|
+
- Domain depends on nothing external
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: readme-writing
|
|
3
|
+
description: Write README.md for a project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# README Generator Skill
|
|
7
|
+
|
|
8
|
+
Generates or updates the `README.md` file at the project root.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. **Analyze the project**
|
|
13
|
+
- Read `package.json` (or equivalent) for metadata;
|
|
14
|
+
- Read `.claude/CLAUDE.md` and `.claude/rules/` to understand context;
|
|
15
|
+
- Scan folder structure (`src/`, `lib/`, `app/`, etc.);
|
|
16
|
+
- Identify the tech stack in use.
|
|
17
|
+
|
|
18
|
+
2. **Collect existing information**
|
|
19
|
+
- If `README.md` exists: read it to preserve custom content;
|
|
20
|
+
- Identify sections to keep vs regenerate.
|
|
21
|
+
|
|
22
|
+
3. **Generate content**
|
|
23
|
+
- Follow the structure below;
|
|
24
|
+
- Use a professional and concise tone;
|
|
25
|
+
- No emojis unless explicitly requested.
|
|
26
|
+
|
|
27
|
+
## README Structure
|
|
28
|
+
|
|
29
|
+
```markdown
|
|
30
|
+
# Project Name
|
|
31
|
+
|
|
32
|
+
Short and impactful description (1-2 sentences).
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- Feature 1;
|
|
37
|
+
- Feature 2.
|
|
38
|
+
|
|
39
|
+
## Tech Stack
|
|
40
|
+
|
|
41
|
+
| Category | Technologies |
|
|
42
|
+
| -------- | ------------ |
|
|
43
|
+
| Frontend | ... |
|
|
44
|
+
| Backend | ... |
|
|
45
|
+
|
|
46
|
+
## Getting Started
|
|
47
|
+
|
|
48
|
+
### Prerequisites
|
|
49
|
+
|
|
50
|
+
- Node.js >= X.X;
|
|
51
|
+
- pnpm/npm.
|
|
52
|
+
|
|
53
|
+
### Environment Files
|
|
54
|
+
|
|
55
|
+
#### `.env`
|
|
56
|
+
|
|
57
|
+
\`\`\`text
|
|
58
|
+
ENV_VAR=
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
#### `.env.local`
|
|
62
|
+
|
|
63
|
+
\`\`\`text
|
|
64
|
+
ENV_LOCAL_VAR=
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
### Installation
|
|
68
|
+
|
|
69
|
+
\`\`\`bash
|
|
70
|
+
pnpm install
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
### Development
|
|
74
|
+
|
|
75
|
+
\`\`\`bash
|
|
76
|
+
pnpm dev
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
## Project Structure
|
|
80
|
+
|
|
81
|
+
\`\`\`
|
|
82
|
+
src/
|
|
83
|
+
├── ...
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
## Scripts
|
|
87
|
+
|
|
88
|
+
| Script | Description |
|
|
89
|
+
| ------ | ----------- |
|
|
90
|
+
| `dev` | ... |
|
|
91
|
+
|
|
92
|
+
## Rules
|
|
93
|
+
|
|
94
|
+
- **Never invent** features not present in the code;
|
|
95
|
+
- **Preserve** existing custom sections (Contributing, Acknowledgments, etc.);
|
|
96
|
+
- **Adapt** the structure based on project type (lib, app, monorepo);
|
|
97
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mvagnon-agents",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared configuration and conventions to bootstrap AI coding assistants.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"opencode-workflow": "./bootstrap.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bootstrap.mjs",
|
|
11
|
+
"config/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"bootstrap": "node bootstrap.mjs"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"coding-assistant",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"cursor",
|
|
21
|
+
"opencode",
|
|
22
|
+
"codex",
|
|
23
|
+
"bootstrap",
|
|
24
|
+
"typescript",
|
|
25
|
+
"react"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@clack/prompts": "^0.11.0"
|
|
30
|
+
}
|
|
31
|
+
}
|