create-agent-room 1.2.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.
Files changed (58) hide show
  1. package/README.md +229 -0
  2. package/bin/cli.js +186 -0
  3. package/examples/python-project/.agent-room.json +14 -0
  4. package/examples/python-project/AGENTS.md +32 -0
  5. package/examples/rust-project/.agent-room.json +12 -0
  6. package/examples/rust-project/AGENTS.md +32 -0
  7. package/lib/color.js +31 -0
  8. package/lib/fsutil.js +218 -0
  9. package/lib/init.js +660 -0
  10. package/lib/lint-sessions.js +278 -0
  11. package/lib/metrics.js +190 -0
  12. package/lib/pr.js +176 -0
  13. package/lib/prompt.js +20 -0
  14. package/lib/session-utils.js +213 -0
  15. package/lib/sync.js +138 -0
  16. package/lib/validate.js +179 -0
  17. package/package.json +48 -0
  18. package/templates/.agent-room/anti-patterns.md +22 -0
  19. package/templates/.agent-room/coordination/handoff-protocol.md +60 -0
  20. package/templates/.agent-room/coordination/scope-boundaries.md +57 -0
  21. package/templates/.agent-room/coordination/session-log-format.md +62 -0
  22. package/templates/.agent-room/decisions.md +17 -0
  23. package/templates/.agent-room/guardrails.json +23 -0
  24. package/templates/.agent-room/guardrails.md +56 -0
  25. package/templates/.agent-room/principles.md +306 -0
  26. package/templates/.agent-room/sessions/.gitkeep +4 -0
  27. package/templates/.agent-room/skills/brainstorming.md +64 -0
  28. package/templates/.agent-room/skills/closing-the-loop.md +67 -0
  29. package/templates/.agent-room/skills/systematic-debugging.md +85 -0
  30. package/templates/.agent-room/skills/test-driven-development.md +100 -0
  31. package/templates/.agent-room/skills/verification-before-completion.md +56 -0
  32. package/templates/.agent-room/skills/writing-plans.md +87 -0
  33. package/templates/.agent-room/workflow-classifier.md +127 -0
  34. package/templates/AGENTS.md.tmpl +85 -0
  35. package/templates/adapters/CLAUDE.md.tmpl +38 -0
  36. package/templates/adapters/claude-hooks/close-the-loop-check.js +96 -0
  37. package/templates/adapters/clinerules.tmpl +14 -0
  38. package/templates/adapters/codexrules.tmpl +45 -0
  39. package/templates/adapters/cursorrules.tmpl +14 -0
  40. package/templates/adapters/git-hooks/guardrails-check.js +140 -0
  41. package/templates/adapters/git-hooks/pre-commit.tmpl +43 -0
  42. package/templates/adapters/windsurfrules.tmpl +14 -0
  43. package/templates/docs/plans/.gitkeep +0 -0
  44. package/templates/skill-packs/api-design/api-design.md +152 -0
  45. package/templates/skill-packs/code-review/code-review.md +113 -0
  46. package/templates/skill-packs/database/database-migrations.md +123 -0
  47. package/templates/skill-packs/documentation/documentation.md +155 -0
  48. package/templates/skill-packs/observability/observability.md +128 -0
  49. package/templates/skill-packs/performance/performance-optimization.md +150 -0
  50. package/templates/skill-packs/release/release-management.md +145 -0
  51. package/templates/skill-packs/security/security-principles.md +127 -0
  52. package/templates/skill-packs/testing/integration-testing.md +127 -0
  53. package/templates/stacks/python/.agent-room/skills/python-testing.md +59 -0
  54. package/templates/stacks/python/AGENTS.md.tmpl +35 -0
  55. package/templates/stacks/react/.agent-room/skills/react-component-testing.md +76 -0
  56. package/templates/stacks/react/AGENTS.md.tmpl +37 -0
  57. package/templates/stacks/typescript/.agent-room/skills/typescript-testing.md +63 -0
  58. package/templates/stacks/typescript/AGENTS.md.tmpl +36 -0
