agent-rules-init 0.1.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/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { type WriteResult } from "./core/writer.js";
3
+ import { type PromptFn } from "./core/prompt-engine.js";
4
+ import { type ExecFn } from "./core/llm-bridge.js";
5
+ export interface RunCliOptions {
6
+ promptFn?: PromptFn;
7
+ execFn?: ExecFn;
8
+ skipLlm?: boolean;
9
+ }
10
+ export declare function runCli(rootPath: string, options?: RunCliOptions): Promise<WriteResult[]>;
package/dist/cli.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import * as clack from "@clack/prompts";
3
+ import { pathToFileURL } from "node:url";
4
+ import { scanRepo } from "./core/scanner.js";
5
+ import { writeGeneratedFiles } from "./core/writer.js";
6
+ import { renderClaudeMd, renderAgentsMd, renderCopilotInstructions, renderPromptFiles, } from "./core/templates.js";
7
+ import { collectLowConfidenceQuestions, askQuestions, applyAnswers, defaultPromptFn, } from "./core/prompt-engine.js";
8
+ import { detectAvailableAssistants, polishWithAssistant, defaultExecFn } from "./core/llm-bridge.js";
9
+ import { jsTsPack } from "agent-rules-pack-js-ts";
10
+ import { pythonPack } from "agent-rules-pack-python";
11
+ import { javaPack } from "agent-rules-pack-java";
12
+ import { phpPack } from "agent-rules-pack-php";
13
+ const ALL_PACKS = [jsTsPack, pythonPack, javaPack, phpPack];
14
+ export async function runCli(rootPath, options = {}) {
15
+ const promptFn = options.promptFn ?? defaultPromptFn;
16
+ const execFn = options.execFn ?? defaultExecFn;
17
+ const signals = scanRepo(rootPath);
18
+ const rawDetections = ALL_PACKS.map((pack) => pack.detect(signals)).filter((d) => d !== null);
19
+ const questions = collectLowConfidenceQuestions(rawDetections);
20
+ const answers = await askQuestions(questions, promptFn);
21
+ const detections = applyAnswers(rawDetections, answers);
22
+ const entries = detections.map((detection) => {
23
+ const pack = ALL_PACKS.find((p) => p.id === detection.packId);
24
+ return { detection, ruleSet: pack.rules(detection) };
25
+ });
26
+ const files = [];
27
+ if (entries.length > 0) {
28
+ files.push({ path: "CLAUDE.generated.md", content: renderClaudeMd(entries) });
29
+ files.push({ path: "AGENTS.generated.md", content: renderAgentsMd(entries) });
30
+ files.push({
31
+ path: ".github/copilot-instructions.generated.md",
32
+ content: renderCopilotInstructions(entries),
33
+ });
34
+ for (const detection of detections) {
35
+ const pack = ALL_PACKS.find((p) => p.id === detection.packId);
36
+ for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection))) {
37
+ files.push(file);
38
+ }
39
+ }
40
+ }
41
+ else {
42
+ files.push({
43
+ path: "CLAUDE.generated.md",
44
+ content: "# CLAUDE.md\n\nNo se detectó ningún stack conocido. Completa este archivo manualmente.\n",
45
+ });
46
+ }
47
+ if (!options.skipLlm) {
48
+ const assistants = await detectAvailableAssistants(execFn);
49
+ if (assistants.length > 0) {
50
+ const chosenAssistant = assistants[0];
51
+ const usePolish = await clack.confirm({
52
+ message: `Se detectó ${chosenAssistant}. ¿Quieres que pula la redacción final?`,
53
+ });
54
+ if (usePolish === true) {
55
+ for (const file of files) {
56
+ file.content = await polishWithAssistant(chosenAssistant, file.content, execFn);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ return writeGeneratedFiles(rootPath, files);
62
+ }
63
+ async function main() {
64
+ const results = await runCli(process.cwd());
65
+ const failures = results.filter((r) => r.status === "error");
66
+ for (const result of results) {
67
+ if (result.status === "written") {
68
+ clack.log.success(result.path);
69
+ }
70
+ else {
71
+ clack.log.warn(`${result.path}: ${result.error}`);
72
+ }
73
+ }
74
+ if (failures.length > 0) {
75
+ process.exitCode = 1;
76
+ }
77
+ }
78
+ const isMainModule = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
79
+ if (isMainModule) {
80
+ main();
81
+ }
@@ -0,0 +1,9 @@
1
+ export type AssistantId = "claude" | "codex";
2
+ export interface ExecResult {
3
+ stdout: string;
4
+ exitCode: number;
5
+ }
6
+ export type ExecFn = (command: string, args: string[]) => Promise<ExecResult>;
7
+ export declare const defaultExecFn: ExecFn;
8
+ export declare function detectAvailableAssistants(execFn?: ExecFn): Promise<AssistantId[]>;
9
+ export declare function polishWithAssistant(assistant: AssistantId, content: string, execFn?: ExecFn): Promise<string>;
@@ -0,0 +1,45 @@
1
+ import { spawn } from "node:child_process";
2
+ const VERSION_ARGS = {
3
+ claude: ["--version"],
4
+ codex: ["--version"],
5
+ };
6
+ export const defaultExecFn = (command, args) => new Promise((resolve, reject) => {
7
+ // shell:true is only needed on Windows, to resolve npm-installed .cmd/.ps1 shims.
8
+ // On POSIX, spawn(command, args) execs the binary directly with argv, so passing
9
+ // large generated content as an argument (see polishWithAssistant) can never be
10
+ // reinterpreted by a shell there.
11
+ const child = spawn(command, args, { shell: process.platform === "win32" });
12
+ let stdout = "";
13
+ child.stdout?.on("data", (chunk) => (stdout += chunk.toString()));
14
+ child.on("error", reject);
15
+ child.on("close", (exitCode) => {
16
+ if (exitCode === 0)
17
+ resolve({ stdout, exitCode });
18
+ else
19
+ reject(new Error(`${command} exited with code ${exitCode}`));
20
+ });
21
+ });
22
+ export async function detectAvailableAssistants(execFn = defaultExecFn) {
23
+ const candidates = ["claude", "codex"];
24
+ const results = await Promise.all(candidates.map(async (id) => {
25
+ try {
26
+ await execFn(id, VERSION_ARGS[id]);
27
+ return id;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }));
33
+ return results.filter((id) => id !== null);
34
+ }
35
+ export async function polishWithAssistant(assistant, content, execFn = defaultExecFn) {
36
+ const prompt = `Pule la redacción del siguiente documento de instrucciones para un agente de IA, sin cambiar su significado ni estructura:\n\n${content}`;
37
+ try {
38
+ const result = await execFn(assistant, ["-p", prompt]);
39
+ return result.stdout.trim() || content;
40
+ }
41
+ catch (err) {
42
+ console.warn(`No se pudo pulir el contenido con ${assistant}, se mantiene el original: ${err.message}`);
43
+ return content;
44
+ }
45
+ }
@@ -0,0 +1,12 @@
1
+ import type { DetectionResult } from "./types.js";
2
+ export type QuestionField = "framework" | "testRunner" | "linter" | "packageManager";
3
+ export interface Question {
4
+ packId: string;
5
+ field: QuestionField;
6
+ message: string;
7
+ }
8
+ export declare function collectLowConfidenceQuestions(detections: DetectionResult[]): Question[];
9
+ export type PromptFn = (message: string) => Promise<string>;
10
+ export declare const defaultPromptFn: PromptFn;
11
+ export declare function askQuestions(questions: Question[], promptFn?: PromptFn): Promise<Record<string, string>>;
12
+ export declare function applyAnswers(detections: DetectionResult[], answers: Record<string, string>): DetectionResult[];
@@ -0,0 +1,45 @@
1
+ import * as clack from "@clack/prompts";
2
+ const FIELDS = ["framework", "testRunner", "linter", "packageManager"];
3
+ export function collectLowConfidenceQuestions(detections) {
4
+ const questions = [];
5
+ for (const detection of detections) {
6
+ for (const field of FIELDS) {
7
+ const detectionField = detection[field];
8
+ if (detectionField && detectionField.confidence === "low") {
9
+ questions.push({
10
+ packId: detection.packId,
11
+ field,
12
+ message: `No se pudo determinar el ${field} para ${detection.language}. ¿Cuál usáis?`,
13
+ });
14
+ }
15
+ }
16
+ }
17
+ return questions;
18
+ }
19
+ export const defaultPromptFn = async (message) => {
20
+ const answer = await clack.text({ message });
21
+ if (clack.isCancel(answer)) {
22
+ clack.cancel("Operación cancelada.");
23
+ process.exit(1);
24
+ }
25
+ return answer;
26
+ };
27
+ export async function askQuestions(questions, promptFn = defaultPromptFn) {
28
+ const answers = {};
29
+ for (const question of questions) {
30
+ answers[`${question.packId}:${question.field}`] = await promptFn(question.message);
31
+ }
32
+ return answers;
33
+ }
34
+ export function applyAnswers(detections, answers) {
35
+ return detections.map((detection) => {
36
+ const updated = { ...detection };
37
+ for (const field of FIELDS) {
38
+ const answer = answers[`${detection.packId}:${field}`];
39
+ if (answer) {
40
+ updated[field] = { value: answer, confidence: "high" };
41
+ }
42
+ }
43
+ return updated;
44
+ });
45
+ }
@@ -0,0 +1,2 @@
1
+ import type { RepoSignals } from "./types.js";
2
+ export declare function scanRepo(rootPath: string): RepoSignals;
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".venv", "__pycache__"]);
4
+ const MAX_DEPTH = 4;
5
+ function walk(rootPath) {
6
+ const results = [];
7
+ function recurse(dir, depth) {
8
+ if (depth > MAX_DEPTH)
9
+ return;
10
+ let entries;
11
+ try {
12
+ entries = fs.readdirSync(dir, { withFileTypes: true });
13
+ }
14
+ catch {
15
+ return;
16
+ }
17
+ for (const entry of entries) {
18
+ if (entry.isDirectory()) {
19
+ if (IGNORED_DIRS.has(entry.name))
20
+ continue;
21
+ recurse(path.join(dir, entry.name), depth + 1);
22
+ }
23
+ else {
24
+ results.push(path.relative(rootPath, path.join(dir, entry.name)));
25
+ }
26
+ }
27
+ }
28
+ recurse(rootPath, 0);
29
+ return results;
30
+ }
31
+ function readJsonIfExists(filePath) {
32
+ if (!fs.existsSync(filePath))
33
+ return undefined;
34
+ try {
35
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ function readTextIfExists(filePath) {
42
+ if (!fs.existsSync(filePath))
43
+ return undefined;
44
+ try {
45
+ return fs.readFileSync(filePath, "utf-8");
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ function findFirst(files, fileName) {
52
+ const matches = files.filter((f) => path.basename(f) === fileName);
53
+ if (matches.length === 0)
54
+ return undefined;
55
+ return matches.reduce((shallowest, candidate) => candidate.split(path.sep).length < shallowest.split(path.sep).length ? candidate : shallowest);
56
+ }
57
+ export function scanRepo(rootPath) {
58
+ const files = walk(rootPath);
59
+ const fileSet = new Set(files.map((f) => f.split(path.sep).join("/")));
60
+ const packageJsonPath = findFirst(files, "package.json");
61
+ const rawPackageJson = packageJsonPath
62
+ ? readJsonIfExists(path.join(rootPath, packageJsonPath))
63
+ : undefined;
64
+ const packageJson = rawPackageJson
65
+ ? {
66
+ name: rawPackageJson.name,
67
+ dependencies: rawPackageJson.dependencies ?? {},
68
+ devDependencies: rawPackageJson.devDependencies ?? {},
69
+ scripts: rawPackageJson.scripts ?? {},
70
+ }
71
+ : undefined;
72
+ const composerJsonPath = findFirst(files, "composer.json");
73
+ const rawComposerJson = composerJsonPath
74
+ ? readJsonIfExists(path.join(rootPath, composerJsonPath))
75
+ : undefined;
76
+ const composerJson = rawComposerJson
77
+ ? {
78
+ require: rawComposerJson.require ?? {},
79
+ requireDev: rawComposerJson["require-dev"] ?? {},
80
+ }
81
+ : undefined;
82
+ const pyprojectPath = findFirst(files, "pyproject.toml");
83
+ const requirementsPath = findFirst(files, "requirements.txt");
84
+ const pomPath = findFirst(files, "pom.xml");
85
+ const buildGradlePath = findFirst(files, "build.gradle") ?? findFirst(files, "build.gradle.kts");
86
+ return {
87
+ rootPath,
88
+ files,
89
+ hasFile: (relativePath) => fileSet.has(relativePath.split(path.sep).join("/")),
90
+ hasDir: (relativeDir) => fs.existsSync(path.join(rootPath, relativeDir)) &&
91
+ fs.statSync(path.join(rootPath, relativeDir)).isDirectory(),
92
+ packageJson,
93
+ pyprojectToml: pyprojectPath ? readTextIfExists(path.join(rootPath, pyprojectPath)) : undefined,
94
+ requirementsTxt: requirementsPath
95
+ ? readTextIfExists(path.join(rootPath, requirementsPath))
96
+ : undefined,
97
+ pomXml: pomPath ? readTextIfExists(path.join(rootPath, pomPath)) : undefined,
98
+ buildGradle: buildGradlePath ? readTextIfExists(path.join(rootPath, buildGradlePath)) : undefined,
99
+ composerJson,
100
+ };
101
+ }
@@ -0,0 +1,12 @@
1
+ import type { DetectionResult, PromptTemplate, RuleSet } from "./types.js";
2
+ export interface RenderEntry {
3
+ detection: DetectionResult;
4
+ ruleSet: RuleSet;
5
+ }
6
+ export declare function renderClaudeMd(entries: RenderEntry[]): string;
7
+ export declare function renderAgentsMd(entries: RenderEntry[]): string;
8
+ export declare function renderCopilotInstructions(entries: RenderEntry[]): string;
9
+ export declare function renderPromptFiles(packId: string, templates: PromptTemplate[]): {
10
+ path: string;
11
+ content: string;
12
+ }[];
@@ -0,0 +1,58 @@
1
+ function renderSection(entries) {
2
+ return entries
3
+ .map(({ detection, ruleSet }) => {
4
+ const conventions = ruleSet.conventions.map((c) => `- ${c}`).join("\n");
5
+ const architecture = ruleSet.architectureNotes.map((a) => `- ${a}`).join("\n");
6
+ return [
7
+ `## ${detection.language} (${detection.packId})`,
8
+ "",
9
+ ruleSet.summary,
10
+ "",
11
+ "### Convenciones",
12
+ conventions,
13
+ "",
14
+ "### Arquitectura",
15
+ architecture,
16
+ ].join("\n");
17
+ })
18
+ .join("\n\n");
19
+ }
20
+ export function renderClaudeMd(entries) {
21
+ return [
22
+ "# CLAUDE.md",
23
+ "",
24
+ "Generado por agent-rules-init a partir de lo detectado en este repo.",
25
+ "",
26
+ renderSection(entries),
27
+ ].join("\n");
28
+ }
29
+ export function renderAgentsMd(entries) {
30
+ return [
31
+ "# AGENTS.md",
32
+ "",
33
+ "Generado por agent-rules-init a partir de lo detectado en este repo.",
34
+ "",
35
+ renderSection(entries),
36
+ ].join("\n");
37
+ }
38
+ export function renderCopilotInstructions(entries) {
39
+ return [
40
+ "# Copilot Instructions",
41
+ "",
42
+ "Generado por agent-rules-init a partir de lo detectado en este repo.",
43
+ "",
44
+ renderSection(entries),
45
+ ].join("\n");
46
+ }
47
+ export function renderPromptFiles(packId, templates) {
48
+ return templates.flatMap((template) => [
49
+ {
50
+ path: `.claude/commands/${packId}-${template.id}.generated.md`,
51
+ content: `# ${template.title}\n\n${template.body}\n`,
52
+ },
53
+ {
54
+ path: `.github/prompts/${packId}-${template.id}.generated.prompt.md`,
55
+ content: `# ${template.title}\n\n${template.body}\n`,
56
+ },
57
+ ]);
58
+ }
@@ -0,0 +1 @@
1
+ export * from "agent-rules-pack-types";
@@ -0,0 +1 @@
1
+ export * from "agent-rules-pack-types";
@@ -0,0 +1,10 @@
1
+ export interface GeneratedFile {
2
+ path: string;
3
+ content: string;
4
+ }
5
+ export interface WriteResult {
6
+ path: string;
7
+ status: "written" | "error";
8
+ error?: string;
9
+ }
10
+ export declare function writeGeneratedFiles(rootPath: string, files: GeneratedFile[]): WriteResult[];
@@ -0,0 +1,18 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function writeGeneratedFiles(rootPath, files) {
4
+ return files.map(({ path: relativePath, content }) => {
5
+ const absolutePath = path.join(rootPath, relativePath);
6
+ try {
7
+ if (fs.existsSync(absolutePath)) {
8
+ return { path: relativePath, status: "error", error: "file already exists" };
9
+ }
10
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
11
+ fs.writeFileSync(absolutePath, content);
12
+ return { path: relativePath, status: "written" };
13
+ }
14
+ catch (err) {
15
+ return { path: relativePath, status: "error", error: err.message };
16
+ }
17
+ });
18
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "agent-rules-init",
3
+ "version": "0.1.0",
4
+ "description": "Generates CLAUDE.md, AGENTS.md, copilot-instructions.md and prompt files from what your repo actually is.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "agent-rules-init": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json",
15
+ "test": "vitest run",
16
+ "lint": "tsc -p tsconfig.json --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@clack/prompts": "^0.9.1",
20
+ "agent-rules-pack-java": "0.1.0",
21
+ "agent-rules-pack-js-ts": "0.1.0",
22
+ "agent-rules-pack-php": "0.1.0",
23
+ "agent-rules-pack-python": "0.1.0",
24
+ "agent-rules-pack-types": "0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.6.0",
28
+ "vitest": "^2.1.0"
29
+ }
30
+ }