diff --git a/packages/rules/__tests__/init/detect-stack.test.js b/packages/rules/__tests__/init/detect-stack.test.js new file mode 100644 index 00000000..20f4154c --- /dev/null +++ b/packages/rules/__tests__/init/detect-stack.test.js @@ -0,0 +1,120 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); + +const { detectStack } = require('../../lib/init/detect-stack'); + +describe('detectStack', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cb-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('detects Node.js project from package.json', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'my-app', dependencies: { express: '^4.0.0' } }) + ); + const result = detectStack(tmpDir); + assert.equal(result.runtime, 'node'); + assert.ok(result.frameworks.includes('express')); + }); + + it('detects React project', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ dependencies: { react: '^18.0.0', 'react-dom': '^18.0.0' } }) + ); + const result = detectStack(tmpDir); + assert.equal(result.runtime, 'node'); + assert.ok(result.frameworks.includes('react')); + assert.equal(result.category, 'frontend'); + }); + + it('detects Next.js project', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ dependencies: { next: '^14.0.0', react: '^18.0.0' } }) + ); + const result = detectStack(tmpDir); + assert.ok(result.frameworks.includes('next')); + assert.equal(result.category, 'fullstack'); + }); + + it('detects NestJS backend project', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ dependencies: { '@nestjs/core': '^10.0.0' } }) + ); + const result = detectStack(tmpDir); + assert.ok(result.frameworks.includes('nestjs')); + assert.equal(result.category, 'backend'); + }); + + it('detects Python project from pyproject.toml', () => { + fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "myapp"\n'); + const result = detectStack(tmpDir); + assert.equal(result.runtime, 'python'); + assert.equal(result.category, 'backend'); + }); + + it('detects Django in pyproject.toml', () => { + fs.writeFileSync( + path.join(tmpDir, 'pyproject.toml'), + '[project]\ndependencies = ["django>=4.0"]\n' + ); + const result = detectStack(tmpDir); + assert.equal(result.runtime, 'python'); + assert.ok(result.frameworks.includes('django')); + }); + + it('detects Go project from go.mod', () => { + fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/myapp\n\ngo 1.21\n'); + const result = detectStack(tmpDir); + assert.equal(result.runtime, 'go'); + assert.equal(result.category, 'backend'); + }); + + it('detects Rust project from Cargo.toml', () => { + fs.writeFileSync( + path.join(tmpDir, 'Cargo.toml'), + '[package]\nname = "myapp"\nversion = "0.1.0"\n' + ); + const result = detectStack(tmpDir); + assert.equal(result.runtime, 'rust'); + assert.equal(result.category, 'backend'); + }); + + it('returns unknown for empty directory', () => { + const result = detectStack(tmpDir); + assert.equal(result.runtime, 'unknown'); + assert.deepEqual(result.frameworks, []); + assert.equal(result.category, 'unknown'); + }); + + it('detects TypeScript usage', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ devDependencies: { typescript: '^5.0.0' } }) + ); + const result = detectStack(tmpDir); + assert.equal(result.language, 'typescript'); + }); + + it('detects Vue.js frontend project', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ dependencies: { vue: '^3.0.0' } }) + ); + const result = detectStack(tmpDir); + assert.ok(result.frameworks.includes('vue')); + assert.equal(result.category, 'frontend'); + }); +}); diff --git a/packages/rules/__tests__/init/generate-config.test.js b/packages/rules/__tests__/init/generate-config.test.js new file mode 100644 index 00000000..e9627527 --- /dev/null +++ b/packages/rules/__tests__/init/generate-config.test.js @@ -0,0 +1,77 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); + +const { generateConfig } = require('../../lib/init/generate-config'); + +describe('generateConfig', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cb-cfg-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('creates codingbuddy.config.json with defaults', () => { + const options = { + language: 'en', + primaryAgent: 'software-engineer', + techStack: { runtime: 'node', language: 'typescript', frameworks: ['next'], category: 'fullstack' }, + }; + generateConfig(tmpDir, options); + + const configPath = path.join(tmpDir, 'codingbuddy.config.json'); + assert.ok(fs.existsSync(configPath)); + + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + assert.equal(config.language, 'en'); + assert.equal(config.primaryAgent, 'software-engineer'); + assert.equal(config.techStack.runtime, 'node'); + }); + + it('includes all required fields', () => { + generateConfig(tmpDir, { + language: 'ko', + primaryAgent: 'backend-developer', + techStack: { runtime: 'python', language: 'python', frameworks: ['django'], category: 'backend' }, + }); + + const config = JSON.parse(fs.readFileSync(path.join(tmpDir, 'codingbuddy.config.json'), 'utf-8')); + assert.ok('language' in config); + assert.ok('primaryAgent' in config); + assert.ok('techStack' in config); + assert.ok('version' in config); + }); + + it('does not overwrite existing config', () => { + const configPath = path.join(tmpDir, 'codingbuddy.config.json'); + fs.writeFileSync(configPath, JSON.stringify({ language: 'ja', custom: true })); + + const result = generateConfig(tmpDir, { + language: 'en', + primaryAgent: 'software-engineer', + techStack: { runtime: 'node', language: 'javascript', frameworks: [], category: 'backend' }, + }); + + assert.equal(result.skipped, true); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + assert.equal(config.language, 'ja'); + assert.equal(config.custom, true); + }); + + it('writes formatted JSON', () => { + generateConfig(tmpDir, { + language: 'en', + primaryAgent: 'frontend-developer', + techStack: { runtime: 'node', language: 'typescript', frameworks: ['react'], category: 'frontend' }, + }); + + const raw = fs.readFileSync(path.join(tmpDir, 'codingbuddy.config.json'), 'utf-8'); + assert.ok(raw.includes('\n'), 'should be formatted with newlines'); + }); +}); diff --git a/packages/rules/__tests__/init/scaffold.test.js b/packages/rules/__tests__/init/scaffold.test.js new file mode 100644 index 00000000..9de6007d --- /dev/null +++ b/packages/rules/__tests__/init/scaffold.test.js @@ -0,0 +1,59 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); + +const { scaffold } = require('../../lib/init/scaffold'); + +describe('scaffold', () => { + let tmpDir; + const rulesSource = path.resolve(__dirname, '../../.ai-rules'); + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cb-scaffold-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('creates .ai-rules directory in target', () => { + scaffold(tmpDir, { source: rulesSource }); + assert.ok(fs.existsSync(path.join(tmpDir, '.ai-rules'))); + }); + + it('copies rules/ directory', () => { + scaffold(tmpDir, { source: rulesSource }); + assert.ok(fs.existsSync(path.join(tmpDir, '.ai-rules', 'rules'))); + assert.ok(fs.existsSync(path.join(tmpDir, '.ai-rules', 'rules', 'core.md'))); + }); + + it('copies agents/ directory', () => { + scaffold(tmpDir, { source: rulesSource }); + assert.ok(fs.existsSync(path.join(tmpDir, '.ai-rules', 'agents'))); + }); + + it('copies README.md', () => { + scaffold(tmpDir, { source: rulesSource }); + assert.ok(fs.existsSync(path.join(tmpDir, '.ai-rules', 'README.md'))); + }); + + it('does not overwrite existing .ai-rules', () => { + const existingDir = path.join(tmpDir, '.ai-rules'); + fs.mkdirSync(existingDir); + fs.writeFileSync(path.join(existingDir, 'custom.md'), 'my rules'); + + const result = scaffold(tmpDir, { source: rulesSource }); + assert.equal(result.skipped, true); + assert.ok(fs.existsSync(path.join(existingDir, 'custom.md'))); + }); + + it('returns list of copied directories', () => { + const result = scaffold(tmpDir, { source: rulesSource }); + assert.equal(result.skipped, false); + assert.ok(result.dirs.length > 0); + assert.ok(result.dirs.includes('rules')); + assert.ok(result.dirs.includes('agents')); + }); +}); diff --git a/packages/rules/__tests__/init/suggest-agent.test.js b/packages/rules/__tests__/init/suggest-agent.test.js new file mode 100644 index 00000000..7cc8dd38 --- /dev/null +++ b/packages/rules/__tests__/init/suggest-agent.test.js @@ -0,0 +1,56 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { suggestAgent } = require('../../lib/init/suggest-agent'); + +describe('suggestAgent', () => { + it('suggests frontend-developer for React projects', () => { + const result = suggestAgent({ runtime: 'node', category: 'frontend', frameworks: ['react'] }); + assert.equal(result, 'frontend-developer'); + }); + + it('suggests frontend-developer for Vue projects', () => { + const result = suggestAgent({ runtime: 'node', category: 'frontend', frameworks: ['vue'] }); + assert.equal(result, 'frontend-developer'); + }); + + it('suggests software-engineer for Next.js fullstack', () => { + const result = suggestAgent({ runtime: 'node', category: 'fullstack', frameworks: ['next', 'react'] }); + assert.equal(result, 'software-engineer'); + }); + + it('suggests backend-developer for NestJS', () => { + const result = suggestAgent({ runtime: 'node', category: 'backend', frameworks: ['nestjs'] }); + assert.equal(result, 'backend-developer'); + }); + + it('suggests backend-developer for Python', () => { + const result = suggestAgent({ runtime: 'python', category: 'backend', frameworks: ['django'] }); + assert.equal(result, 'backend-developer'); + }); + + it('suggests backend-developer for Go', () => { + const result = suggestAgent({ runtime: 'go', category: 'backend', frameworks: [] }); + assert.equal(result, 'backend-developer'); + }); + + it('suggests systems-developer for Rust', () => { + const result = suggestAgent({ runtime: 'rust', category: 'backend', frameworks: [] }); + assert.equal(result, 'systems-developer'); + }); + + it('suggests mobile-developer for React Native', () => { + const result = suggestAgent({ runtime: 'node', category: 'mobile', frameworks: ['react-native'] }); + assert.equal(result, 'mobile-developer'); + }); + + it('suggests software-engineer for unknown stack', () => { + const result = suggestAgent({ runtime: 'unknown', category: 'unknown', frameworks: [] }); + assert.equal(result, 'software-engineer'); + }); + + it('suggests data-scientist for Python with jupyter/pandas', () => { + const result = suggestAgent({ runtime: 'python', category: 'backend', frameworks: ['pandas'] }); + assert.equal(result, 'data-scientist'); + }); +}); diff --git a/packages/rules/bin/cli.js b/packages/rules/bin/cli.js index 69ff29fc..06f5e201 100755 --- a/packages/rules/bin/cli.js +++ b/packages/rules/bin/cli.js @@ -139,11 +139,11 @@ function validate() { } function init() { - console.log( - 'codingbuddy init is not yet implemented.\n' + - 'See https://github.com/JeremyDev87/codingbuddy/issues/813 for progress.', - ); - process.exit(0); + const { run } = require('../lib/init'); + run().catch((err) => { + console.error('Error:', err.message); + process.exit(1); + }); } // --- Main --- diff --git a/packages/rules/lib/init/detect-stack.js b/packages/rules/lib/init/detect-stack.js new file mode 100644 index 00000000..f6c3f2fa --- /dev/null +++ b/packages/rules/lib/init/detect-stack.js @@ -0,0 +1,148 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const FRONTEND_FRAMEWORKS = ['react', 'vue', 'angular', 'svelte', 'solid-js']; +const FULLSTACK_FRAMEWORKS = ['next', 'nuxt', 'remix', 'sveltekit', 'astro']; +const BACKEND_FRAMEWORKS = [ + '@nestjs/core', 'express', 'fastify', 'koa', 'hapi', + '@hono/node-server', 'hono', +]; +const MOBILE_FRAMEWORKS = ['react-native', 'expo', '@capacitor/core', 'ionic']; + +/** + * Detect tech stack from project files in the given directory. + * @param {string} cwd - Directory to scan + * @returns {{ runtime: string, language: string, frameworks: string[], category: string }} + */ +function detectStack(cwd) { + const result = { runtime: 'unknown', language: 'javascript', frameworks: [], category: 'unknown' }; + + if (tryDetectNode(cwd, result)) return result; + if (tryDetectPython(cwd, result)) return result; + if (tryDetectGo(cwd, result)) return result; + if (tryDetectRust(cwd, result)) return result; + + result.language = 'unknown'; + return result; +} + +function tryDetectNode(cwd, result) { + const pkgPath = path.join(cwd, 'package.json'); + if (!fs.existsSync(pkgPath)) return false; + + let pkg; + try { + pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + } catch { + return false; + } + + result.runtime = 'node'; + + const allDeps = { + ...pkg.dependencies, + ...pkg.devDependencies, + }; + + // Detect language + if (allDeps.typescript) { + result.language = 'typescript'; + } + + // Detect frameworks + const detected = []; + + for (const fw of FULLSTACK_FRAMEWORKS) { + if (allDeps[fw]) detected.push(fw); + } + for (const fw of FRONTEND_FRAMEWORKS) { + if (allDeps[fw]) detected.push(fw); + } + for (const fw of BACKEND_FRAMEWORKS) { + if (allDeps[fw]) { + const name = fw.startsWith('@nestjs') ? 'nestjs' : fw; + detected.push(name); + } + } + for (const fw of MOBILE_FRAMEWORKS) { + if (allDeps[fw]) detected.push(fw); + } + + result.frameworks = detected; + + // Determine category + if (detected.some(f => MOBILE_FRAMEWORKS.includes(f) || f === 'react-native' || f === 'expo')) { + result.category = 'mobile'; + } else if (detected.some(f => FULLSTACK_FRAMEWORKS.includes(f))) { + result.category = 'fullstack'; + } else if (detected.some(f => FRONTEND_FRAMEWORKS.includes(f))) { + result.category = 'frontend'; + } else if (detected.some(f => ['nestjs', ...BACKEND_FRAMEWORKS.map(b => b.startsWith('@') ? 'nestjs' : b)].includes(f))) { + result.category = 'backend'; + } else { + result.category = 'backend'; // default for Node without frameworks + } + + return true; +} + +function tryDetectPython(cwd, result) { + const pyprojectPath = path.join(cwd, 'pyproject.toml'); + if (!fs.existsSync(pyprojectPath)) return false; + + result.runtime = 'python'; + result.language = 'python'; + result.category = 'backend'; + + const content = fs.readFileSync(pyprojectPath, 'utf-8'); + const pyFrameworks = { + django: 'django', + flask: 'flask', + fastapi: 'fastapi', + starlette: 'starlette', + }; + + for (const [key, name] of Object.entries(pyFrameworks)) { + if (content.toLowerCase().includes(key)) { + result.frameworks.push(name); + } + } + + return true; +} + +function tryDetectGo(cwd, result) { + const goModPath = path.join(cwd, 'go.mod'); + if (!fs.existsSync(goModPath)) return false; + + result.runtime = 'go'; + result.language = 'go'; + result.category = 'backend'; + + const content = fs.readFileSync(goModPath, 'utf-8'); + if (content.includes('github.com/gin-gonic/gin')) result.frameworks.push('gin'); + if (content.includes('github.com/gofiber/fiber')) result.frameworks.push('fiber'); + if (content.includes('github.com/labstack/echo')) result.frameworks.push('echo'); + + return true; +} + +function tryDetectRust(cwd, result) { + const cargoPath = path.join(cwd, 'Cargo.toml'); + if (!fs.existsSync(cargoPath)) return false; + + result.runtime = 'rust'; + result.language = 'rust'; + result.category = 'backend'; + + const content = fs.readFileSync(cargoPath, 'utf-8'); + if (content.includes('actix-web')) result.frameworks.push('actix-web'); + if (content.includes('axum')) result.frameworks.push('axum'); + if (content.includes('rocket')) result.frameworks.push('rocket'); + + return true; +} + +module.exports = { detectStack }; diff --git a/packages/rules/lib/init/generate-config.js b/packages/rules/lib/init/generate-config.js new file mode 100644 index 00000000..5aecdb50 --- /dev/null +++ b/packages/rules/lib/init/generate-config.js @@ -0,0 +1,31 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +/** + * Generate codingbuddy.config.json in the target directory. + * @param {string} cwd - Target directory + * @param {{ language: string, primaryAgent: string, techStack: object }} options + * @returns {{ created: boolean, skipped: boolean, path: string }} + */ +function generateConfig(cwd, options) { + const configPath = path.join(cwd, 'codingbuddy.config.json'); + + if (fs.existsSync(configPath)) { + return { created: false, skipped: true, path: configPath }; + } + + const config = { + version: '1.0.0', + language: options.language || 'en', + primaryAgent: options.primaryAgent || 'software-engineer', + techStack: options.techStack || {}, + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8'); + + return { created: true, skipped: false, path: configPath }; +} + +module.exports = { generateConfig }; diff --git a/packages/rules/lib/init/index.js b/packages/rules/lib/init/index.js new file mode 100644 index 00000000..cdd07d0d --- /dev/null +++ b/packages/rules/lib/init/index.js @@ -0,0 +1,86 @@ +'use strict'; + +const path = require('node:path'); +const { detectStack } = require('./detect-stack'); +const { suggestAgent } = require('./suggest-agent'); +const { generateConfig } = require('./generate-config'); +const { scaffold } = require('./scaffold'); +const { ask, select, confirm } = require('./prompt'); + +const LANGUAGES = ['en', 'ko', 'ja', 'zh', 'es']; + +/** + * Run the codingbuddy init wizard. + * @param {string} [cwd=process.cwd()] + */ +async function run(cwd) { + const targetDir = cwd || process.cwd(); + + console.log('\n codingbuddy init\n'); + console.log(' Initializing codingbuddy for your project...\n'); + + // Step 1: Detect tech stack + console.log(' Detecting tech stack...'); + const stack = detectStack(targetDir); + + if (stack.runtime !== 'unknown') { + console.log(` Detected: ${stack.runtime} (${stack.language})`); + if (stack.frameworks.length > 0) { + console.log(` Frameworks: ${stack.frameworks.join(', ')}`); + } + console.log(` Category: ${stack.category}`); + } else { + console.log(' No recognized project files found.'); + } + + // Step 2: Language selection + const language = await select('Select communication language:', LANGUAGES, 0); + console.log(` Language: ${language}`); + + // Step 3: Agent suggestion + const suggested = suggestAgent(stack); + console.log(`\n Recommended primary agent: ${suggested}`); + const useAgent = await confirm(` Use ${suggested} as primary agent?`, true); + + let primaryAgent = suggested; + if (!useAgent) { + primaryAgent = await ask(' Enter agent name', 'software-engineer'); + } + + // Step 4: Generate config + console.log('\n Creating codingbuddy.config.json...'); + const configResult = generateConfig(targetDir, { + language, + primaryAgent, + techStack: stack, + }); + + if (configResult.skipped) { + console.log(' codingbuddy.config.json already exists, skipped.'); + } else { + console.log(' Created codingbuddy.config.json'); + } + + // Step 5: Scaffold .ai-rules + const doScaffold = await confirm(' Scaffold .ai-rules/ directory with default rules?', true); + + if (doScaffold) { + console.log(' Scaffolding .ai-rules/...'); + const scaffoldResult = scaffold(targetDir); + + if (scaffoldResult.skipped) { + console.log(' .ai-rules/ already exists, skipped.'); + } else { + console.log(` Created .ai-rules/ with: ${scaffoldResult.dirs.join(', ')}`); + } + } + + // Done + console.log('\n Done! codingbuddy is ready.\n'); + console.log(' Next steps:'); + console.log(' 1. Review codingbuddy.config.json'); + console.log(' 2. Customize .ai-rules/ for your project'); + console.log(' 3. Start coding with your AI assistant\n'); +} + +module.exports = { run }; diff --git a/packages/rules/lib/init/prompt.js b/packages/rules/lib/init/prompt.js new file mode 100644 index 00000000..08bd5578 --- /dev/null +++ b/packages/rules/lib/init/prompt.js @@ -0,0 +1,60 @@ +'use strict'; + +const readline = require('node:readline'); + +/** + * Ask a question via stdin and return the answer. + * @param {string} question + * @param {string} [defaultValue] + * @returns {Promise} + */ +function ask(question, defaultValue) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const suffix = defaultValue ? ` (${defaultValue})` : ''; + + return new Promise((resolve) => { + rl.question(`${question}${suffix}: `, (answer) => { + rl.close(); + resolve(answer.trim() || defaultValue || ''); + }); + }); +} + +/** + * Ask user to select from a list of options. + * @param {string} question + * @param {string[]} options + * @param {number} [defaultIndex=0] + * @returns {Promise} + */ +async function select(question, options, defaultIndex = 0) { + console.log(`\n${question}`); + options.forEach((opt, i) => { + const marker = i === defaultIndex ? '>' : ' '; + console.log(` ${marker} ${i + 1}. ${opt}`); + }); + + const answer = await ask('Select', String(defaultIndex + 1)); + const index = parseInt(answer, 10) - 1; + + if (index >= 0 && index < options.length) { + return options[index]; + } + return options[defaultIndex]; +} + +/** + * Ask yes/no question. + * @param {string} question + * @param {boolean} [defaultValue=true] + * @returns {Promise} + */ +async function confirm(question, defaultValue = true) { + const hint = defaultValue ? 'Y/n' : 'y/N'; + const answer = await ask(`${question} (${hint})`, ''); + + if (!answer) return defaultValue; + return answer.toLowerCase().startsWith('y'); +} + +module.exports = { ask, select, confirm }; diff --git a/packages/rules/lib/init/scaffold.js b/packages/rules/lib/init/scaffold.js new file mode 100644 index 00000000..1b939449 --- /dev/null +++ b/packages/rules/lib/init/scaffold.js @@ -0,0 +1,67 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const DEFAULT_SOURCE = path.resolve(__dirname, '../../.ai-rules'); + +// Directories to always scaffold +const SCAFFOLD_DIRS = ['rules', 'agents']; +// Top-level files to always copy +const SCAFFOLD_FILES = ['README.md']; + +/** + * Scaffold .ai-rules/ structure in the target directory. + * @param {string} cwd - Target directory + * @param {{ source?: string }} options + * @returns {{ skipped: boolean, dirs: string[], targetPath: string }} + */ +function scaffold(cwd, options = {}) { + const source = options.source || DEFAULT_SOURCE; + const targetDir = path.join(cwd, '.ai-rules'); + + if (fs.existsSync(targetDir)) { + return { skipped: true, dirs: [], targetPath: targetDir }; + } + + fs.mkdirSync(targetDir, { recursive: true }); + + const copiedDirs = []; + + // Copy directories + for (const dir of SCAFFOLD_DIRS) { + const srcDir = path.join(source, dir); + if (fs.existsSync(srcDir)) { + copyDirRecursive(srcDir, path.join(targetDir, dir)); + copiedDirs.push(dir); + } + } + + // Copy top-level files + for (const file of SCAFFOLD_FILES) { + const srcFile = path.join(source, file); + if (fs.existsSync(srcFile)) { + fs.copyFileSync(srcFile, path.join(targetDir, file)); + } + } + + return { skipped: false, dirs: copiedDirs, targetPath: targetDir }; +} + +function copyDirRecursive(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyDirRecursive(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +module.exports = { scaffold }; diff --git a/packages/rules/lib/init/suggest-agent.js b/packages/rules/lib/init/suggest-agent.js new file mode 100644 index 00000000..9716457e --- /dev/null +++ b/packages/rules/lib/init/suggest-agent.js @@ -0,0 +1,46 @@ +'use strict'; + +const AGENT_MAP = { + mobile: 'mobile-developer', + frontend: 'frontend-developer', + fullstack: 'software-engineer', +}; + +const RUNTIME_AGENTS = { + rust: 'systems-developer', + go: 'backend-developer', + python: 'backend-developer', +}; + +const DATA_SCIENCE_FRAMEWORKS = ['pandas', 'numpy', 'scipy', 'jupyter', 'tensorflow', 'pytorch', 'scikit-learn']; + +/** + * Suggest the best primary agent based on detected tech stack. + * @param {{ runtime: string, category: string, frameworks: string[] }} stack + * @returns {string} Agent name (e.g. 'frontend-developer') + */ +function suggestAgent(stack) { + // Data science detection for Python + if (stack.runtime === 'python' && stack.frameworks.some(f => DATA_SCIENCE_FRAMEWORKS.includes(f))) { + return 'data-scientist'; + } + + // Category-based mapping + if (AGENT_MAP[stack.category]) { + return AGENT_MAP[stack.category]; + } + + // Runtime-based mapping + if (RUNTIME_AGENTS[stack.runtime]) { + return RUNTIME_AGENTS[stack.runtime]; + } + + // Backend Node.js + if (stack.runtime === 'node' && stack.category === 'backend') { + return 'backend-developer'; + } + + return 'software-engineer'; +} + +module.exports = { suggestAgent }; diff --git a/packages/rules/package.json b/packages/rules/package.json index c55c03db..947233a9 100644 --- a/packages/rules/package.json +++ b/packages/rules/package.json @@ -7,11 +7,15 @@ "bin": { "codingbuddy": "./bin/cli.js" }, + "scripts": { + "test": "node --test __tests__/**/*.test.js" + }, "files": [ "index.js", "index.d.ts", ".ai-rules", - "bin" + "bin", + "lib" ], "keywords": [ "ai",