Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
node_modules/
.codemap.db
.codemap.db-wal
.codemap.db-shm
.codemap.*
.DS_Store
dist/
*.tgz
Expand Down
85 changes: 83 additions & 2 deletions src/agents-init.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { describe, expect, it } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { resolveAgentsTemplateDir, runAgentsInit } from "./agents-init";
import {
ensureGitignoreCodemapPattern,
resolveAgentsTemplateDir,
runAgentsInit,
} from "./agents-init";

describe("runAgentsInit", () => {
it("copies templates into .agents/", () => {
Expand Down Expand Up @@ -38,4 +49,74 @@ describe("runAgentsInit", () => {
const p = resolveAgentsTemplateDir().replace(/\\/g, "/");
expect(p.endsWith("/templates/agents")).toBe(true);
});

it("ensureGitignoreCodemapPattern appends .codemap.* when .gitignore exists", () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
try {
mkdirSync(join(dir, ".git"), { recursive: true });
const gi = join(dir, ".gitignore");
writeFileSync(gi, "node_modules/\n", "utf-8");
ensureGitignoreCodemapPattern(dir);
expect(readFileSync(gi, "utf-8")).toContain(".codemap.*");
ensureGitignoreCodemapPattern(dir);
const lines = readFileSync(gi, "utf-8").split("\n").filter(Boolean);
expect(lines.filter((l) => l === ".codemap.*").length).toBe(1);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("ensureGitignoreCodemapPattern no-ops when not a Git repo", () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
try {
ensureGitignoreCodemapPattern(dir);
expect(existsSync(join(dir, ".gitignore"))).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("ensureGitignoreCodemapPattern creates .gitignore when Git repo has none", () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
try {
mkdirSync(join(dir, ".git"), { recursive: true });
ensureGitignoreCodemapPattern(dir);
expect(readFileSync(join(dir, ".gitignore"), "utf-8")).toBe(
".codemap.*\n",
);
ensureGitignoreCodemapPattern(dir);
expect(readFileSync(join(dir, ".gitignore"), "utf-8")).toBe(
".codemap.*\n",
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("runAgentsInit updates .gitignore when present", () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
try {
mkdirSync(join(dir, ".git"), { recursive: true });
writeFileSync(join(dir, ".gitignore"), "dist/\n", "utf-8");
expect(runAgentsInit({ projectRoot: dir, force: true })).toBe(true);
expect(readFileSync(join(dir, ".gitignore"), "utf-8")).toContain(
".codemap.*",
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("runAgentsInit creates .gitignore in Git repo without one", () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
try {
mkdirSync(join(dir, ".git"), { recursive: true });
expect(runAgentsInit({ projectRoot: dir, force: true })).toBe(true);
expect(readFileSync(join(dir, ".gitignore"), "utf-8")).toBe(
".codemap.*\n",
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
46 changes: 45 additions & 1 deletion src/agents-init.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { cpSync, existsSync, mkdirSync } from "node:fs";
import {
appendFileSync,
cpSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

Expand All @@ -14,13 +21,49 @@ export function resolveAgentsTemplateDir(): string {
);
}

/** Default DB basename `.codemap` plus SQLite sidecars (`.db`, `-wal`, `-shm`, …). */
const GITIGNORE_CODEMAP_PATTERN = ".codemap.*";

export interface AgentsInitOptions {
/** Project root (`.agents/` is created here). */
projectRoot: string;
/** Overwrite existing files. */
force?: boolean;
}

/**
* Ensure `.codemap.*` is listed in `.gitignore` when the project uses Git:
* - If `<projectRoot>/.git` exists and there is no `.gitignore`, create one with `.codemap.*`.
* - If `.gitignore` exists, append `.codemap.*` once when missing.
* - If there is no `.git`, do nothing (not a Git working tree).
*/
export function ensureGitignoreCodemapPattern(projectRoot: string): void {
const gitDir = join(projectRoot, ".git");
const gitignorePath = join(projectRoot, ".gitignore");
if (!existsSync(gitDir)) {
return;
}
if (!existsSync(gitignorePath)) {
writeFileSync(gitignorePath, `${GITIGNORE_CODEMAP_PATTERN}\n`, "utf-8");
console.log(
` Created .gitignore with ${GITIGNORE_CODEMAP_PATTERN} (Git repo, no .gitignore yet)`,
);
return;
}
const content = readFileSync(gitignorePath, "utf-8");
const lines = content.split(/\r?\n/);
if (lines.some((line) => line.trim() === GITIGNORE_CODEMAP_PATTERN)) {
return;
}
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
appendFileSync(
gitignorePath,
`${needsLeadingNewline ? "\n" : ""}${GITIGNORE_CODEMAP_PATTERN}\n`,
"utf-8",
);
console.log(` Appended ${GITIGNORE_CODEMAP_PATTERN} to .gitignore`);
}

/**
* Copy bundled rules and skills into `<projectRoot>/.agents/`.
* @returns `false` when `.agents/` exists and `--force` was not used.
Expand Down Expand Up @@ -50,6 +93,7 @@ export function runAgentsInit(options: AgentsInitOptions): boolean {
});

console.log(` Wrote agent templates to ${destRoot}`);
ensureGitignoreCodemapPattern(options.projectRoot);
console.log(
` Symlink into .cursor/ if needed — see https://github.com/stainless-code/codemap/blob/main/.github/CONTRIBUTING.md`,
);
Expand Down
Loading