package/lib/init.js ADDED
@@ -0,0 +1,660 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execFileSync } = require('child_process');
7
+ const { ask } = require('./prompt');
8
+ const { ensureDir, resolveTemplateSources, copyFileInherited, copyDirInherited, copyFile } = require('./fsutil');
9
+ const { green, yellow, red, cyan, bold } = require('./color');
10
+
11
+ const VALID_TOOLS = ['claude', 'cursor', 'windsurf', 'cline', 'codex', 'git', 'none'];
12
+
13
+ function parseSafeJSON(str) {
14
+ if (!str || !str.trim()) return {};
15
+ const clean = str.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? '' : m));
16
+ return JSON.parse(clean);
17
+ }
18
+
19
+ function detectWorkspace(target) {
20
+ const result = {
21
+ language: null,
22
+ packageManager: null,
23
+ tools: []
24
+ };
25
+
26
+ if (!fs.existsSync(target)) return result;
27
+
28
+ // 1. Language & Manager Detection
29
+ if (fs.existsSync(path.join(target, 'Cargo.toml'))) {
30
+ result.language = 'rust';
31
+ result.packageManager = 'cargo';
32
+ } else if (fs.existsSync(path.join(target, 'go.mod'))) {
33
+ result.language = 'go';
34
+ result.packageManager = 'go';
35
+ } else if (fs.existsSync(path.join(target, 'package.json'))) {
36
+ result.language = 'javascript';
37
+ if (fs.existsSync(path.join(target, 'tsconfig.json'))) {
38
+ result.language = 'typescript';
39
+ }
40
+ if (fs.existsSync(path.join(target, 'pnpm-lock.yaml'))) {
41
+ result.packageManager = 'pnpm';
42
+ } else if (fs.existsSync(path.join(target, 'yarn.lock'))) {
43
+ result.packageManager = 'yarn';
44
+ } else if (fs.existsSync(path.join(target, 'bun.lockb')) || fs.existsSync(path.join(target, 'bun.lock'))) {
45
+ result.packageManager = 'bun';
46
+ } else {
47
+ result.packageManager = 'npm';
48
+ }
49
+ } else if (
50
+ fs.existsSync(path.join(target, 'Pipfile')) ||
51
+ fs.existsSync(path.join(target, 'requirements.txt')) ||
52
+ fs.existsSync(path.join(target, 'pyproject.toml'))
53
+ ) {
54
+ result.language = 'python';
55
+ if (fs.existsSync(path.join(target, 'pyproject.toml'))) {
56
+ result.packageManager = 'poetry';
57
+ } else if (fs.existsSync(path.join(target, 'Pipfile'))) {
58
+ result.packageManager = 'pipenv';
59
+ } else {
60
+ result.packageManager = 'pip';
61
+ }
62
+ }
63
+
64
+ // 2. Tools Auto-detection
65
+ if (fs.existsSync(path.join(target, '.git'))) {
66
+ result.tools.push('git');
67
+ }
68
+ if (fs.existsSync(path.join(target, '.claude')) || fs.existsSync(path.join(target, 'CLAUDE.md'))) {
69
+ result.tools.push('claude');
70
+ }
71
+ if (
72
+ fs.existsSync(path.join(target, '.cursor')) ||
73
+ fs.existsSync(path.join(target, '.cursorrules')) ||
74
+ fs.existsSync(path.join(target, '.cursor/rules'))
75
+ ) {
76
+ result.tools.push('cursor');
77
+ }
78
+ if (fs.existsSync(path.join(target, '.windsurfrules'))) {
79
+ result.tools.push('windsurf');
80
+ }
81
+ if (fs.existsSync(path.join(target, '.clinerules'))) {
82
+ result.tools.push('cline');
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ async function resolveLanguage(args, detected) {
89
+ if (args.language) return args.language;
90
+ const defaultLang = detected.language || 'javascript';
91
+ if (args.yes || !process.stdin.isTTY) return defaultLang;
92
+ const answer = await ask(`Project programming language (default: ${defaultLang}): `);
93
+ return answer || defaultLang;
94
+ }
95
+
96
+ async function resolvePackageManager(args, language, detected) {
97
+ if (args['package-manager']) return args['package-manager'];
98
+ const defaultPM = (() => {
99
+ const lang = (language || 'javascript').toLowerCase();
100
+ if (detected.language && detected.language.toLowerCase() === lang && detected.packageManager) {
101
+ return detected.packageManager;
102
+ }
103
+ if (lang === 'python') return 'pip';
104
+ if (lang === 'go') return 'go';
105
+ if (lang === 'rust') return 'cargo';
106
+ return 'npm';
107
+ })();
108
+ if (args.yes || !process.stdin.isTTY) return defaultPM;
109
+ const answer = await ask(`Package manager (default: ${defaultPM}): `);
110
+ return answer || defaultPM;
111
+ }
112
+
113
+ async function resolveBranch(args) {
114
+ if (args.branch) return args.branch;
115
+ if (args.yes || !process.stdin.isTTY) return 'main';
116
+ const answer = await ask('Default git branch (default: main): ');
117
+ return answer || 'main';
118
+ }
119
+
120
+ async function resolveSkillPacks(args) {
121
+ if (args['skill-packs']) {
122
+ return args['skill-packs']
123
+ .split(',')
124
+ .map((s) => s.trim())
125
+ .filter(Boolean);
126
+ }
127
+ if (args.yes || !process.stdin.isTTY) {
128
+ return [];
129
+ }
130
+ const answer = await ask('Optional skill packs to include? (comma-separated: testing,security,release,code-review,api-design,database,performance,observability,documentation; blank = none): ');
131
+ return answer
132
+ .split(',')
133
+ .map((s) => s.trim())
134
+ .filter(Boolean);
135
+ }
136
+
137
+ function getDerivedCommands(language, packageManager) {
138
+ const lang = (language || 'javascript').toLowerCase();
139
+ const pm = (packageManager || 'npm').toLowerCase();
140
+
141
+ let testCommand = `${pm} test`;
142
+ let lintCommand = `${pm} run lint`;
143
+
144
+ if (lang === 'typescript' || lang === 'javascript') {
145
+ if (pm === 'npm') {
146
+ testCommand = 'npm test';
147
+ lintCommand = 'npm run lint';
148
+ } else if (pm === 'yarn') {
149
+ testCommand = 'yarn test';
150
+ lintCommand = 'yarn lint';
151
+ } else if (pm === 'pnpm') {
152
+ testCommand = 'pnpm test';
153
+ lintCommand = 'pnpm lint';
154
+ } else if (pm === 'bun') {
155
+ testCommand = 'bun test';
156
+ lintCommand = 'bun lint';
157
+ }
158
+ } else if (lang === 'python') {
159
+ testCommand = 'pytest';
160
+ lintCommand = 'flake8 .';
161
+ if (pm === 'poetry') {
162
+ testCommand = 'poetry run pytest';
163
+ lintCommand = 'poetry run flake8 .';
164
+ } else if (pm === 'pipenv') {
165
+ testCommand = 'pipenv run pytest';
166
+ lintCommand = 'pipenv run flake8 .';
167
+ }
168
+ } else if (lang === 'go') {
169
+ testCommand = 'go test ./...';
170
+ lintCommand = 'golangci-lint run';
171
+ } else if (lang === 'rust') {
172
+ testCommand = 'cargo test';
173
+ lintCommand = 'cargo clippy';
174
+ } else if (lang === 'java' || lang === 'kotlin') {
175
+ if (pm === 'gradle') {
176
+ testCommand = './gradlew test';
177
+ lintCommand = './gradlew checkstyleMain';
178
+ } else {
179
+ testCommand = 'mvn test';
180
+ lintCommand = 'mvn checkstyle:check';
181
+ }
182
+ }
183
+
184
+ return { testCommand, lintCommand };
185
+ }
186
+
187
+ function isGitUrl(str) {
188
+ return (
189
+ str.startsWith('git+') ||
190
+ str.startsWith('git://') ||
191
+ str.includes('github.com/') ||
192
+ str.includes('gitlab.com/') ||
193
+ str.endsWith('.git')
194
+ );
195
+ }
196
+
197
+ function copySkillsFromDirectory(sourceDir, target, vars, opts) {
198
+ const results = [];
199
+ const destDir = path.join(target, '.agent-room', 'skills');
200
+ ensureDir(destDir);
201
+
202
+ const skillsDir = path.join(sourceDir, 'skills');
203
+ const hasSkillsSubdir = fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory();
204
+
205
+ const scanDir = hasSkillsSubdir ? skillsDir : sourceDir;
206
+
207
+ const files = fs.readdirSync(scanDir).filter(
208
+ (f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md' && f.toLowerCase() !== 'agents.md'
209
+ );
210
+
211
+ const root = (opts && opts.root) || destDir;
212
+
213
+ for (const file of files) {
214
+ const srcPath = path.join(scanDir, file);
215
+ const destPath = path.join(destDir, file);
216
+ const res = copyFile(srcPath, destPath, vars, opts);
217
+ results.push(Object.assign({ path: path.relative(root, destPath) }, res));
218
+ }
219
+ return results;
220
+ }
221
+
222
+ function installSkillPacks(target, skillPacks, srcDirs, vars, opts) {
223
+ const results = [];
224
+ const validPacks = ['testing', 'security', 'release', 'code-review', 'api-design', 'database', 'performance', 'observability', 'documentation'];
225
+ const tempDirs = [];
226
+
227
+ for (const pack of skillPacks) {
228
+ if (validPacks.includes(pack)) {
229
+ const packSrcDirs = srcDirs.map((d) => path.join(d, 'skill-packs', pack));
230
+ const destDir = path.join(target, '.agent-room', 'skills');
231
+ results.push(...copyDirInherited(packSrcDirs, '', destDir, vars, opts));
232
+ } else if (isGitUrl(pack)) {
233
+ const tempCloneDir = path.join(os.tmpdir(), `create-agent-room-clone-${Date.now()}`);
234
+ tempDirs.push(tempCloneDir);
235
+ try {
236
+ console.log(`Cloning external skill pack from: ${cyan(pack)}`);
237
+ const cleanUrl = pack.replace(/^git\+/, '');
238
+ execFileSync('git', ['clone', '--depth', '1', cleanUrl, tempCloneDir], { stdio: 'ignore' });
239
+ results.push(...copySkillsFromDirectory(tempCloneDir, target, vars, opts));
240
+ } catch (err) {
241
+ console.warn(red(`Warning: Failed to clone external skill pack from ${pack}: ${err.message}`));
242
+ }
243
+ } else {
244
+ const localPath = path.resolve(pack);
245
+ if (fs.existsSync(localPath) && fs.statSync(localPath).isDirectory()) {
246
+ results.push(...copySkillsFromDirectory(localPath, target, vars, opts));
247
+ } else {
248
+ console.warn(yellow(`Warning: Unknown skill pack, git URL, or local path skipped: ${pack}`));
249
+ }
250
+ }
251
+ }
252
+
253
+ // Cleanup temporary clone directories
254
+ for (const d of tempDirs) {
255
+ try {
256
+ fs.rmSync(d, { recursive: true, force: true });
257
+ } catch (err) {
258
+ // ignore
259
+ }
260
+ }
261
+
262
+ return results;
263
+ }
264
+
265
+ async function resolveName(target, args) {
266
+ if (args.name) return args.name;
267
+ const base = path.basename(target);
268
+ if (args.yes || !process.stdin.isTTY) return base;
269
+ const answer = await ask(`Project name (default: ${base}): `);
270
+ return answer || base;
271
+ }
272
+
273
+ async function resolveTools(args, detected) {
274
+ if (args.tools) {
275
+ return args.tools
276
+ .split(',')
277
+ .map((t) => t.trim())
278
+ .filter(Boolean);
279
+ }
280
+ const defaultTools = detected.tools.length > 0 ? detected.tools : [];
281
+ if (args.yes || !process.stdin.isTTY) {
282
+ return defaultTools;
283
+ }
284
+ const defaultStr = defaultTools.join(',') || 'none';
285
+ const answer = await ask(
286
+ `Which agent tools should get adapters? (comma-separated: claude,cursor,windsurf,cline,codex,git; default: ${defaultStr}): `
287
+ );
288
+ if (!answer.trim()) return defaultTools.length > 0 ? defaultTools : [];
289
+ return answer
290
+ .split(',')
291
+ .map((t) => t.trim())
292
+ .filter(Boolean);
293
+ }
294
+
295
+ function gitInit(target) {
296
+ try {
297
+ execFileSync('git', ['init'], { cwd: target, stdio: 'ignore' });
298
+ execFileSync('git', ['add', '.'], { cwd: target, stdio: 'ignore' });
299
+ execFileSync('git', ['commit', '-m', 'Scaffold project with create-agent-room'], {
300
+ cwd: target,
301
+ stdio: 'ignore',
302
+ });
303
+ return true;
304
+ } catch (err) {
305
+ return false;
306
+ }
307
+ }
308
+
309
+ function mirrorSkillsToClaude(target, opts) {
310
+ const srcDir = path.join(target, '.agent-room', 'skills');
311
+ const results = [];
312
+ if (!fs.existsSync(srcDir)) return results;
313
+ for (const file of fs.readdirSync(srcDir)) {
314
+ if (!file.endsWith('.md')) continue;
315
+ const skillName = file.replace(/\.md$/, '');
316
+ const dest = path.join(target, '.claude', 'skills', skillName, 'SKILL.md');
317
+ const exists = fs.existsSync(dest);
318
+ ensureDir(path.dirname(dest));
319
+ fs.writeFileSync(dest, fs.readFileSync(path.join(srcDir, file), 'utf8'));
320
+ if (!exists && opts && typeof opts.onWrite === 'function') {
321
+ opts.onWrite(dest);
322
+ }
323
+ results.push({ path: path.relative(target, dest), written: true });
324
+ }
325
+ return results;
326
+ }
327
+
328
+ const STOP_HOOK_COMMAND = 'node .agent-room/hooks/close-the-loop-check.js';
329
+
330
+ function installCloseTheLoopHook(target, srcDirs, vars, opts) {
331
+ const results = [];
332
+
333
+ results.push(
334
+ Object.assign(
335
+ { path: path.join('.agent-room', 'hooks', 'close-the-loop-check.js') },
336
+ copyFileInherited(
337
+ srcDirs,
338
+ path.join('adapters', 'claude-hooks', 'close-the-loop-check.js'),
339
+ path.join(target, '.agent-room', 'hooks', 'close-the-loop-check.js'),
340
+ vars,
341
+ opts
342
+ )
343
+ )
344
+ );
345
+
346
+ const settingsPath = path.join(target, '.claude', 'settings.json');
347
+ let settings = {};
348
+ if (fs.existsSync(settingsPath)) {
349
+ try {
350
+ const content = fs.readFileSync(settingsPath, 'utf8');
351
+ settings = parseSafeJSON(content);
352
+ } catch (err) {
353
+ console.warn(yellow(`Warning: Failed to parse existing .claude/settings.json: ${err.message}. Re-initializing settings.`));
354
+ settings = {};
355
+ }
356
+ }
357
+
358
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
359
+ settings = {};
360
+ }
361
+
362
+ settings.hooks = settings.hooks || {};
363
+ settings.hooks.Stop = settings.hooks.Stop || [];
364
+
365
+ const alreadyWired = settings.hooks.Stop.some(
366
+ (entry) =>
367
+ Array.isArray(entry.hooks) && entry.hooks.some((h) => h.command === STOP_HOOK_COMMAND)
368
+ );
369
+
370
+ if (!alreadyWired) {
371
+ settings.hooks.Stop.push({ hooks: [{ type: 'command', command: STOP_HOOK_COMMAND }] });
372
+ const settingsExists = fs.existsSync(settingsPath);
373
+ ensureDir(path.dirname(settingsPath));
374
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
375
+ if (!settingsExists && opts && typeof opts.onWrite === 'function') {
376
+ opts.onWrite(settingsPath);
377
+ }
378
+ results.push({ path: path.join('.claude', 'settings.json'), written: true });
379
+ } else {
380
+ results.push({
381
+ path: path.join('.claude', 'settings.json'),
382
+ written: false,
383
+ reason: 'already wired',
384
+ });
385
+ }
386
+
387
+ return results;
388
+ }
389
+
390
+ function reportResults(results) {
391
+ for (const r of results) {
392
+ if (r.written) {
393
+ console.log(` ${green('created')} ${cyan(r.path)}`);
394
+ } else if (r.reason === 'already wired') {
395
+ console.log(` ${yellow('skipped')} ${r.path} (hook already wired)`);
396
+ } else {
397
+ console.log(` ${yellow('skipped')} ${r.path} (already exists, use --force to overwrite)`);
398
+ }
399
+ }
400
+ }
401
+
402
+ async function runInit(target, args) {
403
+ ensureDir(target);
404
+
405
+ const detected = detectWorkspace(target);
406
+ const name = await resolveName(target, args);
407
+ const tools = await resolveTools(args, detected);
408
+ const unknown = tools.filter((t) => !VALID_TOOLS.includes(t));
409
+ if (unknown.length) {
410
+ throw new Error(`Unknown tool(s): ${unknown.join(', ')}. Valid: ${VALID_TOOLS.join(', ')}`);
411
+ }
412
+
413
+ let isGit = fs.existsSync(path.join(target, '.git'));
414
+ if (args.git && !isGit) {
415
+ try {
416
+ execFileSync('git', ['init'], { cwd: target, stdio: 'ignore' });
417
+ isGit = true;
418
+ } catch (err) {
419
+ // ignore
420
+ }
421
+ }
422
+
423
+ const language = await resolveLanguage(args, detected);
424
+ const packageManager = await resolvePackageManager(args, language, detected);
425
+ const defaultBranch = await resolveBranch(args);
426
+ const skillPacks = await resolveSkillPacks(args);
427
+ const { testCommand, lintCommand } = getDerivedCommands(language, packageManager);
428
+
429
+ const srcDirs = resolveTemplateSources(target, args, language, packageManager);
430
+
431
+ const vars = {
432
+ PROJECT_NAME: name,
433
+ LANGUAGE: language,
434
+ PACKAGE_MANAGER: packageManager,
435
+ 'package-manager': packageManager,
436
+ DEFAULT_BRANCH: defaultBranch,
437
+ 'default-branch': defaultBranch,
438
+ TEST_COMMAND: testCommand,
439
+ 'test-command': testCommand,
440
+ LINT_COMMAND: lintCommand,
441
+ 'lint-command': lintCommand
442
+ };
443
+
444
+ const createdFiles = [];
445
+ const opts = {
446
+ force: !!args.force,
447
+ root: target,
448
+ onWrite: (filePath) => {
449
+ createdFiles.push(filePath);
450
+ }
451
+ };
452
+
453
+ console.log(`Scaffolding agent-room structure into ${cyan(target)}\n`);
454
+
455
+ const results = [];
456
+
457
+ try {
458
+ results.push(
459
+ Object.assign(
460
+ { path: 'AGENTS.md' },
461
+ copyFileInherited(srcDirs, 'AGENTS.md.tmpl', path.join(target, 'AGENTS.md'), vars, opts)
462
+ )
463
+ );
464
+ results.push(...copyDirInherited(srcDirs, '.agent-room', path.join(target, '.agent-room'), vars, opts));
465
+ results.push(...copyDirInherited(srcDirs, 'docs', path.join(target, 'docs'), vars, opts));
466
+
467
+ results.push(...installSkillPacks(target, skillPacks, srcDirs, vars, opts));
468
+
469
+ const configPath = path.join(target, '.agent-room.json');
470
+ if (!fs.existsSync(configPath) || opts.force) {
471
+ const configData = {
472
+ name,
473
+ tools,
474
+ language,
475
+ packageManager,
476
+ defaultBranch,
477
+ skillPacks
478
+ };
479
+ const configExists = fs.existsSync(configPath);
480
+ fs.writeFileSync(configPath, JSON.stringify(configData, null, 2) + '\n');
481
+ if (!configExists) {
482
+ createdFiles.push(configPath);
483
+ }
484
+ results.push({ path: '.agent-room.json', written: true });
485
+ } else {
486
+ results.push({ path: '.agent-room.json', written: false, reason: 'exists' });
487
+ }
488
+
489
+ if (tools.includes('claude')) {
490
+ results.push(
491
+ Object.assign(
492
+ { path: 'CLAUDE.md' },
493
+ copyFileInherited(
494
+ srcDirs,
495
+ path.join('adapters', 'CLAUDE.md.tmpl'),
496
+ path.join(target, 'CLAUDE.md'),
497
+ vars,
498
+ opts
499
+ )
500
+ )
501
+ );
502
+ results.push(...mirrorSkillsToClaude(target, opts));
503
+ results.push(...installCloseTheLoopHook(target, srcDirs, vars, opts));
504
+ }
505
+
506
+ if (tools.includes('cursor')) {
507
+ results.push(
508
+ Object.assign(
509
+ { path: path.join('.cursor', 'rules', 'agent-room.md') },
510
+ copyFileInherited(
511
+ srcDirs,
512
+ path.join('adapters', 'cursorrules.tmpl'),
513
+ path.join(target, '.cursor', 'rules', 'agent-room.md'),
514
+ vars,
515
+ opts
516
+ )
517
+ )
518
+ );
519
+ }
520
+
521
+ if (tools.includes('windsurf')) {
522
+ results.push(
523
+ Object.assign(
524
+ { path: '.windsurfrules' },
525
+ copyFileInherited(
526
+ srcDirs,
527
+ path.join('adapters', 'windsurfrules.tmpl'),
528
+ path.join(target, '.windsurfrules'),
529
+ vars,
530
+ opts
531
+ )
532
+ )
533
+ );
534
+ }
535
+
536
+ if (tools.includes('cline')) {
537
+ results.push(
538
+ Object.assign(
539
+ { path: '.clinerules' },
540
+ copyFileInherited(
541
+ srcDirs,
542
+ path.join('adapters', 'clinerules.tmpl'),
543
+ path.join(target, '.clinerules'),
544
+ vars,
545
+ opts
546
+ )
547
+ )
548
+ );
549
+ }
550
+
551
+ if (tools.includes('codex')) {
552
+ results.push(
553
+ Object.assign(
554
+ { path: '.codexrules' },
555
+ copyFileInherited(
556
+ srcDirs,
557
+ path.join('adapters', 'codexrules.tmpl'),
558
+ path.join(target, '.codexrules'),
559
+ vars,
560
+ opts
561
+ )
562
+ )
563
+ );
564
+ }
565
+
566
+ if (tools.includes('git') && isGit) {
567
+ const hookDest = path.join(target, '.git', 'hooks', 'pre-commit');
568
+ const hookExists = fs.existsSync(hookDest);
569
+ results.push(
570
+ Object.assign(
571
+ { path: '.git/hooks/pre-commit' },
572
+ copyFileInherited(
573
+ srcDirs,
574
+ path.join('adapters', 'git-hooks', 'pre-commit.tmpl'),
575
+ hookDest,
576
+ vars,
577
+ opts
578
+ )
579
+ )
580
+ );
581
+ if (!hookExists && fs.existsSync(hookDest)) {
582
+ createdFiles.push(hookDest);
583
+ }
584
+ try {
585
+ fs.chmodSync(hookDest, '755');
586
+ } catch (err) {
587
+ // Ignore if chmod fails (e.g. on Windows)
588
+ }
589
+
590
+ // Install guardrails-check hook
591
+ const guardrailsHookDest = path.join(target, '.agent-room', 'hooks', 'guardrails-check.js');
592
+ const guardrailsHookExists = fs.existsSync(guardrailsHookDest);
593
+ results.push(
594
+ Object.assign(
595
+ { path: '.agent-room/hooks/guardrails-check.js' },
596
+ copyFileInherited(
597
+ srcDirs,
598
+ path.join('adapters', 'git-hooks', 'guardrails-check.js'),
599
+ guardrailsHookDest,
600
+ vars,
601
+ opts
602
+ )
603
+ )
604
+ );
605
+ if (!guardrailsHookExists && fs.existsSync(guardrailsHookDest)) {
606
+ createdFiles.push(guardrailsHookDest);
607
+ }
608
+ try {
609
+ fs.chmodSync(guardrailsHookDest, '755');
610
+ } catch (err) {
611
+ // Ignore if chmod fails (e.g. on Windows)
612
+ }
613
+ } else if (tools.includes('git') && !isGit) {
614
+ console.warn(yellow('\nWarning: Git adapter requested but target is not a git repository. Skipping pre-commit hook.'));
615
+ }
616
+
617
+ } catch (err) {
618
+ console.error(red(`\nScaffolding failed: ${err.message}`));
619
+ console.log(yellow('Rolling back created files...'));
620
+ for (const file of createdFiles) {
621
+ if (fs.existsSync(file)) {
622
+ try {
623
+ fs.rmSync(file, { force: true });
624
+ } catch (e) {
625
+ // ignore rollback failures
626
+ }
627
+ }
628
+ }
629
+ throw err;
630
+ }
631
+
632
+ reportResults(results);
633
+
634
+ if (args.git) {
635
+ const ok = gitInit(target);
636
+ console.log(
637
+ ok
638
+ ? `\n${green('Initialized git repo and created initial commit.')}`
639
+ : `\n${yellow('Git init/commit skipped (already a repo, nothing to commit, or git unavailable).')}`
640
+ );
641
+ }
642
+
643
+ console.log(`\n======================================================`);
644
+ console.log(`${bold(green('Scaffolding Complete!'))}`);
645
+ console.log(`------------------------------------------------------`);
646
+ console.log(`${bold('Project Name:')} ${cyan(name)}`);
647
+ console.log(`${bold('Language:')} ${cyan(language)} (${packageManager})`);
648
+ console.log(`${bold('Default Branch:')} ${cyan(defaultBranch)}`);
649
+ if (skillPacks.length > 0) {
650
+ console.log(`${bold('Skill Packs:')} ${cyan(skillPacks.join(', '))}`);
651
+ }
652
+ if (tools.length > 0 && tools[0] !== 'none') {
653
+ console.log(`${bold('Adapters:')} ${cyan(tools.join(', '))}`);
654
+ }
655
+ console.log(`======================================================`);
656
+
657
+ console.log(`\nDone. Start by reading ${bold(cyan(path.join(target, 'AGENTS.md')))}.`);
658
+ }
659
+
660
+ module.exports = { runInit, mirrorSkillsToClaude, VALID_TOOLS, parseSafeJSON, detectWorkspace };