buildcrew 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +60 -87
- package/README.md +71 -104
- package/bin/setup.js +269 -224
- package/package.json +3 -1
- package/templates/api-spec.md +52 -0
- package/templates/architecture.md +49 -0
- package/templates/design-system.md +66 -0
- package/templates/env-vars.md +36 -0
- package/templates/erd.md +32 -0
- package/templates/glossary.md +27 -0
- package/templates/user-flow.md +40 -0
package/bin/setup.js
CHANGED
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
import { readdir, copyFile, mkdir, readFile, writeFile, access } from "fs/promises";
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { createInterface } from "readline";
|
|
7
6
|
|
|
8
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
8
|
const AGENTS_SRC = join(__dirname, "..", "agents");
|
|
10
9
|
const TEMPLATES_SRC = join(__dirname, "..", "templates");
|
|
11
10
|
const TARGET_DIR = join(process.cwd(), ".claude", "agents");
|
|
12
11
|
const HARNESS_DIR = join(process.cwd(), ".claude", "harness");
|
|
13
|
-
const VERSION = "1.
|
|
12
|
+
const VERSION = "1.1.0";
|
|
14
13
|
|
|
15
14
|
const RESET = "\x1b[0m";
|
|
16
15
|
const BOLD = "\x1b[1m";
|
|
@@ -41,246 +40,330 @@ const TEMPLATES = {
|
|
|
41
40
|
"env-vars": { file: "env-vars.md", desc: "Environment variables & secrets guide", category: "system" },
|
|
42
41
|
};
|
|
43
42
|
|
|
44
|
-
// ───
|
|
45
|
-
|
|
46
|
-
function createPrompt() {
|
|
47
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
48
|
-
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
49
|
-
const close = () => rl.close();
|
|
50
|
-
return { ask, close };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ─── Auto-detect project info ───
|
|
43
|
+
// ─── Deep auto-detect ───
|
|
54
44
|
|
|
55
45
|
async function detectProject() {
|
|
56
|
-
const info = {
|
|
46
|
+
const info = {
|
|
47
|
+
name: "", description: "", stack: [], framework: "",
|
|
48
|
+
hasTS: false, hasTailwind: false, hasI18n: false, hasAuth: false, hasDB: false,
|
|
49
|
+
hasPayments: false, hasAI: false, deploy: "", dbName: "", authName: "", paymentName: "",
|
|
50
|
+
components: [], apiRoutes: [], locales: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// package.json
|
|
57
54
|
try {
|
|
58
55
|
const pkg = JSON.parse(await readFile(join(process.cwd(), "package.json"), "utf8"));
|
|
59
56
|
info.name = pkg.name || "";
|
|
57
|
+
info.description = pkg.description || "";
|
|
60
58
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
59
|
+
|
|
60
|
+
// Framework
|
|
61
61
|
if (allDeps["next"]) { info.framework = "Next.js"; info.stack.push("Next.js"); }
|
|
62
62
|
else if (allDeps["nuxt"]) { info.framework = "Nuxt"; info.stack.push("Nuxt"); }
|
|
63
63
|
else if (allDeps["react"]) { info.framework = "React"; info.stack.push("React"); }
|
|
64
64
|
else if (allDeps["vue"]) { info.framework = "Vue"; info.stack.push("Vue"); }
|
|
65
65
|
else if (allDeps["svelte"] || allDeps["@sveltejs/kit"]) { info.framework = "SvelteKit"; info.stack.push("SvelteKit"); }
|
|
66
66
|
else if (allDeps["express"]) { info.framework = "Express"; info.stack.push("Express"); }
|
|
67
|
+
|
|
68
|
+
// Core
|
|
67
69
|
if (allDeps["typescript"]) { info.hasTS = true; info.stack.push("TypeScript"); }
|
|
68
70
|
if (allDeps["tailwindcss"]) { info.hasTailwind = true; info.stack.push("TailwindCSS"); }
|
|
69
|
-
if (allDeps["
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
if (allDeps["framer-motion"]) info.stack.push("Framer Motion");
|
|
72
|
+
|
|
73
|
+
// i18n
|
|
74
|
+
if (allDeps["next-intl"] || allDeps["i18next"] || allDeps["react-intl"] || allDeps["next-i18next"]) {
|
|
75
|
+
info.hasI18n = true; info.stack.push("i18n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// DB
|
|
79
|
+
if (allDeps["@supabase/supabase-js"]) { info.hasDB = true; info.dbName = "Supabase"; info.stack.push("Supabase"); }
|
|
80
|
+
else if (allDeps["prisma"] || allDeps["@prisma/client"]) { info.hasDB = true; info.dbName = "Prisma"; info.stack.push("Prisma"); }
|
|
81
|
+
else if (allDeps["drizzle-orm"]) { info.hasDB = true; info.dbName = "Drizzle"; info.stack.push("Drizzle"); }
|
|
82
|
+
else if (allDeps["mongoose"]) { info.hasDB = true; info.dbName = "MongoDB"; info.stack.push("MongoDB"); }
|
|
83
|
+
|
|
84
|
+
// Auth
|
|
85
|
+
if (allDeps["next-auth"] || allDeps["@auth/core"]) { info.hasAuth = true; info.authName = "NextAuth"; info.stack.push("NextAuth"); }
|
|
86
|
+
else if (allDeps["@supabase/auth-helpers-nextjs"] || allDeps["@supabase/ssr"]) { info.hasAuth = true; info.authName = "Supabase Auth"; info.stack.push("Supabase Auth"); }
|
|
87
|
+
else if (allDeps["firebase"]) { info.hasAuth = true; info.authName = "Firebase Auth"; info.stack.push("Firebase"); }
|
|
88
|
+
|
|
89
|
+
// Payments
|
|
90
|
+
if (allDeps["stripe"]) { info.hasPayments = true; info.paymentName = "Stripe"; info.stack.push("Stripe"); }
|
|
91
|
+
else if (allDeps["@paddle/paddle-js"]) { info.hasPayments = true; info.paymentName = "Paddle"; info.stack.push("Paddle"); }
|
|
92
|
+
else if (allDeps["@tosspayments/payment-sdk"]) { info.hasPayments = true; info.paymentName = "Toss Payments"; info.stack.push("Toss Payments"); }
|
|
93
|
+
|
|
94
|
+
// AI
|
|
95
|
+
if (allDeps["openai"]) { info.hasAI = true; info.stack.push("OpenAI"); }
|
|
96
|
+
else if (allDeps["@anthropic-ai/sdk"]) { info.hasAI = true; info.stack.push("Anthropic"); }
|
|
97
|
+
else if (allDeps["@google/generative-ai"]) { info.hasAI = true; info.stack.push("Google AI"); }
|
|
75
98
|
} catch {}
|
|
76
|
-
return info;
|
|
77
|
-
}
|
|
78
99
|
|
|
79
|
-
//
|
|
100
|
+
// Detect deploy from vercel.json / netlify.toml / fly.toml
|
|
101
|
+
if (await exists(join(process.cwd(), "vercel.json")) || await exists(join(process.cwd(), ".vercel"))) info.deploy = "Vercel";
|
|
102
|
+
else if (await exists(join(process.cwd(), "netlify.toml"))) info.deploy = "Netlify";
|
|
103
|
+
else if (await exists(join(process.cwd(), "fly.toml"))) info.deploy = "Fly.io";
|
|
104
|
+
else if (await exists(join(process.cwd(), "Dockerfile"))) info.deploy = "Docker";
|
|
80
105
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (items.length === 0) continue;
|
|
88
|
-
log(` ${CYAN}${label}${RESET}`);
|
|
89
|
-
for (const [name, info] of items) {
|
|
90
|
-
const installed = await exists(join(HARNESS_DIR, `${name}.md`));
|
|
91
|
-
const status = installed ? `${GREEN}installed${RESET}` : `${DIM}not installed${RESET}`;
|
|
92
|
-
log(` ${BOLD}${name.padEnd(16)}${RESET} ${status} ${DIM}${info.desc}${RESET}`);
|
|
93
|
-
}
|
|
94
|
-
log("");
|
|
106
|
+
// Scan components
|
|
107
|
+
try {
|
|
108
|
+
const compsDir = join(process.cwd(), "src", "components");
|
|
109
|
+
if (await exists(compsDir)) {
|
|
110
|
+
const files = await readdir(compsDir);
|
|
111
|
+
info.components = files.filter(f => f.endsWith(".tsx") || f.endsWith(".vue") || f.endsWith(".svelte")).map(f => f.replace(/\.\w+$/, ""));
|
|
95
112
|
}
|
|
96
|
-
|
|
97
|
-
log(` ${BOLD}Example:${RESET} npx buildcrew add erd\n`);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const template = TEMPLATES[type];
|
|
102
|
-
if (!template) {
|
|
103
|
-
log(`\n ${RED}Unknown template: ${type}${RESET}`);
|
|
104
|
-
log(` ${DIM}Run ${BOLD}npx buildcrew add${RESET}${DIM} to see available templates.${RESET}\n`);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
113
|
+
} catch {}
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
// Scan API routes
|
|
116
|
+
try {
|
|
117
|
+
const apiDir = join(process.cwd(), "src", "app", "api");
|
|
118
|
+
if (await exists(apiDir)) {
|
|
119
|
+
const scanDir = async (dir, prefix = "") => {
|
|
120
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
121
|
+
const routes = [];
|
|
122
|
+
for (const e of entries) {
|
|
123
|
+
if (e.isDirectory()) routes.push(...await scanDir(join(dir, e.name), `${prefix}/${e.name}`));
|
|
124
|
+
else if (e.name === "route.ts" || e.name === "route.js") routes.push(prefix || "/");
|
|
125
|
+
}
|
|
126
|
+
return routes;
|
|
127
|
+
};
|
|
128
|
+
info.apiRoutes = await scanDir(apiDir);
|
|
129
|
+
}
|
|
130
|
+
} catch {}
|
|
113
131
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
// Scan i18n locales
|
|
133
|
+
try {
|
|
134
|
+
for (const localeDir of ["src/i18n/dictionaries", "src/locales", "locales", "messages", "public/locales"]) {
|
|
135
|
+
const dir = join(process.cwd(), localeDir);
|
|
136
|
+
if (await exists(dir)) {
|
|
137
|
+
const files = await readdir(dir);
|
|
138
|
+
info.locales = files.filter(f => f.endsWith(".json")).map(f => f.replace(".json", ""));
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
119
143
|
|
|
120
|
-
|
|
121
|
-
await copyFile(join(TEMPLATES_SRC, template.file), target);
|
|
122
|
-
log(`\n ${GREEN} + ${RESET} .claude/harness/${type}.md`);
|
|
123
|
-
log(` ${DIM}Edit this file to fill in your project details.${RESET}`);
|
|
124
|
-
log(` ${DIM}All agents will read it automatically.${RESET}\n`);
|
|
144
|
+
return info;
|
|
125
145
|
}
|
|
126
146
|
|
|
127
|
-
// ─── Init:
|
|
147
|
+
// ─── Init: Zero-question harness generation ───
|
|
128
148
|
|
|
129
149
|
async function runInit(force) {
|
|
130
|
-
log(`\n ${BOLD}buildcrew init${RESET}
|
|
131
|
-
log(` ${DIM}Setting up project context and team rules.${RESET}`);
|
|
132
|
-
log(` ${DIM}All 11 agents will read these before every task.${RESET}\n`);
|
|
150
|
+
log(`\n ${BOLD}buildcrew init${RESET} v${VERSION}\n`);
|
|
133
151
|
|
|
134
152
|
if ((await exists(join(HARNESS_DIR, "project.md"))) && !force) {
|
|
135
153
|
log(` ${YELLOW}Harness already exists at .claude/harness/${RESET}`);
|
|
136
|
-
log(` ${DIM}Use ${BOLD}
|
|
154
|
+
log(` ${DIM}Use ${BOLD}--force${RESET}${DIM} to regenerate. Or just edit the files directly.${RESET}\n`);
|
|
137
155
|
return;
|
|
138
156
|
}
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
const
|
|
158
|
+
log(` ${DIM}Scanning project...${RESET}\n`);
|
|
159
|
+
const d = await detectProject();
|
|
142
160
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const stackAuto = detected.stack.length > 0 ? detected.stack.join(", ") : "not detected";
|
|
148
|
-
const stackExtra = await ask(` ${BOLD}Tech stack${RESET} ${DIM}(detected: ${stackAuto})${RESET}\n ${DIM}Add anything missing (comma-separated, or Enter):${RESET} `);
|
|
149
|
-
const deployTarget = await ask(` ${BOLD}Deploy target${RESET} ${DIM}(Vercel, AWS, etc.)${RESET}: `);
|
|
150
|
-
const prodUrl = await ask(` ${BOLD}Production URL${RESET} ${DIM}(or Enter to skip)${RESET}: `);
|
|
151
|
-
|
|
152
|
-
log(`\n ${CYAN}${BOLD}[2/3] Team Rules${RESET}\n`);
|
|
153
|
-
const conventions = await ask(` ${BOLD}Coding conventions${RESET}\n ${DIM}(e.g., "functional components only, no default exports")${RESET}\n : `);
|
|
154
|
-
const priorities = await ask(` ${BOLD}What to prioritize${RESET}\n ${DIM}(e.g., "UX over performance, security first")${RESET}\n : `);
|
|
155
|
-
const avoid = await ask(` ${BOLD}What to avoid${RESET}\n ${DIM}(e.g., "no class components, no any types")${RESET}\n : `);
|
|
156
|
-
const quality = await ask(` ${BOLD}Quality standards${RESET}\n ${DIM}(e.g., "all code must pass tsc, no console.log")${RESET}\n : `);
|
|
157
|
-
const reviewRules = await ask(` ${BOLD}Review rules${RESET}\n ${DIM}(e.g., "always check auth on API routes, mobile-first")${RESET}\n : `);
|
|
158
|
-
|
|
159
|
-
log(`\n ${CYAN}${BOLD}[3/3] Domain Knowledge${RESET}\n`);
|
|
160
|
-
const domain = await ask(` ${BOLD}Domain${RESET} ${DIM}(e.g., "e-commerce", "fintech")${RESET}: `);
|
|
161
|
-
const userTypes = await ask(` ${BOLD}User types${RESET} ${DIM}(e.g., "free, premium, admin")${RESET}: `);
|
|
162
|
-
const keyTerms = await ask(` ${BOLD}Key terms${RESET}\n ${DIM}(e.g., "reading=tarot session, spread=card layout")${RESET}\n : `);
|
|
163
|
-
const businessRules = await ask(` ${BOLD}Business rules${RESET}\n ${DIM}(e.g., "free=3 reads/day, premium=unlimited")${RESET}\n : `);
|
|
164
|
-
|
|
165
|
-
close();
|
|
166
|
-
|
|
167
|
-
await mkdir(HARNESS_DIR, { recursive: true });
|
|
168
|
-
const fullStack = [...detected.stack, ...(stackExtra ? stackExtra.split(",").map(s => s.trim()) : [])].filter(Boolean);
|
|
169
|
-
|
|
170
|
-
const projectMd = `# Project: ${name}
|
|
161
|
+
await mkdir(HARNESS_DIR, { recursive: true });
|
|
162
|
+
|
|
163
|
+
// ─── project.md ───
|
|
164
|
+
const projectMd = `# Project: ${d.name || "my-project"}
|
|
171
165
|
|
|
172
166
|
## Overview
|
|
173
|
-
${description || "
|
|
167
|
+
${d.description || "<!-- Describe what this project does in 1-2 sentences -->"}
|
|
174
168
|
|
|
175
169
|
## Tech Stack
|
|
176
|
-
${
|
|
170
|
+
${d.stack.map(s => `- ${s}`).join("\n") || "<!-- Add your tech stack -->"}
|
|
177
171
|
|
|
178
172
|
## Framework
|
|
179
|
-
${
|
|
173
|
+
${d.framework || "<!-- Not detected -->"}
|
|
180
174
|
|
|
181
175
|
## Infrastructure
|
|
182
|
-
- **Deploy**: ${
|
|
183
|
-
- **Production URL**:
|
|
184
|
-
- **TypeScript**: ${
|
|
185
|
-
- **CSS**: ${
|
|
186
|
-
- **i18n**: ${
|
|
187
|
-
- **Auth**: ${
|
|
188
|
-
- **Database**: ${
|
|
176
|
+
- **Deploy**: ${d.deploy || "<!-- Vercel / AWS / Netlify / etc. -->"}
|
|
177
|
+
- **Production URL**: <!-- https://your-app.com -->
|
|
178
|
+
- **TypeScript**: ${d.hasTS ? "Yes" : "No"}
|
|
179
|
+
- **CSS**: ${d.hasTailwind ? "TailwindCSS" : "<!-- Your CSS solution -->"}
|
|
180
|
+
- **i18n**: ${d.hasI18n ? `Yes (${d.locales.join(", ")})` : "No"}
|
|
181
|
+
- **Auth**: ${d.hasAuth ? d.authName : "No"}
|
|
182
|
+
- **Database**: ${d.hasDB ? d.dbName : "No"}
|
|
183
|
+
- **Payments**: ${d.hasPayments ? d.paymentName : "No"}
|
|
184
|
+
- **AI**: ${d.hasAI ? d.stack.find(s => ["OpenAI","Anthropic","Google AI"].includes(s)) || "Yes" : "No"}
|
|
185
|
+
|
|
186
|
+
## Key Components
|
|
187
|
+
${d.components.length > 0 ? d.components.map(c => `- ${c}`).join("\n") : "<!-- List your main components -->"}
|
|
188
|
+
|
|
189
|
+
## API Routes
|
|
190
|
+
${d.apiRoutes.length > 0 ? d.apiRoutes.map(r => `- \`/api${r}\``).join("\n") : "<!-- List your API endpoints -->"}
|
|
189
191
|
|
|
190
192
|
## Domain
|
|
191
|
-
- **Industry**:
|
|
192
|
-
- **User types**:
|
|
193
|
+
- **Industry**: <!-- e.g., e-commerce, fintech, entertainment -->
|
|
194
|
+
- **User types**: <!-- e.g., free users, premium users, admins -->
|
|
193
195
|
|
|
194
196
|
## Key Domain Terms
|
|
195
|
-
|
|
197
|
+
<!-- Define project-specific terms so agents use consistent language -->
|
|
198
|
+
<!-- - term = definition -->
|
|
196
199
|
|
|
197
200
|
## Business Rules
|
|
198
|
-
|
|
201
|
+
<!-- Rules that affect feature development -->
|
|
202
|
+
<!-- - Free users: 3 actions per day -->
|
|
203
|
+
<!-- - Premium: unlimited access -->
|
|
199
204
|
`;
|
|
200
205
|
|
|
201
|
-
|
|
206
|
+
// ─── rules.md ───
|
|
207
|
+
const rulesMd = `# Team Rules
|
|
202
208
|
|
|
203
|
-
|
|
209
|
+
All agents read this before every task. Edit freely.
|
|
204
210
|
|
|
205
211
|
## Coding Conventions
|
|
206
|
-
${
|
|
212
|
+
${d.framework === "Next.js" ? `- Use App Router patterns
|
|
213
|
+
- Server components by default, "use client" only when needed
|
|
214
|
+
- Functional components only` : "<!-- Add your coding conventions -->"}
|
|
215
|
+
${d.hasTS ? "- Strict TypeScript — no \`any\` types" : ""}
|
|
216
|
+
${d.hasTailwind ? "- Use TailwindCSS utility classes, avoid custom CSS" : ""}
|
|
217
|
+
- No \`console.log\` in production code
|
|
218
|
+
- No default exports (except pages/layouts)
|
|
207
219
|
|
|
208
220
|
## Priorities
|
|
209
|
-
|
|
221
|
+
- UX over premature optimization
|
|
222
|
+
- Mobile-first responsive design
|
|
223
|
+
${d.hasI18n ? `- i18n: all user-facing text must be in locale files (${d.locales.join(", ")})` : ""}
|
|
224
|
+
${d.hasPayments ? "- Payment security: amounts validated server-side only" : ""}
|
|
210
225
|
|
|
211
226
|
## What to Avoid
|
|
212
|
-
|
|
227
|
+
- Class components
|
|
228
|
+
- \`any\` type assertions
|
|
229
|
+
- Inline styles (use Tailwind or CSS modules)
|
|
230
|
+
- Hardcoded strings in UI (use i18n)
|
|
231
|
+
- Direct DB access from client components
|
|
213
232
|
|
|
214
233
|
## Quality Standards
|
|
215
|
-
${
|
|
234
|
+
${d.hasTS ? "- \`npx tsc --noEmit\` must pass" : ""}
|
|
235
|
+
- \`npm run lint\` must pass
|
|
236
|
+
- \`npm run build\` must pass
|
|
237
|
+
${d.hasI18n && d.locales.length > 0 ? `- All ${d.locales.length} locale files must be in sync` : ""}
|
|
238
|
+
- No unused imports or variables
|
|
216
239
|
|
|
217
240
|
## Review Standards
|
|
218
|
-
${
|
|
241
|
+
${d.hasAuth ? "- All API routes must check authentication" : ""}
|
|
242
|
+
${d.hasPayments ? "- Payment mutations must be server-side and atomic" : ""}
|
|
243
|
+
${d.hasAI ? "- AI-generated content treated as untrusted (sanitize before render)" : ""}
|
|
244
|
+
- Check for XSS on any user input rendering
|
|
245
|
+
- Verify responsive layout at 375px, 768px, 1440px
|
|
219
246
|
`;
|
|
220
247
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
];
|
|
239
|
-
for (const [name, desc] of suggestions) {
|
|
240
|
-
log(` ${DIM}npx buildcrew add ${CYAN}${name.padEnd(15)}${RESET} ${DIM}${desc}${RESET}`);
|
|
248
|
+
await writeFile(join(HARNESS_DIR, "project.md"), projectMd);
|
|
249
|
+
await writeFile(join(HARNESS_DIR, "rules.md"), rulesMd);
|
|
250
|
+
|
|
251
|
+
// Auto-add relevant templates
|
|
252
|
+
let templateCount = 0;
|
|
253
|
+
const autoTemplates = [];
|
|
254
|
+
if (d.hasDB) autoTemplates.push("erd");
|
|
255
|
+
if (d.apiRoutes.length > 0) autoTemplates.push("api-spec");
|
|
256
|
+
if (d.hasTailwind) autoTemplates.push("design-system");
|
|
257
|
+
if (d.hasI18n || d.components.length > 5) autoTemplates.push("user-flow");
|
|
258
|
+
autoTemplates.push("architecture");
|
|
259
|
+
|
|
260
|
+
for (const name of autoTemplates) {
|
|
261
|
+
const t = TEMPLATES[name];
|
|
262
|
+
if (t && t.file) {
|
|
263
|
+
await copyFile(join(TEMPLATES_SRC, t.file), join(HARNESS_DIR, `${name}.md`));
|
|
264
|
+
templateCount++;
|
|
241
265
|
}
|
|
266
|
+
}
|
|
242
267
|
|
|
243
|
-
|
|
268
|
+
// Print results
|
|
269
|
+
log(` ${GREEN}${BOLD}Harness generated!${RESET} (${2 + templateCount} files)\n`);
|
|
270
|
+
log(` ${GREEN} + ${RESET} project.md ${DIM}auto-detected from package.json${RESET}`);
|
|
271
|
+
log(` ${GREEN} + ${RESET} rules.md ${DIM}smart defaults for ${d.framework || "your stack"}${RESET}`);
|
|
272
|
+
for (const name of autoTemplates) {
|
|
273
|
+
log(` ${GREEN} + ${RESET} ${(name + ".md").padEnd(17)}${DIM}${TEMPLATES[name].desc}${RESET}`);
|
|
274
|
+
}
|
|
244
275
|
|
|
245
|
-
|
|
246
|
-
|
|
276
|
+
// Show what was detected
|
|
277
|
+
log(`\n ${BOLD}Detected:${RESET}`);
|
|
278
|
+
if (d.stack.length > 0) log(` ${CYAN}Stack${RESET} ${d.stack.join(", ")}`);
|
|
279
|
+
if (d.deploy) log(` ${CYAN}Deploy${RESET} ${d.deploy}`);
|
|
280
|
+
if (d.components.length > 0) log(` ${CYAN}Components${RESET} ${d.components.length} found`);
|
|
281
|
+
if (d.apiRoutes.length > 0) log(` ${CYAN}API Routes${RESET} ${d.apiRoutes.length} found`);
|
|
282
|
+
if (d.locales.length > 0) log(` ${CYAN}Locales${RESET} ${d.locales.join(", ")}`);
|
|
283
|
+
|
|
284
|
+
// Available extras
|
|
285
|
+
const remaining = Object.entries(TEMPLATES).filter(([name, t]) => t.file && !autoTemplates.includes(name));
|
|
286
|
+
if (remaining.length > 0) {
|
|
287
|
+
log(`\n ${BOLD}Add more:${RESET}`);
|
|
288
|
+
for (const [name, t] of remaining) {
|
|
289
|
+
log(` ${DIM}npx buildcrew add ${CYAN}${name}${RESET}`);
|
|
247
290
|
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
log(`\n ${BOLD}Next step:${RESET} Edit ${CYAN}.claude/harness/*.md${RESET} to fill in project-specific details.`);
|
|
294
|
+
log(` ${DIM}Look for <!-- comments --> — those are the parts to customize.${RESET}\n`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─── Add: copy template ───
|
|
248
298
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
299
|
+
async function runAdd(type, force) {
|
|
300
|
+
if (!type) {
|
|
301
|
+
log(`\n ${BOLD}Available harness templates:${RESET}\n`);
|
|
302
|
+
const categories = { core: "Core (auto-created by init)", data: "Data", system: "System", design: "Design", domain: "Domain" };
|
|
303
|
+
for (const [cat, label] of Object.entries(categories)) {
|
|
304
|
+
const items = Object.entries(TEMPLATES).filter(([, v]) => v.category === cat);
|
|
305
|
+
if (items.length === 0) continue;
|
|
306
|
+
log(` ${CYAN}${label}${RESET}`);
|
|
307
|
+
for (const [name, info] of items) {
|
|
308
|
+
const installed = await exists(join(HARNESS_DIR, `${name}.md`));
|
|
309
|
+
const status = installed ? `${GREEN}installed${RESET}` : `${DIM}not installed${RESET}`;
|
|
310
|
+
log(` ${BOLD}${name.padEnd(16)}${RESET} ${status} ${DIM}${info.desc}${RESET}`);
|
|
311
|
+
}
|
|
312
|
+
log("");
|
|
313
|
+
}
|
|
314
|
+
log(` ${BOLD}Usage:${RESET} npx buildcrew add ${CYAN}<template>${RESET}\n`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const template = TEMPLATES[type];
|
|
319
|
+
if (!template) {
|
|
320
|
+
log(`\n ${RED}Unknown template: ${type}${RESET}`);
|
|
321
|
+
log(` ${DIM}Run ${BOLD}npx buildcrew add${RESET}${DIM} to see available templates.${RESET}\n`);
|
|
322
|
+
return;
|
|
252
323
|
}
|
|
324
|
+
|
|
325
|
+
if (!template.file) {
|
|
326
|
+
log(`\n ${YELLOW}${type}.md is auto-generated by ${BOLD}npx buildcrew init${RESET}\n`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const target = join(HARNESS_DIR, `${type}.md`);
|
|
331
|
+
if ((await exists(target)) && !force) {
|
|
332
|
+
log(`\n ${YELLOW}${type}.md already exists.${RESET} Use ${BOLD}--force${RESET} to overwrite.\n`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await mkdir(HARNESS_DIR, { recursive: true });
|
|
337
|
+
await copyFile(join(TEMPLATES_SRC, template.file), target);
|
|
338
|
+
log(`\n ${GREEN} + ${RESET} .claude/harness/${type}.md`);
|
|
339
|
+
log(` ${DIM}Edit to fill in your project details. Agents read it automatically.${RESET}\n`);
|
|
253
340
|
}
|
|
254
341
|
|
|
255
342
|
// ─── Harness status ───
|
|
256
343
|
|
|
257
344
|
async function runHarnessStatus() {
|
|
258
|
-
log(`\n ${BOLD}Harness
|
|
345
|
+
log(`\n ${BOLD}Harness${RESET} ${DIM}(.claude/harness/)${RESET}\n`);
|
|
259
346
|
|
|
260
347
|
if (!(await exists(HARNESS_DIR))) {
|
|
261
|
-
log(` ${YELLOW}No harness
|
|
262
|
-
log(` ${DIM}Run ${BOLD}npx buildcrew init${RESET}${DIM} to get started.${RESET}\n`);
|
|
348
|
+
log(` ${YELLOW}No harness found.${RESET} Run ${BOLD}npx buildcrew init${RESET}\n`);
|
|
263
349
|
return;
|
|
264
350
|
}
|
|
265
351
|
|
|
266
352
|
const files = (await readdir(HARNESS_DIR)).filter(f => f.endsWith(".md")).sort();
|
|
267
|
-
if (files.length === 0) {
|
|
268
|
-
log(` ${YELLOW}Harness directory is empty.${RESET}\n`);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
353
|
+
if (files.length === 0) { log(` ${YELLOW}Empty.${RESET}\n`); return; }
|
|
271
354
|
|
|
272
355
|
for (const file of files) {
|
|
273
356
|
const name = file.replace(".md", "");
|
|
274
|
-
const isTemplate = TEMPLATES[name];
|
|
275
357
|
const content = await readFile(join(HARNESS_DIR, file), "utf8");
|
|
276
358
|
const lines = content.split("\n").length;
|
|
277
|
-
const
|
|
278
|
-
const status =
|
|
279
|
-
const
|
|
280
|
-
|
|
359
|
+
const hasComments = content.includes("<!--");
|
|
360
|
+
const status = hasComments ? `${YELLOW}needs editing${RESET}` : `${GREEN}configured${RESET}`;
|
|
361
|
+
const t = TEMPLATES[name];
|
|
362
|
+
const desc = t ? `${DIM}${t.desc}${RESET}` : `${DIM}(custom)${RESET}`;
|
|
363
|
+
log(` ${BOLD}${name.padEnd(16)}${RESET} ${status} ${DIM}${lines} lines${RESET} ${desc}`);
|
|
281
364
|
}
|
|
282
365
|
|
|
283
|
-
log(`\n ${DIM}
|
|
366
|
+
log(`\n ${DIM}Edit files to replace <!-- comments --> with your content.${RESET}\n`);
|
|
284
367
|
}
|
|
285
368
|
|
|
286
369
|
// ─── Install agents ───
|
|
@@ -288,78 +371,48 @@ async function runHarnessStatus() {
|
|
|
288
371
|
async function runInstall(force) {
|
|
289
372
|
const files = (await readdir(AGENTS_SRC)).filter(f => f.endsWith(".md"));
|
|
290
373
|
log(`\n ${BOLD}buildcrew${RESET} v${VERSION}\n ${DIM}11 AI agents for Claude Code${RESET}\n`);
|
|
291
|
-
log(`${DIM} Installing to ${TARGET_DIR}${RESET}\n`);
|
|
292
374
|
|
|
293
375
|
await mkdir(TARGET_DIR, { recursive: true });
|
|
294
376
|
let installed = 0, skipped = 0;
|
|
295
377
|
|
|
296
378
|
for (const file of files) {
|
|
297
379
|
const target = join(TARGET_DIR, file);
|
|
298
|
-
if ((await exists(target)) && !force) {
|
|
299
|
-
log(` ${YELLOW}skip${RESET} ${file} ${DIM}(exists, use --force)${RESET}`);
|
|
300
|
-
skipped++;
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
380
|
+
if ((await exists(target)) && !force) { skipped++; continue; }
|
|
303
381
|
await copyFile(join(AGENTS_SRC, file), target);
|
|
304
|
-
log(` ${GREEN} +
|
|
382
|
+
log(` ${GREEN} + ${RESET} ${file}`);
|
|
305
383
|
installed++;
|
|
306
384
|
}
|
|
307
385
|
|
|
308
386
|
log("");
|
|
309
|
-
if (installed > 0) {
|
|
310
|
-
|
|
311
|
-
} else {
|
|
312
|
-
log(` ${YELLOW}All agents already exist.${RESET} Use ${BOLD}--force${RESET} to overwrite.\n`);
|
|
313
|
-
}
|
|
387
|
+
if (installed > 0) log(` ${GREEN}${BOLD}Done!${RESET} ${installed} agents installed.${skipped > 0 ? ` ${skipped} skipped.` : ""}\n`);
|
|
388
|
+
else log(` ${YELLOW}All agents already exist.${RESET} Use ${BOLD}--force${RESET} to overwrite.\n`);
|
|
314
389
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
log(` ${CYAN}Next:${RESET} Run ${BOLD}npx buildcrew init${RESET} to set up your project harness.\n`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
let hasPlaywright = false;
|
|
321
|
-
try {
|
|
322
|
-
const settingsPath = join(process.env.HOME, ".claude", "settings.json");
|
|
323
|
-
if (await exists(settingsPath)) {
|
|
324
|
-
hasPlaywright = (await readFile(settingsPath, "utf8")).includes("playwright");
|
|
325
|
-
}
|
|
326
|
-
} catch {}
|
|
327
|
-
if (!hasPlaywright) {
|
|
328
|
-
log(` ${YELLOW}Optional:${RESET} Playwright MCP not detected.`);
|
|
329
|
-
log(` ${DIM}Setup: claude mcp add playwright -- npx @anthropic-ai/mcp-server-playwright${RESET}\n`);
|
|
390
|
+
if (!(await exists(join(HARNESS_DIR, "project.md")))) {
|
|
391
|
+
log(` ${CYAN}Next:${RESET} ${BOLD}npx buildcrew init${RESET} — auto-generates project harness from your codebase.\n`);
|
|
330
392
|
}
|
|
331
393
|
|
|
332
|
-
log(` ${BOLD}
|
|
394
|
+
log(` ${BOLD}Start:${RESET} @constitution [your request]\n`);
|
|
333
395
|
}
|
|
334
396
|
|
|
335
|
-
// ─── List
|
|
397
|
+
// ─── List / Uninstall ───
|
|
336
398
|
|
|
337
399
|
async function runList() {
|
|
338
400
|
const files = (await readdir(AGENTS_SRC)).filter(f => f.endsWith(".md"));
|
|
339
401
|
log(`\n ${BOLD}buildcrew${RESET} v${VERSION} — 11 agents\n`);
|
|
340
402
|
for (const file of files) {
|
|
341
403
|
const content = await readFile(join(AGENTS_SRC, file), "utf8");
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
const name = nameMatch ? nameMatch[1] : file.replace(".md", "");
|
|
346
|
-
const desc = descMatch ? descMatch[1] : "";
|
|
347
|
-
const model = modelMatch ? modelMatch[1] : "sonnet";
|
|
404
|
+
const name = (content.match(/^name:\s*(.+)$/m) || [])[1] || file.replace(".md", "");
|
|
405
|
+
const desc = (content.match(/^description:\s*(.+)$/m) || [])[1] || "";
|
|
406
|
+
const model = (content.match(/^model:\s*(.+)$/m) || [])[1] || "sonnet";
|
|
348
407
|
const modelTag = model === "opus" ? `${MAGENTA}opus${RESET}` : `${DIM}sonnet${RESET}`;
|
|
349
|
-
|
|
350
|
-
log(` ${color}${name.padEnd(20)}${RESET} ${modelTag} ${DIM}${desc.slice(0, 55)}${RESET}`);
|
|
408
|
+
log(` ${name === "constitution" ? BOLD : ""}${name.padEnd(20)}${RESET} ${modelTag} ${DIM}${desc.slice(0, 55)}${RESET}`);
|
|
351
409
|
}
|
|
352
410
|
log("");
|
|
353
411
|
}
|
|
354
412
|
|
|
355
|
-
// ─── Uninstall ───
|
|
356
|
-
|
|
357
413
|
async function runUninstall() {
|
|
358
414
|
const files = (await readdir(AGENTS_SRC)).filter(f => f.endsWith(".md"));
|
|
359
|
-
if (!(await exists(TARGET_DIR))) {
|
|
360
|
-
log(`${YELLOW}No .claude/agents/ directory found.${RESET}`);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
415
|
+
if (!(await exists(TARGET_DIR))) { log(`${YELLOW}No agents found.${RESET}`); return; }
|
|
363
416
|
let removed = 0;
|
|
364
417
|
for (const file of files) {
|
|
365
418
|
const target = join(TARGET_DIR, file);
|
|
@@ -376,38 +429,30 @@ async function main() {
|
|
|
376
429
|
const subcommand = args[1];
|
|
377
430
|
const force = args.includes("--force") || args.includes("-f");
|
|
378
431
|
|
|
379
|
-
if (args.includes("--version") || args.includes("-v")) {
|
|
380
|
-
log(`buildcrew v${VERSION}`);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
432
|
+
if (args.includes("--version") || args.includes("-v")) { log(`buildcrew v${VERSION}`); return; }
|
|
383
433
|
|
|
384
434
|
if (args.includes("--help") || args.includes("-h")) {
|
|
385
435
|
log(`
|
|
386
436
|
${BOLD}buildcrew${RESET} v${VERSION} — 11 AI agents for Claude Code
|
|
387
437
|
|
|
388
438
|
${BOLD}Commands:${RESET}
|
|
389
|
-
npx buildcrew
|
|
390
|
-
npx buildcrew init
|
|
391
|
-
npx buildcrew add
|
|
392
|
-
npx buildcrew add <
|
|
393
|
-
npx buildcrew harness
|
|
439
|
+
npx buildcrew Install agents
|
|
440
|
+
npx buildcrew init Auto-generate project harness (zero questions)
|
|
441
|
+
npx buildcrew add List harness templates
|
|
442
|
+
npx buildcrew add <name> Add a harness template
|
|
443
|
+
npx buildcrew harness Show harness file status
|
|
394
444
|
|
|
395
445
|
${BOLD}Options:${RESET}
|
|
396
|
-
--force, -f
|
|
397
|
-
--list, -l
|
|
398
|
-
--uninstall
|
|
399
|
-
--version
|
|
400
|
-
|
|
401
|
-
${BOLD}Setup
|
|
402
|
-
${GREEN}1.${RESET} npx buildcrew
|
|
403
|
-
${GREEN}2.${RESET} npx buildcrew init
|
|
404
|
-
${GREEN}3.${RESET}
|
|
405
|
-
${GREEN}4.${RESET} @constitution [task]
|
|
406
|
-
|
|
407
|
-
${BOLD}Harness templates:${RESET}
|
|
408
|
-
erd, architecture, api-spec, design-system,
|
|
409
|
-
glossary, user-flow, env-vars
|
|
410
|
-
${DIM}Or create any .md file in .claude/harness/${RESET}
|
|
446
|
+
--force, -f Overwrite existing files
|
|
447
|
+
--list, -l List all agents
|
|
448
|
+
--uninstall Remove agents
|
|
449
|
+
--version Show version
|
|
450
|
+
|
|
451
|
+
${BOLD}Setup:${RESET}
|
|
452
|
+
${GREEN}1.${RESET} npx buildcrew ${DIM}Install agents${RESET}
|
|
453
|
+
${GREEN}2.${RESET} npx buildcrew init ${DIM}Auto-generate harness from codebase${RESET}
|
|
454
|
+
${GREEN}3.${RESET} Edit .claude/harness/ ${DIM}Customize (replace <!-- comments -->)${RESET}
|
|
455
|
+
${GREEN}4.${RESET} @constitution [task] ${DIM}Start working${RESET}
|
|
411
456
|
|
|
412
457
|
${BOLD}More info:${RESET} https://github.com/z1nun/buildcrew
|
|
413
458
|
`);
|