diff --git a/README.md b/README.md index cc0f2433..8e8ea1a2 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,18 @@ AI-DLC is an intelligent software development workflow that adapts to your needs ## Quick Start +### Option A: Interactive Installer (Recommended) + +If you have Node.js 18+ installed, run the following in your project directory: + +```bash +npx aidlc-install +``` + +This will auto-detect your coding agents and interactively install the rules to the correct paths. See [CLI Installer](cli/README.md) for details and CI usage. + +### Option B: Manual Setup + 1. Download the latest release zip from the [Releases page](../../releases/latest) to a folder **outside** your project directory (e.g., `~/Downloads`). 2. Extract the zip. It contains an `aidlc-rules/` folder with two subdirectories: - `aws-aidlc-rules/` — the core AI-DLC workflow rules diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 00000000..caacbdd7 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.tgz +aidlc-rules/ diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..f8342093 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,67 @@ +# aidlc-install + +Interactive CLI installer for [AI-DLC](https://github.com/awslabs/aidlc-workflows) rules across coding agents. + +## Usage + +```bash +npx aidlc-install +``` + +Interactively select which agents to install AI-DLC rules for: + +``` +┌ AI-DLC Rules Installer +│ +◇ Agent Detection +│ Detected: Kiro, Claude Code +│ +◆ Select agents to install rules for: +│ ● Kiro .kiro/steering/ (detected) +│ ○ Amazon Q Developer .amazonq/rules/ +│ ○ Cursor IDE .cursor/rules/ai-dlc-workflow.mdc +│ ○ Cline .clinerules/ +│ ● Claude Code CLAUDE.md (detected) +│ ○ GitHub Copilot .github/copilot-instructions.md +│ +◆ Installation mode: +│ ● Symlink (recommended) +│ ○ Copy +│ +└ Done! Restart your agent to load the new rules. +``` + +## Supported Agents + +| Agent | Install Path | +|-------|-------------| +| Kiro | `.kiro/steering/aws-aidlc-rules` | +| Amazon Q Developer | `.amazonq/rules/aws-aidlc-rules` | +| Cursor IDE | `.cursor/rules/ai-dlc-workflow.mdc` | +| Cline | `.clinerules/core-workflow.md` | +| Claude Code | `CLAUDE.md` | +| GitHub Copilot | `.github/copilot-instructions.md` | + +## Commands + +```bash +npx aidlc-install # Install rules (interactive) +npx aidlc-install remove # Uninstall rules +npx aidlc-install list # Show installed rules +``` + +## CI / Non-Interactive + +```bash +npx aidlc-install --agent kiro --agent claude --copy -y +``` + +| Flag | Description | +|------|-------------| +| `-a, --agent ` | Target specific agents (repeatable) | +| `-y, --yes` | Skip prompts | +| `--copy` | Copy files instead of symlink | + +## License + +MIT-0 diff --git a/cli/package-lock.json b/cli/package-lock.json new file mode 100644 index 00000000..de73b229 --- /dev/null +++ b/cli/package-lock.json @@ -0,0 +1,55 @@ +{ + "name": "@inariku/aidlc-install", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@inariku/aidlc-install", + "version": "0.1.0", + "license": "MIT-0", + "dependencies": { + "@clack/prompts": "^0.11.0" + }, + "bin": { + "aidlc-install": "src/index.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", + "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", + "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", + "license": "MIT", + "dependencies": { + "@clack/core": "0.5.0", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + } + } +} diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 00000000..587f3d37 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,25 @@ +{ + "name": "@inariku/aidlc-install", + "version": "0.1.1", + "description": "Interactive installer for AI-DLC rules across coding agents", + "license": "MIT-0", + "bin": { + "aidlc-install": "src/index.mjs" + }, + "type": "module", + "files": [ + "src/", + "aidlc-rules/", + "README.md" + ], + "scripts": { + "prepack": "node -e \"require('fs').cpSync('../aidlc-rules','aidlc-rules',{recursive:true})\"", + "postpack": "node -e \"require('fs').rmSync('aidlc-rules',{recursive:true,force:true})\"" + }, + "dependencies": { + "@clack/prompts": "^0.11.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/cli/src/index.mjs b/cli/src/index.mjs new file mode 100755 index 00000000..3cd6d9ff --- /dev/null +++ b/cli/src/index.mjs @@ -0,0 +1,486 @@ +#!/usr/bin/env node + +import { intro, outro, multiselect, select, confirm, note, isCancel } from "@clack/prompts"; +import { + cpSync, mkdirSync, existsSync, readFileSync, writeFileSync, + rmSync, symlinkSync, copyFileSync, +} from "node:fs"; +import { resolve, dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; +import { parseArgs } from "node:util"; + +// --- Paths --- + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkgRules = resolve(__dirname, "..", "aidlc-rules"); +const devRules = resolve(__dirname, "..", "..", "aidlc-rules"); +const rulesSource = existsSync(pkgRules) ? pkgRules : devRules; +const coreWorkflow = join(rulesSource, "aws-aidlc-rules", "core-workflow.md"); +const rulesDir = join(rulesSource, "aws-aidlc-rules"); +const detailsDir = join(rulesSource, "aws-aidlc-rule-details"); +const configDir = join(homedir(), ".aidlc"); +const configPath = join(configDir, "config.json"); +const lockPath = join(configDir, "lock.json"); + +// --- Agent definitions (matching upstream README exactly) --- + +const CURSOR_FRONTMATTER = `--- +description: "AI-DLC (AI-Driven Development Life Cycle) adaptive workflow for software development" +alwaysApply: true +--- + +`; + +const AGENTS = [ + { + value: "kiro", + label: "Kiro", + hint: ".kiro/steering/", + detect: [".kiro"], + // cp -R aws-aidlc-rules .kiro/steering/ + cp -R aws-aidlc-rule-details .kiro/ + install(cwd, mode) { + const r = join(cwd, ".kiro", "steering", "aws-aidlc-rules"); + const d = join(cwd, ".kiro", "aws-aidlc-rule-details"); + return [ + { src: rulesDir, dest: r, type: "dir" }, + { src: detailsDir, dest: d, type: "dir" }, + ]; + }, + paths: [".kiro/steering/aws-aidlc-rules", ".kiro/aws-aidlc-rule-details"], + }, + { + value: "amazonq", + label: "Amazon Q Developer", + hint: ".amazonq/rules/", + detect: [".amazonq"], + // cp -R aws-aidlc-rules .amazonq/rules/ + cp -R aws-aidlc-rule-details .amazonq/ + install(cwd) { + const r = join(cwd, ".amazonq", "rules", "aws-aidlc-rules"); + const d = join(cwd, ".amazonq", "aws-aidlc-rule-details"); + return [ + { src: rulesDir, dest: r, type: "dir" }, + { src: detailsDir, dest: d, type: "dir" }, + ]; + }, + paths: [".amazonq/rules/aws-aidlc-rules", ".amazonq/aws-aidlc-rule-details"], + }, + { + value: "cursor", + label: "Cursor IDE", + hint: ".cursor/rules/ai-dlc-workflow.mdc", + detect: [".cursor", ".cursorrules"], + // Generate .mdc with frontmatter + core-workflow.md content + // cp -R aws-aidlc-rule-details/* .aidlc-rule-details/ + install(cwd) { + const mdc = join(cwd, ".cursor", "rules", "ai-dlc-workflow.mdc"); + const d = join(cwd, ".aidlc-rule-details"); + return [ + { dest: mdc, type: "generate-mdc" }, + { src: detailsDir, dest: d, type: "dir-contents" }, + ]; + }, + paths: [".cursor/rules/ai-dlc-workflow.mdc", ".aidlc-rule-details/"], + }, + { + value: "cline", + label: "Cline", + hint: ".clinerules/", + detect: [".clinerules"], + // cp core-workflow.md .clinerules/ + cp -R aws-aidlc-rule-details/* .aidlc-rule-details/ + install(cwd) { + const r = join(cwd, ".clinerules", "core-workflow.md"); + const d = join(cwd, ".aidlc-rule-details"); + return [ + { src: coreWorkflow, dest: r, type: "file" }, + { src: detailsDir, dest: d, type: "dir-contents" }, + ]; + }, + paths: [".clinerules/core-workflow.md", ".aidlc-rule-details/"], + }, + { + value: "claude", + label: "Claude Code", + hint: "CLAUDE.md", + detect: [".claude", "CLAUDE.md"], + // cp core-workflow.md ./CLAUDE.md + cp -R aws-aidlc-rule-details/* .aidlc-rule-details/ + install(cwd) { + const r = join(cwd, "CLAUDE.md"); + const d = join(cwd, ".aidlc-rule-details"); + return [ + { src: coreWorkflow, dest: r, type: "file" }, + { src: detailsDir, dest: d, type: "dir-contents" }, + ]; + }, + paths: ["CLAUDE.md", ".aidlc-rule-details/"], + }, + { + value: "copilot", + label: "GitHub Copilot", + hint: ".github/copilot-instructions.md", + detect: [".github/copilot-instructions.md"], + // cp core-workflow.md .github/copilot-instructions.md + cp -R aws-aidlc-rule-details/* .aidlc-rule-details/ + install(cwd) { + const r = join(cwd, ".github", "copilot-instructions.md"); + const d = join(cwd, ".aidlc-rule-details"); + return [ + { src: coreWorkflow, dest: r, type: "file" }, + { src: detailsDir, dest: d, type: "dir-contents" }, + ]; + }, + paths: [".github/copilot-instructions.md", ".aidlc-rule-details/"], + }, +]; + +// --- CLI args --- + +const { values: flags, positionals } = parseArgs({ + options: { + yes: { type: "boolean", short: "y", default: false }, + agent: { type: "string", short: "a", multiple: true, default: [] }, + copy: { type: "boolean", default: false }, + help: { type: "boolean", short: "h", default: false }, + version: { type: "boolean", short: "v", default: false }, + }, + allowPositionals: true, + strict: false, +}); + +const command = positionals[0] ?? "add"; + +// --- Helpers --- + +function cancel() { + outro("Cancelled."); + process.exit(0); +} + +function copyDir(src, dest) { + mkdirSync(dirname(dest), { recursive: true }); + cpSync(src, dest, { recursive: true }); +} + +function copyDirContents(src, dest) { + mkdirSync(dest, { recursive: true }); + cpSync(src, dest, { recursive: true }); +} + +function trySymlink(src, dest) { + try { + mkdirSync(dirname(dest), { recursive: true }); + if (existsSync(dest)) rmSync(dest, { recursive: true }); + symlinkSync(src, dest, "dir"); + return true; + } catch { + return false; + } +} + +function executeOp(op, mode) { + if (op.type === "dir") { + if (mode === "symlink" && trySymlink(op.src, op.dest)) return "symlink"; + copyDir(op.src, op.dest); + return "copy"; + } + if (op.type === "dir-contents") { + if (mode === "symlink" && trySymlink(op.src, op.dest)) return "symlink"; + copyDirContents(op.src, op.dest); + return "copy"; + } + if (op.type === "file") { + mkdirSync(dirname(op.dest), { recursive: true }); + copyFileSync(op.src, op.dest); + return "copy"; + } + if (op.type === "generate-mdc") { + mkdirSync(dirname(op.dest), { recursive: true }); + const content = CURSOR_FRONTMATTER + readFileSync(coreWorkflow, "utf-8"); + writeFileSync(op.dest, content); + return "generated"; + } +} + +function detectAgents(cwd) { + return AGENTS.filter((a) => + a.detect.some((p) => existsSync(join(cwd, p))) + ).map((a) => a.value); +} + +// --- Config --- + +function loadConfig() { + try { return JSON.parse(readFileSync(configPath, "utf-8")); } + catch { return {}; } +} + +function saveConfig(data) { + mkdirSync(configDir, { recursive: true }); + writeFileSync(configPath, JSON.stringify({ ...loadConfig(), ...data }, null, 2)); +} + +// --- Lock file --- + +function loadLock() { + try { return JSON.parse(readFileSync(lockPath, "utf-8")); } + catch { return { installations: [] }; } +} + +function saveLock(lock) { + mkdirSync(configDir, { recursive: true }); + writeFileSync(lockPath, JSON.stringify(lock, null, 2)); +} + +function addToLock(entries) { + const lock = loadLock(); + for (const entry of entries) { + lock.installations = lock.installations.filter( + (e) => !(e.agent === entry.agent && e.cwd === entry.cwd) + ); + lock.installations.push(entry); + } + saveLock(lock); +} + +function removeFromLock(agent, cwd) { + const lock = loadLock(); + lock.installations = lock.installations.filter( + (e) => !(e.agent === agent && e.cwd === cwd) + ); + saveLock(lock); +} + +// --- Commands --- + +async function runAdd() { + intro("AI-DLC Rules Installer"); + + if (!existsSync(coreWorkflow)) { + outro("Error: aidlc-rules not found."); + process.exit(1); + } + + const cwd = process.cwd(); + const nonInteractive = flags.yes; + + // Step 1: Agent selection + let selected; + if (flags.agent.length > 0) { + const valid = flags.agent.filter((a) => AGENTS.some((ag) => ag.value === a)); + const invalid = flags.agent.filter((a) => !AGENTS.some((ag) => ag.value === a)); + if (invalid.length > 0) { + outro(`Unknown agents: ${invalid.join(", ")}. Available: ${AGENTS.map((a) => a.value).join(", ")}`); + process.exit(1); + } + selected = valid; + } else { + const detected = detectAgents(cwd); + const lastSelected = loadConfig().lastSelected; + const initialSelected = lastSelected ?? (detected.length > 0 ? detected : []); + + if (detected.length > 0) { + note( + `Detected: ${detected.map((v) => AGENTS.find((a) => a.value === v).label).join(", ")}`, + "Agent Detection" + ); + } + + if (nonInteractive) { + selected = initialSelected.length > 0 ? initialSelected : AGENTS.map((a) => a.value); + } else { + const choice = await multiselect({ + message: "Select agents to install rules for:", + options: AGENTS.map((a) => ({ + value: a.value, + label: a.label, + hint: detected.includes(a.value) ? `${a.hint} (detected)` : a.hint, + })), + initialValues: initialSelected, + required: true, + }); + if (isCancel(choice)) cancel(); + selected = choice; + } + } + + // Step 2: Mode selection (symlink vs copy) + let mode; + if (flags.copy) { + mode = "copy"; + } else if (nonInteractive) { + mode = "symlink"; + } else { + const modeChoice = await select({ + message: "Installation mode:", + options: [ + { value: "symlink", label: "Symlink", hint: "Links to source, auto-updates (recommended)" }, + { value: "copy", label: "Copy", hint: "Independent copy of rule files" }, + ], + }); + if (isCancel(modeChoice)) cancel(); + mode = modeChoice; + } + + const agents = AGENTS.filter((a) => selected.includes(a.value)); + + // Step 3: Build installation plan + const plan = agents.map((a) => { + const ops = a.install(cwd); + const hasExisting = ops.some((op) => existsSync(op.dest)); + return { agent: a, ops, hasExisting }; + }); + + // Step 4: Summary + const summaryLines = plan.map((p) => { + const flag = p.hasExisting ? " (overwrite)" : ""; + return ` ${p.agent.label} → ${p.agent.paths.join(", ")}${flag}`; + }); + note(summaryLines.join("\n"), "Installation Summary"); + + // Step 5: Confirm + if (!nonInteractive) { + const hasOverwrite = plan.some((p) => p.hasExisting); + const ok = await confirm({ + message: hasOverwrite + ? "Some rules already exist and will be overwritten. Proceed?" + : "Install rules for the selected agents?", + }); + if (isCancel(ok) || !ok) cancel(); + } + + // Step 6: Install + const lockEntries = []; + for (const { agent, ops } of plan) { + const modes = ops.map((op) => executeOp(op, mode)); + const modeLabel = [...new Set(modes)].join("+"); + console.log(` ✓ ${agent.label} (${modeLabel})`); + + lockEntries.push({ + agent: agent.value, + cwd, + paths: ops.map((op) => op.dest), + mode: modeLabel, + installedAt: new Date().toISOString(), + }); + } + + // Step 7: Update lock + config + addToLock(lockEntries); + saveConfig({ lastSelected: selected }); + + // Step 8: Results + note( + plan.map((p) => ` ✓ ${p.agent.label} → ${p.agent.paths[0]}`).join("\n"), + "Installed" + ); + + outro("Done! Restart your agent to load the new rules."); +} + +async function runRemove() { + intro("AI-DLC Rules Uninstaller"); + + const cwd = process.cwd(); + const lock = loadLock(); + const relevant = lock.installations.filter((e) => e.cwd === cwd); + + if (relevant.length === 0) { + outro("No AI-DLC rules found in this project."); + process.exit(0); + } + + let toRemove; + if (flags.yes) { + toRemove = relevant; + } else if (flags.agent.length > 0) { + toRemove = relevant.filter((e) => flags.agent.includes(e.agent)); + } else { + const choice = await multiselect({ + message: "Select agents to remove rules from:", + options: relevant.map((e) => ({ + value: e.agent, + label: AGENTS.find((a) => a.value === e.agent)?.label ?? e.agent, + hint: e.paths[0], + })), + required: true, + }); + if (isCancel(choice)) cancel(); + toRemove = relevant.filter((e) => choice.includes(e.agent)); + } + + if (!flags.yes) { + const ok = await confirm({ + message: `Remove rules for ${toRemove.map((e) => AGENTS.find((a) => a.value === e.agent)?.label ?? e.agent).join(", ")}?`, + }); + if (isCancel(ok) || !ok) cancel(); + } + + for (const entry of toRemove) { + for (const p of entry.paths) { + if (existsSync(p)) rmSync(p, { recursive: true }); + } + removeFromLock(entry.agent, entry.cwd); + console.log(` ✓ Removed ${AGENTS.find((a) => a.value === entry.agent)?.label ?? entry.agent}`); + } + + outro("Done!"); +} + +function runList() { + const lock = loadLock(); + if (lock.installations.length === 0) { + console.log("No AI-DLC rules installed."); + return; + } + console.log("\nInstalled AI-DLC rules:\n"); + for (const entry of lock.installations) { + const label = AGENTS.find((a) => a.value === entry.agent)?.label ?? entry.agent; + const date = entry.installedAt ? ` (${entry.installedAt.split("T")[0]})` : ""; + console.log(` ${label} [${entry.mode}]${date}`); + console.log(` → ${entry.paths[0]}`); + } + console.log(); +} + +function showHelp() { + console.log(` +AI-DLC Rules Installer + +Usage: + npx @inariku/aidlc-install [command] [options] + +Commands: + add Install rules (default) + remove Uninstall rules + list Show installed rules + +Options: + -a, --agent Target specific agents (repeatable) + -y, --yes Skip prompts (CI-friendly) + --copy Copy files instead of symlink + -v, --version Show version + -h, --help Show this help + +Agents: ${AGENTS.map((a) => a.value).join(", ")} + +Examples: + npx @inariku/aidlc-install + npx @inariku/aidlc-install add --agent kiro --agent claude -y + npx @inariku/aidlc-install remove --agent cursor + npx @inariku/aidlc-install list +`); +} + +// --- Entry point --- + +if (flags.help) { + showHelp(); +} else if (flags.version) { + const pkg = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf-8")); + console.log(pkg.version); +} else if (command === "remove") { + runRemove(); +} else if (command === "list") { + runList(); +} else { + runAdd(); +}