ai-dev-setup 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/LICENSE +21 -0
- package/README.md +338 -0
- package/bin/ai-dev-setup.js +7 -0
- package/package.json +35 -0
- package/src/cli/index.js +142 -0
- package/src/cli/logger.js +29 -0
- package/src/cli/prompts.js +267 -0
- package/src/commands/init.js +321 -0
- package/src/commands/update.js +6 -0
- package/src/constants.js +34 -0
- package/src/core/detector.js +164 -0
- package/src/core/git-ref.js +28 -0
- package/src/core/gitignore-vendor.js +126 -0
- package/src/core/renderer.js +97 -0
- package/src/core/vendors.js +354 -0
- package/src/core/writer.js +67 -0
- package/src/platforms/claude-code.js +33 -0
- package/src/platforms/cursor.js +33 -0
- package/src/platforms/platform.js +18 -0
- package/src/platforms/registry.js +56 -0
- package/src/templates/claude-code/claude.md.tmpl +58 -0
- package/src/templates/claude-code/commands/kickoff.md.tmpl +18 -0
- package/src/templates/claude-code/commands/review.md.tmpl +20 -0
- package/src/templates/claude-code/commands/ship.md.tmpl +19 -0
- package/src/templates/claude-code/settings.json.tmpl +17 -0
- package/src/templates/cursor/cursorrules.tmpl +36 -0
- package/src/templates/cursor/rules/agents.mdc.tmpl +12 -0
- package/src/templates/cursor/rules/core-rules.mdc.tmpl +14 -0
- package/src/templates/cursor/rules/review.mdc.tmpl +13 -0
- package/src/templates/cursor/rules/workflow.mdc.tmpl +12 -0
- package/src/templates/ignore/claudeignore.tmpl +25 -0
- package/src/templates/ignore/cursorignore.tmpl +25 -0
- package/src/templates/shared/agents.md.tmpl +41 -0
- package/src/templates/shared/docs/api-patterns.md.tmpl +39 -0
- package/src/templates/shared/docs/architecture.md.tmpl +41 -0
- package/src/templates/shared/docs/conventions.md.tmpl +50 -0
- package/src/templates/shared/docs/error-handling.md.tmpl +32 -0
- package/src/templates/shared/docs/security.md.tmpl +37 -0
- package/src/templates/shared/docs/testing.md.tmpl +34 -0
- package/src/templates/shared/rules.md.tmpl +65 -0
- package/src/templates/shared/workflow.md.tmpl +42 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { assertSafeGitRef } from './git-ref.js';
|
|
5
|
+
import { createReadStream } from 'node:fs';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
|
|
8
|
+
export const SUPERPOWERS_GIT_URL = 'https://github.com/obra/superpowers.git';
|
|
9
|
+
export const AGENCY_GIT_URL = 'https://github.com/msitarzewski/agency-agents.git';
|
|
10
|
+
|
|
11
|
+
export const VENDOR_SUPERPOWERS = path.join('vendor', 'superpowers');
|
|
12
|
+
export const VENDOR_AGENCY = path.join('vendor', 'agency-agents');
|
|
13
|
+
|
|
14
|
+
/** Same division roots as agency-agents `scripts/install.sh` install_claude_code */
|
|
15
|
+
export const AGENCY_CLAUDE_DIVISIONS = [
|
|
16
|
+
'academic',
|
|
17
|
+
'design',
|
|
18
|
+
'engineering',
|
|
19
|
+
'game-development',
|
|
20
|
+
'marketing',
|
|
21
|
+
'paid-media',
|
|
22
|
+
'sales',
|
|
23
|
+
'product',
|
|
24
|
+
'project-management',
|
|
25
|
+
'testing',
|
|
26
|
+
'support',
|
|
27
|
+
'spatial-computing',
|
|
28
|
+
'specialized',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} command
|
|
33
|
+
* @param {string[]} args
|
|
34
|
+
* @param {{ cwd?: string }} [opts]
|
|
35
|
+
*/
|
|
36
|
+
export function runProcess(command, args, opts = {}) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const child = spawn(command, args, {
|
|
39
|
+
cwd: opts.cwd,
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
+
});
|
|
42
|
+
let stdout = '';
|
|
43
|
+
let stderr = '';
|
|
44
|
+
child.stdout?.on('data', (c) => {
|
|
45
|
+
stdout += c;
|
|
46
|
+
});
|
|
47
|
+
child.stderr?.on('data', (c) => {
|
|
48
|
+
stderr += c;
|
|
49
|
+
});
|
|
50
|
+
child.on('error', reject);
|
|
51
|
+
child.on('close', (code) => {
|
|
52
|
+
if (code === 0) {
|
|
53
|
+
resolve({ stdout, stderr });
|
|
54
|
+
} else {
|
|
55
|
+
reject(
|
|
56
|
+
new Error(
|
|
57
|
+
`${command} ${args.join(' ')} exited with code ${code}\n${stderr || stdout}`.trim(),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function assertGitAvailable() {
|
|
66
|
+
try {
|
|
67
|
+
await runProcess('git', ['--version']);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error('git is required on PATH to vendor Superpowers and Agency Agents.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function assertBashAvailable() {
|
|
74
|
+
try {
|
|
75
|
+
await runProcess('bash', ['--version']);
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error('bash is required on PATH to run vendor/agency-agents/scripts/convert.sh.');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} cwd
|
|
83
|
+
* @param {string} relDir
|
|
84
|
+
* @param {string} url
|
|
85
|
+
* @param {string} ref
|
|
86
|
+
* @param {boolean} force
|
|
87
|
+
*/
|
|
88
|
+
async function gitShallowClone(cwd, relDir, url, ref, force) {
|
|
89
|
+
const target = path.join(cwd, relDir);
|
|
90
|
+
try {
|
|
91
|
+
await fs.access(target);
|
|
92
|
+
if (force) {
|
|
93
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
94
|
+
} else {
|
|
95
|
+
return { skipped: true, path: relDir };
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
/* missing */
|
|
99
|
+
}
|
|
100
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
101
|
+
await runProcess('git', ['clone', '--depth', '1', '--branch', ref, url, target], { cwd });
|
|
102
|
+
return { skipped: false, path: relDir };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Rewrite Superpowers .cursor-plugin paths for workspace-root install.
|
|
107
|
+
* @param {Record<string, unknown>} plugin
|
|
108
|
+
* @param {string} posixPrefix e.g. ./vendor/superpowers/
|
|
109
|
+
*/
|
|
110
|
+
export function rewriteSuperpowersCursorPlugin(plugin, posixPrefix) {
|
|
111
|
+
const out = { ...plugin };
|
|
112
|
+
for (const key of ['skills', 'agents', 'commands', 'hooks']) {
|
|
113
|
+
const v = out[key];
|
|
114
|
+
if (typeof v === 'string' && v.startsWith('./')) {
|
|
115
|
+
const rest = v.slice(2);
|
|
116
|
+
out[key] = `${posixPrefix}${rest}`.replace(/\/{2,}/g, '/');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} filePath
|
|
124
|
+
*/
|
|
125
|
+
export async function markdownStartsWithFrontmatter(filePath) {
|
|
126
|
+
const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
|
|
127
|
+
try {
|
|
128
|
+
for await (const line of rl) {
|
|
129
|
+
return line.trimStart().startsWith('---');
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
rl.close();
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {string} agencyRepoRoot vendor/agency-agents absolute
|
|
139
|
+
* @param {string} projectRoot
|
|
140
|
+
* @param {boolean} force
|
|
141
|
+
*/
|
|
142
|
+
export async function copyAgencyClaudeAgents(agencyRepoRoot, projectRoot, force) {
|
|
143
|
+
const dest = path.join(projectRoot, '.claude', 'agents');
|
|
144
|
+
await fs.mkdir(dest, { recursive: true });
|
|
145
|
+
let count = 0;
|
|
146
|
+
for (const division of AGENCY_CLAUDE_DIVISIONS) {
|
|
147
|
+
const divPath = path.join(agencyRepoRoot, division);
|
|
148
|
+
let stat;
|
|
149
|
+
try {
|
|
150
|
+
stat = await fs.stat(divPath);
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!stat.isDirectory()) continue;
|
|
155
|
+
count += await copyMarkdownAgentsFromTree(divPath, dest, force);
|
|
156
|
+
}
|
|
157
|
+
return count;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} dir
|
|
162
|
+
* @param {string} destDir
|
|
163
|
+
* @param {boolean} force
|
|
164
|
+
*/
|
|
165
|
+
async function copyMarkdownAgentsFromTree(dir, destDir, force) {
|
|
166
|
+
let n = 0;
|
|
167
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
168
|
+
for (const ent of entries) {
|
|
169
|
+
const full = path.join(dir, ent.name);
|
|
170
|
+
if (ent.isDirectory()) {
|
|
171
|
+
n += await copyMarkdownAgentsFromTree(full, destDir, force);
|
|
172
|
+
} else if (ent.isFile() && ent.name.endsWith('.md')) {
|
|
173
|
+
if (!(await markdownStartsWithFrontmatter(full))) continue;
|
|
174
|
+
const destFile = path.join(destDir, ent.name);
|
|
175
|
+
if (!force) {
|
|
176
|
+
try {
|
|
177
|
+
await fs.access(destFile);
|
|
178
|
+
continue;
|
|
179
|
+
} catch {
|
|
180
|
+
/* ok */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
await fs.copyFile(full, destFile);
|
|
184
|
+
n++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return n;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {string} integrationsRulesDir
|
|
192
|
+
* @param {string} projectRulesDir .cursor/rules
|
|
193
|
+
* @param {boolean} force overwrite agency-*.mdc
|
|
194
|
+
*/
|
|
195
|
+
export async function copyAgencyCursorRules(integrationsRulesDir, projectRulesDir, force) {
|
|
196
|
+
let count = 0;
|
|
197
|
+
try {
|
|
198
|
+
await fs.access(integrationsRulesDir);
|
|
199
|
+
} catch {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
const files = await fs.readdir(integrationsRulesDir);
|
|
203
|
+
for (const name of files) {
|
|
204
|
+
if (!name.endsWith('.mdc')) continue;
|
|
205
|
+
const prefixed = name.startsWith('agency-') ? name : `agency-${name}`;
|
|
206
|
+
const src = path.join(integrationsRulesDir, name);
|
|
207
|
+
const dest = path.join(projectRulesDir, prefixed);
|
|
208
|
+
if (!force) {
|
|
209
|
+
try {
|
|
210
|
+
await fs.access(dest);
|
|
211
|
+
continue;
|
|
212
|
+
} catch {
|
|
213
|
+
/* copy */
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
await fs.mkdir(projectRulesDir, { recursive: true });
|
|
217
|
+
await fs.copyFile(src, dest);
|
|
218
|
+
count++;
|
|
219
|
+
}
|
|
220
|
+
return count;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} skillsRoot vendor/superpowers/skills
|
|
225
|
+
* @param {string} destRoot .claude/skills
|
|
226
|
+
* @param {boolean} force
|
|
227
|
+
*/
|
|
228
|
+
export async function copySuperpowersSkills(skillsRoot, destRoot, force) {
|
|
229
|
+
let count = 0;
|
|
230
|
+
let rootStat;
|
|
231
|
+
try {
|
|
232
|
+
rootStat = await fs.stat(skillsRoot);
|
|
233
|
+
} catch {
|
|
234
|
+
throw new Error(`Superpowers skills directory missing: ${skillsRoot}`);
|
|
235
|
+
}
|
|
236
|
+
if (!rootStat.isDirectory()) {
|
|
237
|
+
throw new Error(`Superpowers skills path is not a directory: ${skillsRoot}`);
|
|
238
|
+
}
|
|
239
|
+
const skillDirs = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
240
|
+
for (const ent of skillDirs) {
|
|
241
|
+
if (!ent.isDirectory()) continue;
|
|
242
|
+
const src = path.join(skillsRoot, ent.name);
|
|
243
|
+
const dest = path.join(destRoot, ent.name);
|
|
244
|
+
if (!force) {
|
|
245
|
+
try {
|
|
246
|
+
await fs.access(dest);
|
|
247
|
+
continue;
|
|
248
|
+
} catch {
|
|
249
|
+
/* copy */
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
await fs.rm(dest, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
await fs.cp(src, dest, { recursive: true });
|
|
255
|
+
count++;
|
|
256
|
+
}
|
|
257
|
+
return count;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @param {string} projectRoot
|
|
262
|
+
* @param {string} superpowersRoot absolute vendor/superpowers
|
|
263
|
+
*/
|
|
264
|
+
export async function writeSuperpowersCursorPluginFile(projectRoot, superpowersRoot) {
|
|
265
|
+
const pluginSrc = path.join(superpowersRoot, '.cursor-plugin', 'plugin.json');
|
|
266
|
+
const raw = await fs.readFile(pluginSrc, 'utf8');
|
|
267
|
+
const plugin = JSON.parse(raw);
|
|
268
|
+
const rewritten = rewriteSuperpowersCursorPlugin(plugin, './vendor/superpowers/');
|
|
269
|
+
const hooksRel = rewritten.hooks;
|
|
270
|
+
if (typeof hooksRel === 'string') {
|
|
271
|
+
const rel = hooksRel.replace(/^\.\//, '');
|
|
272
|
+
const hooksAbs = path.join(projectRoot, rel);
|
|
273
|
+
try {
|
|
274
|
+
await fs.access(hooksAbs);
|
|
275
|
+
} catch {
|
|
276
|
+
throw new Error(`Superpowers Cursor hooks file missing after clone: ${hooksRel}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const outDir = path.join(projectRoot, '.cursor-plugin');
|
|
280
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
281
|
+
await fs.writeFile(path.join(outDir, 'plugin.json'), `${JSON.stringify(rewritten, null, 2)}\n`, 'utf8');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {object} options
|
|
286
|
+
* @param {string[]} options.platformKeys
|
|
287
|
+
* @param {boolean} options.force
|
|
288
|
+
* @param {string} options.superpowersRef
|
|
289
|
+
* @param {string} options.agencyRef
|
|
290
|
+
* @returns {Promise<string[]>} log lines
|
|
291
|
+
*/
|
|
292
|
+
export async function installVendors(projectRoot, options) {
|
|
293
|
+
const { platformKeys, force } = options;
|
|
294
|
+
const superpowersRef = assertSafeGitRef(
|
|
295
|
+
options.superpowersRef != null && options.superpowersRef !== ''
|
|
296
|
+
? String(options.superpowersRef)
|
|
297
|
+
: 'main',
|
|
298
|
+
);
|
|
299
|
+
const agencyRef = assertSafeGitRef(
|
|
300
|
+
options.agencyRef != null && options.agencyRef !== '' ? String(options.agencyRef) : 'main',
|
|
301
|
+
);
|
|
302
|
+
const wantClaude = platformKeys.includes('claude');
|
|
303
|
+
const wantCursor = platformKeys.includes('cursor');
|
|
304
|
+
const lines = [];
|
|
305
|
+
|
|
306
|
+
await assertGitAvailable();
|
|
307
|
+
|
|
308
|
+
const spRel = VENDOR_SUPERPOWERS;
|
|
309
|
+
const agRel = VENDOR_AGENCY;
|
|
310
|
+
|
|
311
|
+
const sp = await gitShallowClone(projectRoot, spRel, SUPERPOWERS_GIT_URL, superpowersRef, force);
|
|
312
|
+
lines.push(sp.skipped ? `${sp.path} (already present, skipped clone — use --force to refresh)` : `+ ${sp.path} (cloned)`);
|
|
313
|
+
|
|
314
|
+
const ag = await gitShallowClone(projectRoot, agRel, AGENCY_GIT_URL, agencyRef, force);
|
|
315
|
+
lines.push(ag.skipped ? `${ag.path} (already present, skipped clone — use --force to refresh)` : `+ ${ag.path} (cloned)`);
|
|
316
|
+
|
|
317
|
+
const superAbs = path.join(projectRoot, spRel);
|
|
318
|
+
const agencyAbs = path.join(projectRoot, agRel);
|
|
319
|
+
|
|
320
|
+
if (wantCursor) {
|
|
321
|
+
await writeSuperpowersCursorPluginFile(projectRoot, superAbs);
|
|
322
|
+
lines.push('+ .cursor-plugin/plugin.json (Superpowers paths → vendor/superpowers)');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (wantClaude) {
|
|
326
|
+
const n = await copySuperpowersSkills(path.join(superAbs, 'skills'), path.join(projectRoot, '.claude', 'skills'), force);
|
|
327
|
+
lines.push(`+ .claude/skills (${n} skill trees from Superpowers)`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (wantCursor) {
|
|
331
|
+
await assertBashAvailable();
|
|
332
|
+
lines.push('… running vendor/agency-agents/scripts/convert.sh (may take a minute)');
|
|
333
|
+
await runProcess('bash', ['scripts/convert.sh'], { cwd: agencyAbs });
|
|
334
|
+
const rulesSrc = path.join(agencyAbs, 'integrations', 'cursor', 'rules');
|
|
335
|
+
const rulesDest = path.join(projectRoot, '.cursor', 'rules');
|
|
336
|
+
const n = await copyAgencyCursorRules(rulesSrc, rulesDest, force);
|
|
337
|
+
if (n === 0) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
'Agency Cursor rules not found after convert.sh. Expected integrations/cursor/rules/*.mdc',
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
lines.push(`+ .cursor/rules (agency-*.mdc × ${n})`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (wantClaude) {
|
|
346
|
+
const n = await copyAgencyClaudeAgents(agencyAbs, projectRoot, force);
|
|
347
|
+
if (n === 0) {
|
|
348
|
+
throw new Error('No Agency agent markdown files were copied to .claude/agents/. Check vendor/agency-agents layout.');
|
|
349
|
+
}
|
|
350
|
+
lines.push(`+ .claude/agents (${n} agent files from Agency)`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return lines;
|
|
354
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
function isPathInsideRoot(root, target) {
|
|
6
|
+
const r = path.resolve(root);
|
|
7
|
+
const t = path.resolve(target);
|
|
8
|
+
const prefix = r.endsWith(path.sep) ? r : r + path.sep;
|
|
9
|
+
return t === r || t.startsWith(prefix);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {Array<{ path: string, content: string }>} files
|
|
14
|
+
* @param {{ cwd?: string, force?: boolean }} [options]
|
|
15
|
+
*/
|
|
16
|
+
export async function writeFiles(files, { cwd = process.cwd(), force = false } = {}) {
|
|
17
|
+
const root = path.resolve(cwd);
|
|
18
|
+
/** @type {Array<{ path: string, status: 'written'|'skipped'|'error', error?: string }>} */
|
|
19
|
+
const results = [];
|
|
20
|
+
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const target = path.resolve(root, file.path);
|
|
23
|
+
if (!isPathInsideRoot(root, target)) {
|
|
24
|
+
results.push({
|
|
25
|
+
path: file.path,
|
|
26
|
+
status: 'error',
|
|
27
|
+
error: 'Path escapes working directory',
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
results.push({
|
|
36
|
+
path: file.path,
|
|
37
|
+
status: 'error',
|
|
38
|
+
error: err instanceof Error ? err.message : String(err),
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
let exists = false;
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(target);
|
|
47
|
+
exists = true;
|
|
48
|
+
} catch {
|
|
49
|
+
exists = false;
|
|
50
|
+
}
|
|
51
|
+
if (exists && !force) {
|
|
52
|
+
results.push({ path: file.path, status: 'skipped' });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
await fs.writeFile(target, file.content, 'utf8');
|
|
56
|
+
results.push({ path: file.path, status: 'written' });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
results.push({
|
|
59
|
+
path: file.path,
|
|
60
|
+
status: 'error',
|
|
61
|
+
error: err instanceof Error ? err.message : String(err),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { renderFile } from '../core/renderer.js';
|
|
3
|
+
import { Platform } from './platform.js';
|
|
4
|
+
import { register, TEMPLATES_DIR } from './registry.js';
|
|
5
|
+
|
|
6
|
+
function tpl(rel) {
|
|
7
|
+
return path.join(TEMPLATES_DIR, rel);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ClaudeCodePlatform extends Platform {
|
|
11
|
+
constructor() {
|
|
12
|
+
super('claude', 'Claude Code');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @param {Record<string, unknown>} config */
|
|
16
|
+
async getFiles(config) {
|
|
17
|
+
const pairs = [
|
|
18
|
+
['claude-code/claude.md.tmpl', 'CLAUDE.md'],
|
|
19
|
+
['claude-code/settings.json.tmpl', '.claude/settings.json'],
|
|
20
|
+
['claude-code/commands/kickoff.md.tmpl', '.claude/commands/kickoff.md'],
|
|
21
|
+
['claude-code/commands/review.md.tmpl', '.claude/commands/review.md'],
|
|
22
|
+
['claude-code/commands/ship.md.tmpl', '.claude/commands/ship.md'],
|
|
23
|
+
];
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const [rel, dest] of pairs) {
|
|
26
|
+
const content = await renderFile(tpl(rel), config);
|
|
27
|
+
out.push({ path: dest, content });
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
register(new ClaudeCodePlatform());
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { renderFile } from '../core/renderer.js';
|
|
3
|
+
import { Platform } from './platform.js';
|
|
4
|
+
import { register, TEMPLATES_DIR } from './registry.js';
|
|
5
|
+
|
|
6
|
+
function tpl(rel) {
|
|
7
|
+
return path.join(TEMPLATES_DIR, rel);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class CursorPlatform extends Platform {
|
|
11
|
+
constructor() {
|
|
12
|
+
super('cursor', 'Cursor');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @param {Record<string, unknown>} config */
|
|
16
|
+
async getFiles(config) {
|
|
17
|
+
const pairs = [
|
|
18
|
+
['cursor/cursorrules.tmpl', '.cursorrules'],
|
|
19
|
+
['cursor/rules/core-rules.mdc.tmpl', '.cursor/rules/core-rules.mdc'],
|
|
20
|
+
['cursor/rules/workflow.mdc.tmpl', '.cursor/rules/workflow.mdc'],
|
|
21
|
+
['cursor/rules/review.mdc.tmpl', '.cursor/rules/review.mdc'],
|
|
22
|
+
['cursor/rules/agents.mdc.tmpl', '.cursor/rules/agents.mdc'],
|
|
23
|
+
];
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const [rel, dest] of pairs) {
|
|
26
|
+
const content = await renderFile(tpl(rel), config);
|
|
27
|
+
out.push({ path: dest, content });
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
register(new CursorPlatform());
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy base: each assistant platform implements `getFiles(config)`.
|
|
3
|
+
*/
|
|
4
|
+
export class Platform {
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} key
|
|
7
|
+
* @param {string} label
|
|
8
|
+
*/
|
|
9
|
+
constructor(key, label) {
|
|
10
|
+
this.key = key;
|
|
11
|
+
this.label = label;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {Record<string, unknown>} _config */
|
|
15
|
+
async getFiles(_config) {
|
|
16
|
+
throw new Error(`${this.key}: getFiles() not implemented`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { renderFile } from '../core/renderer.js';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
7
|
+
|
|
8
|
+
/** @type {Map<string, import('./platform.js').Platform>} */
|
|
9
|
+
const platforms = new Map();
|
|
10
|
+
|
|
11
|
+
/** @param {import('./platform.js').Platform} platform */
|
|
12
|
+
export function register(platform) {
|
|
13
|
+
platforms.set(platform.key, platform);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {string} key */
|
|
17
|
+
export function getPlatform(key) {
|
|
18
|
+
return platforms.get(key);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getAllPlatforms() {
|
|
22
|
+
return [...platforms.values()];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {string} rel */
|
|
26
|
+
function tpl(rel) {
|
|
27
|
+
return path.join(TEMPLATES_DIR, rel);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Shared artifacts for every run (.ai/, docs/, ignore files).
|
|
32
|
+
* @param {Record<string, unknown>} config
|
|
33
|
+
*/
|
|
34
|
+
export async function getSharedFiles(config) {
|
|
35
|
+
/** @type {Array<{ template: string, out: string }>} */
|
|
36
|
+
const map = [
|
|
37
|
+
{ template: 'shared/rules.md.tmpl', out: '.ai/rules.md' },
|
|
38
|
+
{ template: 'shared/workflow.md.tmpl', out: '.ai/workflow.md' },
|
|
39
|
+
{ template: 'shared/agents.md.tmpl', out: '.ai/agents.md' },
|
|
40
|
+
{ template: 'shared/docs/architecture.md.tmpl', out: 'docs/ARCHITECTURE.md' },
|
|
41
|
+
{ template: 'shared/docs/conventions.md.tmpl', out: 'docs/CONVENTIONS.md' },
|
|
42
|
+
{ template: 'shared/docs/testing.md.tmpl', out: 'docs/TESTING-STRATEGY.md' },
|
|
43
|
+
{ template: 'shared/docs/api-patterns.md.tmpl', out: 'docs/API-PATTERNS.md' },
|
|
44
|
+
{ template: 'shared/docs/error-handling.md.tmpl', out: 'docs/ERROR-HANDLING.md' },
|
|
45
|
+
{ template: 'shared/docs/security.md.tmpl', out: 'docs/SECURITY.md' },
|
|
46
|
+
{ template: 'ignore/claudeignore.tmpl', out: '.claudeignore' },
|
|
47
|
+
{ template: 'ignore/cursorignore.tmpl', out: '.cursorignore' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const { template, out: dest } of map) {
|
|
52
|
+
const content = await renderFile(tpl(template), config);
|
|
53
|
+
out.push({ path: dest, content });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} — Claude Code
|
|
2
|
+
|
|
3
|
+
## Identity
|
|
4
|
+
|
|
5
|
+
| Field | Value |
|
|
6
|
+
|-------|-------|
|
|
7
|
+
| Language | {{LANGUAGE}} |
|
|
8
|
+
| Framework | {{FRAMEWORK}} |
|
|
9
|
+
| Database | {{DATABASE}} |
|
|
10
|
+
|
|
11
|
+
## Core stack (required)
|
|
12
|
+
|
|
13
|
+
| System | Location | Role |
|
|
14
|
+
|--------|----------|------|
|
|
15
|
+
| Superpowers (obra) | `vendor/superpowers/`, skills in `.claude/skills/` | Workflow engine: brainstorming → design → plans → TDD → review — use these skills for every non-trivial task |
|
|
16
|
+
| Agency Agents | `vendor/agency-agents/`, agents in `.claude/agents/` | Specialist execution: pick the agent that matches the task from `.ai/agents.md` |
|
|
17
|
+
|
|
18
|
+
Do not treat Superpowers or Agency as optional add-ons. If `vendor/` is missing, run `npx ai-dev-setup init` without `--skip-vendor`.
|
|
19
|
+
|
|
20
|
+
## Read first (onboarding)
|
|
21
|
+
|
|
22
|
+
| Order | File |
|
|
23
|
+
|-------|------|
|
|
24
|
+
| 1 | `.ai/rules.md` |
|
|
25
|
+
| 2 | `.ai/workflow.md` |
|
|
26
|
+
| 3 | `docs/ARCHITECTURE.md` (fill if empty) |
|
|
27
|
+
|
|
28
|
+
Load other `docs/*` only when the task touches that area.
|
|
29
|
+
|
|
30
|
+
## Operating rules
|
|
31
|
+
|
|
32
|
+
- Prefer repo scripts: test `{{TEST_CMD}}`, lint `{{LINT_CMD}}`, build `{{BUILD_CMD}}`
|
|
33
|
+
- Keep replies short; point to paths instead of pasting large files
|
|
34
|
+
- After structural change, update `docs/ARCHITECTURE.md` in the same PR when behavior crosses modules
|
|
35
|
+
- Let Superpowers skills drive phase gates; align with `.ai/workflow.md` without duplicating long prose here
|
|
36
|
+
|
|
37
|
+
## Slash commands
|
|
38
|
+
|
|
39
|
+
| Command | Use |
|
|
40
|
+
|---------|-----|
|
|
41
|
+
| `/kickoff` | New feature: scope, risks, plan |
|
|
42
|
+
| `/review` | Pre-merge review checklist |
|
|
43
|
+
| `/ship` | Release/merge readiness |
|
|
44
|
+
|
|
45
|
+
## Docs index
|
|
46
|
+
|
|
47
|
+
| Topic | Path |
|
|
48
|
+
|-------|------|
|
|
49
|
+
| Conventions | `docs/CONVENTIONS.md` |
|
|
50
|
+
| Testing | `docs/TESTING-STRATEGY.md` |
|
|
51
|
+
| API | `docs/API-PATTERNS.md` |
|
|
52
|
+
| Errors | `docs/ERROR-HANDLING.md` |
|
|
53
|
+
| Security | `docs/SECURITY.md` |
|
|
54
|
+
|
|
55
|
+
## Out of scope for this file
|
|
56
|
+
|
|
57
|
+
- Do not duplicate `.ai/rules.md`
|
|
58
|
+
- Do not paste long examples—add them under `docs/` and link
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# /kickoff — {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Turn a feature request into a bounded plan aligned with `.ai/workflow.md`.
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
1. Restate objective and **non-goals** (3–7 bullets)
|
|
10
|
+
2. List unknowns; ask **one** blocking question if needed
|
|
11
|
+
3. Propose architecture touchpoints with links to `docs/ARCHITECTURE.md` sections to update
|
|
12
|
+
4. Emit a task list: each item has **path**, **change**, **verify** (`{{TEST_CMD}}` / manual check)
|
|
13
|
+
|
|
14
|
+
## Output format
|
|
15
|
+
|
|
16
|
+
- ### Summary
|
|
17
|
+
- ### Risks
|
|
18
|
+
- ### Tasks (numbered)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# /review — {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Catch correctness, safety, and maintainability issues before merge.
|
|
6
|
+
|
|
7
|
+
## Checklist
|
|
8
|
+
|
|
9
|
+
- [ ] Matches stated acceptance checks
|
|
10
|
+
- [ ] Errors handled at boundaries per `docs/ERROR-HANDLING.md`
|
|
11
|
+
- [ ] Tests updated or gap documented (`{{TEST_CMD}}`)
|
|
12
|
+
- [ ] No secrets / PII in logs
|
|
13
|
+
- [ ] Public API or behavior change reflected in docs
|
|
14
|
+
- [ ] `{{LINT_CMD}}` clean or waivers explained inline
|
|
15
|
+
|
|
16
|
+
## Output format
|
|
17
|
+
|
|
18
|
+
- ### Findings (severity: high/med/low)
|
|
19
|
+
- ### Suggested patches (file-scoped)
|
|
20
|
+
- ### Merge verdict: approve | changes requested
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# /ship — {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Confirm release/merge readiness.
|
|
6
|
+
|
|
7
|
+
## Verify
|
|
8
|
+
|
|
9
|
+
- [ ] `{{LINT_CMD}}` passes
|
|
10
|
+
- [ ] `{{TEST_CMD}}` passes
|
|
11
|
+
- [ ] `{{BUILD_CMD}}` passes (if applicable)
|
|
12
|
+
- [ ] Changelog / release notes updated when user-visible
|
|
13
|
+
- [ ] Migrations or feature flags documented
|
|
14
|
+
|
|
15
|
+
## Output format
|
|
16
|
+
|
|
17
|
+
- ### Ship status: ready | blocked
|
|
18
|
+
- ### Blockers (if any)
|
|
19
|
+
- ### Rollback notes
|