From 2480d4a05655f7cbc62c2bcae01c289d7f7221e2 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 7 Apr 2026 12:03:23 +0300 Subject: [PATCH 1/4] feat(agents): ensure .codemap.* in .gitignore on agents init - Require a Git repo (.git): create .gitignore with .codemap.* when missing, or append the pattern when .gitignore exists - Consolidate repo .gitignore to .codemap.* - Tests for Git vs non-Git and create vs append --- .gitignore | 4 +- src/agents-init.test.ts | 85 ++++++++++++++++++++++++++++++++++++++++- src/agents-init.ts | 46 +++++++++++++++++++++- 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 2c61f17a..734a1dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ node_modules/ -.codemap.db -.codemap.db-wal -.codemap.db-shm +.codemap.* .DS_Store dist/ *.tgz diff --git a/src/agents-init.test.ts b/src/agents-init.test.ts index 5ae8f46e..ccb612cf 100644 --- a/src/agents-init.test.ts +++ b/src/agents-init.test.ts @@ -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/", () => { @@ -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 }); + } + }); }); diff --git a/src/agents-init.ts b/src/agents-init.ts index 308d43a9..0ce8cd19 100644 --- a/src/agents-init.ts +++ b/src/agents-init.ts @@ -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"; @@ -14,6 +21,9 @@ 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; @@ -21,6 +31,39 @@ export interface AgentsInitOptions { force?: boolean; } +/** + * Ensure `.codemap.*` is listed in `.gitignore` when the project uses Git: + * - If `/.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 `/.agents/`. * @returns `false` when `.agents/` exists and `--force` was not used. @@ -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`, ); From e1ec33d000a14fed71257fe25dfb883ab0a07a27 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 7 Apr 2026 12:10:04 +0300 Subject: [PATCH 2/4] chore: add patch changeset for agents init gitignore --- .changeset/agents-init-gitignore.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/agents-init-gitignore.md diff --git a/.changeset/agents-init-gitignore.md b/.changeset/agents-init-gitignore.md new file mode 100644 index 00000000..cebde0d8 --- /dev/null +++ b/.changeset/agents-init-gitignore.md @@ -0,0 +1,5 @@ +--- +"@stainless-code/codemap": patch +--- + +On `codemap agents init`, ensure `.codemap.*` is listed in `.gitignore` for Git projects (create `.gitignore` when missing, or append once). Repo `.gitignore` consolidated to `.codemap.*`. From 0b84cccdfe67b84d13d20ffe01f7fce345862d2f Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 7 Apr 2026 12:19:47 +0300 Subject: [PATCH 3/4] feat(agents): interactive init, multi-IDE wiring, docs/agents.md - @clack/prompts: codemap agents init -i; symlink/copy for rule mirrors - Integrations: Cursor, Copilot, Windsurf, Continue, Cline, Amazon Q, CLAUDE/AGENTS/GEMINI - Git .gitignore .codemap.*; expand changeset; PR template + doc hub - docs/agents.md; README/architecture/packaging/roadmap/templates links --- .changeset/agents-init-gitignore.md | 4 +- .github/CONTRIBUTING.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- README.md | 5 +- bun.lock | 13 ++ docs/README.md | 3 +- docs/agents.md | 59 ++++++ docs/architecture.md | 42 ++-- docs/packaging.md | 1 + docs/roadmap.md | 2 +- package.json | 1 + src/agents-init-interactive.ts | 164 ++++++++++++++++ src/agents-init.test.ts | 112 +++++++++++ src/agents-init.ts | 293 +++++++++++++++++++++++++++- src/cli/bootstrap.ts | 4 +- src/cli/cmd-agents.ts | 16 +- src/cli/main.ts | 23 ++- templates/agents/README.md | 4 +- 18 files changed, 710 insertions(+), 40 deletions(-) create mode 100644 docs/agents.md create mode 100644 src/agents-init-interactive.ts diff --git a/.changeset/agents-init-gitignore.md b/.changeset/agents-init-gitignore.md index cebde0d8..e4a2bcd2 100644 --- a/.changeset/agents-init-gitignore.md +++ b/.changeset/agents-init-gitignore.md @@ -2,4 +2,6 @@ "@stainless-code/codemap": patch --- -On `codemap agents init`, ensure `.codemap.*` is listed in `.gitignore` for Git projects (create `.gitignore` when missing, or append once). Repo `.gitignore` consolidated to `.codemap.*`. +**`codemap agents init`:** For Git repos, ensure **`.codemap.*`** is in **`.gitignore`** (create the file or append the line once). **`--interactive` / `-i`** — pick IDE integrations (Cursor, GitHub Copilot, Windsurf, Continue, Cline, Amazon Q, **`CLAUDE.md`**, **`AGENTS.md`**, **`GEMINI.md`**) and symlink vs copy for rule mirrors; requires a TTY. Depends on **`@clack/prompts`**. + +**Docs:** New hub section **[`docs/agents.md`](https://github.com/stainless-code/codemap/blob/main/docs/agents.md)**; **[`docs/README.md`](https://github.com/stainless-code/codemap/blob/main/docs/README.md)** index updated. Root **[`.gitignore`](https://github.com/stainless-code/codemap/blob/main/.gitignore)** uses a single **`.codemap.*`** line. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c069df46..8edc4bdf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -52,7 +52,7 @@ Releases: **[@changesets/cli](https://github.com/changesets/changesets)** — ru ## Agent rules and skills (`.agents/`) -**Upstream** skill and rules in this repo (e.g. `codemap`) stay **generic** — placeholder SQL and triggers, no product-specific paths. Consumer projects can run **`codemap agents init`** (ships **`templates/agents`** on npm) or **copy/symlink** manually, then **edit their copy** for team aliases and queries. Customization always belongs in the **consumer** repo. +**Upstream** skill and rules in this repo (e.g. `codemap`) stay **generic** — placeholder SQL and triggers, no product-specific paths. Consumer projects can run **`codemap agents init`** (ships **`templates/agents`** on npm; see [docs/agents.md](../docs/agents.md)) or **copy/symlink** manually, then **edit their copy** for team aliases and queries. Customization always belongs in the **consumer** repo. Rules live under **`.agents/rules/`**; skills under **`.agents/skills//SKILL.md`**. Symlink each into **`.cursor/`** (see [agents-first-convention.mdc](../.agents/rules/agents-first-convention.mdc)): diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1691add6..a81f1fc0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,7 +6,7 @@ - [ ] `bun run check` passes (format, lint, test, typecheck, build) - [ ] User-facing change? **Add a [Changeset](https://github.com/changesets/changesets)** (`bun run changeset`) and commit the `.changeset/*.md` file -- [ ] Docs updated if behavior or public API changed +- [ ] Docs updated if behavior or public API changed — hub: [`docs/README.md`](../docs/README.md); CLI/agents: [`docs/agents.md`](../docs/agents.md) ## Notes diff --git a/README.md b/README.md index de08dbdf..4d6d9ff5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - **Not** full-text search or grep on arbitrary strings — use those when you need raw file-body search. - **Is** a fast, token-efficient way to navigate **structure**: definitions, imports, dependency direction, components, and other extracted facts. -**Documentation:** [docs/README.md](docs/README.md) is the index for technical docs (architecture, packaging, roadmap, benchmarks). **AI / editor agents:** [`.agents/rules/`](.agents/rules/), [`.agents/skills/codemap/SKILL.md`](.agents/skills/codemap/SKILL.md); Cursor uses `.cursor/` symlinks — [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). +**Documentation:** [docs/README.md](docs/README.md) indexes technical docs (architecture, **[`docs/agents.md`](docs/agents.md)** for `codemap agents init`, packaging, roadmap, benchmarks). **Bundled rules/skills:** [`.agents/rules/`](.agents/rules/), [`.agents/skills/codemap/SKILL.md`](.agents/skills/codemap/SKILL.md). **Consumers:** [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). --- @@ -47,9 +47,10 @@ codemap --config /path/to/codemap.config.json --full # Re-index only given paths (relative to project root) codemap --files src/a.ts src/b.tsx -# Scaffold .agents/ rules and skills from bundled templates (see CONTRIBUTING) +# Scaffold .agents/ from bundled templates — full matrix: docs/agents.md codemap agents init codemap agents init --force +codemap agents init --interactive # -i; IDE wiring + symlink vs copy ``` **Environment / flags:** `--root` overrides **`CODEMAP_ROOT`** / **`CODEMAP_TEST_BENCH`**, then **`process.cwd()`**. Indexing a project outside this clone: [docs/benchmark.md § Indexing another project](docs/benchmark.md#indexing-another-project). diff --git a/bun.lock b/bun.lock index 3306cb0e..139e9072 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "@stainless-code/codemap", "dependencies": { + "@clack/prompts": "^1.2.0", "better-sqlite3": "^12.8.0", "fast-glob": "^3.3.3", "lightningcss": "^1.32.0", @@ -79,6 +80,10 @@ "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], + "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], + + "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], + "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], @@ -417,6 +422,12 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="], + + "fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -625,6 +636,8 @@ "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], diff --git a/docs/README.md b/docs/README.md index e6a1a541..eddeec8b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,11 +5,12 @@ Technical docs for **[@stainless-code/codemap](https://github.com/stainless-code | File | Topic | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [architecture.md](./architecture.md) | Schema, layering, CLI, API, [**User config**](./architecture.md#user-config) (Zod), parsers, [Key Files](./architecture.md#key-files) | +| [agents.md](./agents.md) | **`codemap agents init`**, **`--interactive`**, **`.gitignore` / `.codemap.*`**, IDE wiring (Cursor, Copilot, Windsurf, …), **`templates/agents`** | | [benchmark.md](./benchmark.md) | [**Indexing another project**](./benchmark.md#indexing-another-project) · [**Benchmark script**](./benchmark.md#the-benchmark-script) · [`fixtures/minimal/`](../fixtures/minimal/) | | [packaging.md](./packaging.md) | **`CHANGELOG.md` / `dist/` / `templates/`** on npm, **engines**, [**Node vs Bun**](./packaging.md#node-vs-bun), [**Releases**](./packaging.md#releases) (Changesets) | | [roadmap.md](./roadmap.md) | Forward-looking backlog (not a `src/` inventory) | | [why-codemap.md](./why-codemap.md) | Why index + SQL for agents (speed, tokens, accuracy) | -**Cross-cutting:** Runtime splits (SQLite, workers, globs, JSON config I/O) — [packaging § Node vs Bun](./packaging.md#node-vs-bun) only (link it; don’t copy the table). **User config** shape/validation — [architecture § User config](./architecture.md#user-config) only. +**Cross-cutting:** Runtime splits (SQLite, workers, globs, JSON config I/O) — [packaging § Node vs Bun](./packaging.md#node-vs-bun) only (link it; don’t copy the table). **User config** shape/validation — [architecture § User config](./architecture.md#user-config) only. **Agent templates / `agents init`** — [agents.md](./agents.md) only (don’t duplicate the IDE table elsewhere). **Conventions:** One topic per file; relative links; avoid stale file/symbol counts in narrative docs (use `codemap query` / `bun run dev query` after indexing; methodology tables in [benchmark.md](./benchmark.md) are fine). **This repo:** `bun run dev` → `bun src/index.ts`; `bun run build` → tsdown → `dist/`; `bun run clean` / `bun run check-updates` — [.github/CONTRIBUTING.md](../.github/CONTRIBUTING.md). **Contributors:** branch + PR into **`main`** ([CI](../.github/workflows/ci.yml)), `bun run check`, JSDoc on public API. diff --git a/docs/agents.md b/docs/agents.md new file mode 100644 index 00000000..e17b1b8d --- /dev/null +++ b/docs/agents.md @@ -0,0 +1,59 @@ +# Agent templates and `codemap agents init` + +Hub: [README.md](./README.md). **Package layout:** [packaging.md](./packaging.md) (`templates/` on npm). **CLI layering:** [architecture.md § Key Files](./architecture.md#key-files). + +## What it does + +The published package ships **`templates/agents/`** (rules + skills; mirrored in this repo under [`.agents/`](../.agents/)). The command **`codemap agents init`** copies that tree into **`/.agents/`** — the **canonical** copy consumers edit (SQL, team conventions, paths). + +```bash +codemap agents init +codemap agents init --force +codemap agents init --interactive # or -i; requires a TTY +``` + +- **`--force`** — replace an existing **`.agents/`** directory. +- **`--interactive`** — multiselect which tools to wire (see below); choose **symlink** vs **copy** for integrations that mirror **`.agents/rules`** (and Cursor also **`.agents/skills`**). Uses [**@clack/prompts**](https://github.com/bombshell-dev/clack); **non-TTY** runs exit with an error. + +## Git and `.gitignore` + +If **`/.git`** exists, Codemap ensures **`.codemap.*`** is listed so SQLite artifacts (e.g. **`.codemap.db`**, WAL/SHM) stay untracked: + +- No **`.gitignore`** → create one containing **`.codemap.*`**. +- **`.gitignore`** exists → append **`.codemap.*`** once if missing. + +If the project is **not** a Git working tree, **`.gitignore`** is not created. + +## Optional IDE / tool wiring + +All integrations reuse the **same** bundled content under **`.agents/`**. Symlink-style rows use one **link mode** for the whole run (**symlink** or **copy**) when any of them is selected. + +| Integration | What gets created | Notes | +| ------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| **Cursor** | **`.cursor/rules`**, **`.cursor/skills`** → **`.agents/`** | Symlink or copy both trees. | +| **Windsurf** | **`.windsurf/rules`** → **`.agents/rules`** | Rules only. | +| **Continue** | **`.continue/rules`** → **`.agents/rules`** | [Continue rules](https://docs.continue.dev/customize/rules). | +| **Cline** | **`.clinerules`** → **`.agents/rules`** | Directory symlink/copy. | +| **Amazon Q** | **`.amazonq/rules`** → **`.agents/rules`** | [AWS rules](https://aws.amazon.com/blogs/devops/mastering-amazon-q-developer-with-rules/). | +| **GitHub Copilot** | **`.github/copilot-instructions.md`** | Pointer + link to [GitHub Docs](https://docs.github.com/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot). | +| **Claude Code** | **`CLAUDE.md`** | Root onboarding pointer. | +| **Zed / JetBrains / Aider (generic)** | **`AGENTS.md`** | Many tools read root **`AGENTS.md`**; JetBrains/Aider have no single mandated path — this file is the shared hook. | +| **Gemini** | **`GEMINI.md`** | For integrations that load **`GEMINI.md`**. | + +Pointer files (**`CLAUDE.md`**, **`AGENTS.md`**, **`GEMINI.md`**, Copilot instructions) are **skipped** if the file already exists unless **`--force`** (then overwritten where applicable). + +## Implementation (for contributors) + +| Source | Role | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`src/agents-init.ts`** | Copy **`templates/agents`** → **`.agents/`**, **`applyAgentsInitTargets`**, **`ensureGitignoreCodemapPattern`**, exported **`targetsNeedLinkMode`**. | +| **`src/agents-init-interactive.ts`** | **`@clack/prompts`** flow; calls **`runAgentsInit`**. | +| **`src/cli/cmd-agents.ts`** | Lazy-loaded from **`src/cli/main.ts`**. | + +Do **not** duplicate long IDE matrices in **README.md** or **packaging.md** — link **here** instead. + +## Related + +- [architecture.md](./architecture.md) — CLI chunks, layering. +- [.github/CONTRIBUTING.md](../.github/CONTRIBUTING.md) — Cursor symlink notes, **`main`** / PR workflow. +- [why-codemap.md](./why-codemap.md) — why SQL + index for agents. diff --git a/docs/architecture.md b/docs/architecture.md index ad2188fd..d1995c15 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -92,26 +92,26 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru ## Key Files -| File | Purpose | -| ----------------- | ----------------------------------------------------------------------------------------------------------------- | -| `index.ts` | Package entry — re-exports `api` / `config`, runs CLI when main | -| `cli/` | CLI — bootstrap argv, lazy command modules, `query` / `agents init` / index modes | -| `api.ts` | Programmatic API — `createCodemap`, `Codemap`, `runCodemapIndex` | -| `application/` | Indexing use cases and engine (`run-index`, `index-engine`, types) | -| `worker-pool.ts` | Parallel parse workers (Bun / Node) | -| `db.ts` | SQLite adapter — schema DDL, typed CRUD, connection management | -| `parser.ts` | TS/TSX/JS/JSX extraction via `oxc-parser` — symbols, imports, exports, components, markers | -| `css-parser.ts` | CSS extraction via `lightningcss` — custom properties, classes, keyframes, `@theme` blocks | -| `resolver.ts` | Import path resolution via `oxc-resolver` — respects `tsconfig` aliases, builds dependency graph | -| `constants.ts` | Shared constants — e.g. `LANG_MAP` | -| `glob-sync.ts` | Include globs — Bun `Glob` vs `fast-glob` on Node ([packaging § Node vs Bun](./packaging.md#node-vs-bun)) | -| `markers.ts` | Shared marker extraction (`TODO`/`FIXME`/`HACK`/`NOTE`) — used by all parsers | -| `parse-worker.ts` | Worker thread entry point — reads, parses, and extracts file data in parallel | -| `adapters/` | `LanguageAdapter` types and built-in TS/CSS/text implementations | -| `parsed-types.ts` | Shared `ParsedFile` shape for workers and adapters | -| `agents-init.ts` | `codemap agents init` — copies `templates/agents` → `.agents/` | -| `benchmark.ts` | SQL vs traditional timing script — see [benchmark.md § The benchmark script](./benchmark.md#the-benchmark-script) | -| `config.ts` | `codemap.config.*` load path, **Zod** user schema (`codemapUserConfigSchema`), `resolveCodemapConfig` | +| File | Purpose | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `index.ts` | Package entry — re-exports `api` / `config`, runs CLI when main | +| `cli/` | CLI — bootstrap argv, lazy command modules, `query` / `agents init` / index modes | +| `api.ts` | Programmatic API — `createCodemap`, `Codemap`, `runCodemapIndex` | +| `application/` | Indexing use cases and engine (`run-index`, `index-engine`, types) | +| `worker-pool.ts` | Parallel parse workers (Bun / Node) | +| `db.ts` | SQLite adapter — schema DDL, typed CRUD, connection management | +| `parser.ts` | TS/TSX/JS/JSX extraction via `oxc-parser` — symbols, imports, exports, components, markers | +| `css-parser.ts` | CSS extraction via `lightningcss` — custom properties, classes, keyframes, `@theme` blocks | +| `resolver.ts` | Import path resolution via `oxc-resolver` — respects `tsconfig` aliases, builds dependency graph | +| `constants.ts` | Shared constants — e.g. `LANG_MAP` | +| `glob-sync.ts` | Include globs — Bun `Glob` vs `fast-glob` on Node ([packaging § Node vs Bun](./packaging.md#node-vs-bun)) | +| `markers.ts` | Shared marker extraction (`TODO`/`FIXME`/`HACK`/`NOTE`) — used by all parsers | +| `parse-worker.ts` | Worker thread entry point — reads, parses, and extracts file data in parallel | +| `adapters/` | `LanguageAdapter` types and built-in TS/CSS/text implementations | +| `parsed-types.ts` | Shared `ParsedFile` shape for workers and adapters | +| `agents-init.ts` / `agents-init-interactive.ts` | `codemap agents init` — see [agents.md](./agents.md) (templates → `.agents/`, `--interactive`, `.gitignore`, IDE wiring) | +| `benchmark.ts` | SQL vs traditional timing script — see [benchmark.md § The benchmark script](./benchmark.md#the-benchmark-script) | +| `config.ts` | `codemap.config.*` load path, **Zod** user schema (`codemapUserConfigSchema`), `resolveCodemapConfig` | ## CLI usage @@ -131,6 +131,8 @@ codemap --full codemap query "SELECT name, file_path FROM symbols LIMIT 20" ``` +**Agent templates:** `codemap agents init` — [agents.md](./agents.md). + Timings and methodology: [benchmark.md](./benchmark.md). **Startup / Node vs Bun** (not the same as benchmark scenarios): [benchmark.md § CLI and runtime startup](./benchmark.md#cli-and-runtime-startup). ### Help, version, and invalid argv diff --git a/docs/packaging.md b/docs/packaging.md index b9051a8d..8e13ecc0 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -41,3 +41,4 @@ Releases use [**Changesets**](https://github.com/changesets/changesets). Repo co ## Related - [architecture.md](./architecture.md) — schema, layering, API. +- [agents.md](./agents.md) — **`templates/agents`**, **`codemap agents init`**, published **`files`** surface above. diff --git a/docs/roadmap.md b/docs/roadmap.md index 46050cf0..ae95cfee 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap -Forward-looking plans only — **not** a mirror of `src/`. **Hub:** [README.md](./README.md). **Design:** [architecture.md](./architecture.md), [packaging.md](./packaging.md). **Shipped features** (adapters, fixtures, `codemap agents init`) live in `src/` and linked docs — not enumerated here. +Forward-looking plans only — **not** a mirror of `src/`. **Hub:** [README.md](./README.md). **Design:** [architecture.md](./architecture.md), [packaging.md](./packaging.md). **Shipped features** (adapters, fixtures, `codemap agents init` — [agents.md](./agents.md)) live in `src/` and linked docs — not enumerated here. --- diff --git a/package.json b/package.json index c3d48d6b..ea8e92c3 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { + "@clack/prompts": "^1.2.0", "better-sqlite3": "^12.8.0", "fast-glob": "^3.3.3", "lightningcss": "^1.32.0", diff --git a/src/agents-init-interactive.ts b/src/agents-init-interactive.ts new file mode 100644 index 00000000..37ebd12c --- /dev/null +++ b/src/agents-init-interactive.ts @@ -0,0 +1,164 @@ +import { + cancel, + confirm, + intro, + isCancel, + multiselect, + note, + outro, + select, +} from "@clack/prompts"; + +import type { AgentsInitLinkMode, AgentsInitTarget } from "./agents-init"; +import { runAgentsInit, targetsNeedLinkMode } from "./agents-init"; + +export interface RunAgentsInitInteractiveOptions { + projectRoot: string; + force: boolean; +} + +const INTEGRATION_OPTIONS: { + value: AgentsInitTarget; + label: string; + hint: string; +}[] = [ + { + value: "cursor", + label: "Cursor", + hint: ".cursor/rules + skills → .agents/", + }, + { + value: "claude-md", + label: "Claude Code", + hint: "CLAUDE.md", + }, + { + value: "copilot", + label: "GitHub Copilot", + hint: ".github/copilot-instructions.md", + }, + { + value: "windsurf", + label: "Windsurf (Cascade)", + hint: ".windsurf/rules → .agents/rules", + }, + { + value: "continue", + label: "Continue", + hint: ".continue/rules → .agents/rules", + }, + { + value: "cline", + label: "Cline", + hint: ".clinerules → .agents/rules", + }, + { + value: "amazon-q", + label: "Amazon Q Developer", + hint: ".amazonq/rules → .agents/rules", + }, + { + value: "agents-md", + label: "AGENTS.md (Zed, JetBrains, Aider, …)", + hint: "Root AGENTS.md", + }, + { + value: "gemini-md", + label: "Gemini", + hint: "GEMINI.md", + }, +]; + +function summarizeTargets(targets: AgentsInitTarget[]): string[] { + const lines: string[] = []; + for (const t of targets) { + const opt = INTEGRATION_OPTIONS.find((o) => o.value === t); + lines.push(opt ? `${opt.label}: ${opt.hint}` : t); + } + return lines; +} + +/** + * Interactive `codemap agents init`: choose integrations and symlink vs copy for rule mirrors. + */ +export async function runAgentsInitInteractive( + opts: RunAgentsInitInteractiveOptions, +): Promise { + intro("codemap agents init"); + note( + "Canonical templates always install to .agents/ (rules + skills).\nOptional steps wire other tools to the same content.", + "Codemap", + ); + + const targetsRaw = await multiselect({ + message: "Integrations (space to toggle, enter to confirm)", + options: INTEGRATION_OPTIONS, + required: false, + initialValues: [], + }); + + if (isCancel(targetsRaw)) { + cancel("Cancelled."); + return false; + } + + const targets = targetsRaw as AgentsInitTarget[]; + + let linkMode: AgentsInitLinkMode = "symlink"; + if (targetsNeedLinkMode(targets)) { + const mode = await select({ + message: + "How should tools that mirror .agents/rules (and Cursor skills) link?", + options: [ + { + value: "symlink", + label: "Symlink", + hint: "One source of truth; best on macOS / Linux", + }, + { + value: "copy", + label: "Copy", + hint: "Duplicate files; safest on Windows / sandboxes", + }, + ], + initialValue: "symlink", + }); + if (isCancel(mode)) { + cancel("Cancelled."); + return false; + } + linkMode = mode; + } + + const lines = [ + `Project: ${opts.projectRoot}`, + "Will write: .agents/rules, .agents/skills", + ...summarizeTargets(targets).map((l) => `• ${l}`), + ]; + + note(lines.join("\n"), "Summary"); + + const ok = await confirm({ + message: "Proceed?", + initialValue: true, + }); + + if (isCancel(ok) || !ok) { + cancel("Cancelled."); + return false; + } + + const success = runAgentsInit({ + projectRoot: opts.projectRoot, + force: opts.force, + targets, + linkMode, + }); + + if (success) { + outro( + "Done. Edit .agents/ for your team; restart IDEs if rules did not reload.", + ); + } + return success; +} diff --git a/src/agents-init.test.ts b/src/agents-init.test.ts index ccb612cf..89a3e988 100644 --- a/src/agents-init.test.ts +++ b/src/agents-init.test.ts @@ -14,6 +14,7 @@ import { ensureGitignoreCodemapPattern, resolveAgentsTemplateDir, runAgentsInit, + targetsNeedLinkMode, } from "./agents-init"; describe("runAgentsInit", () => { @@ -119,4 +120,115 @@ describe("runAgentsInit", () => { rmSync(dir, { recursive: true, force: true }); } }); + + it("runAgentsInit with Cursor target copies into .cursor/", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); + try { + expect( + runAgentsInit({ + projectRoot: dir, + force: true, + targets: ["cursor"], + linkMode: "copy", + }), + ).toBe(true); + expect( + readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"), + ).toContain("codemap"); + expect( + readFileSync( + join(dir, ".cursor", "skills", "codemap", "SKILL.md"), + "utf-8", + ).length, + ).toBeGreaterThan(100); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("runAgentsInit with claude-md writes CLAUDE.md", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); + try { + expect( + runAgentsInit({ + projectRoot: dir, + force: true, + targets: ["claude-md"], + }), + ).toBe(true); + expect(readFileSync(join(dir, "CLAUDE.md"), "utf-8")).toContain( + "Codemap", + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("targetsNeedLinkMode is true only for symlink-style integrations", () => { + expect(targetsNeedLinkMode([])).toBe(false); + expect(targetsNeedLinkMode(["claude-md", "copilot"])).toBe(false); + expect(targetsNeedLinkMode(["cursor"])).toBe(true); + expect(targetsNeedLinkMode(["windsurf", "agents-md"])).toBe(true); + }); + + it("runAgentsInit writes Copilot and pointer files", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); + try { + expect( + runAgentsInit({ + projectRoot: dir, + force: true, + targets: ["copilot", "agents-md", "gemini-md"], + }), + ).toBe(true); + expect( + readFileSync(join(dir, ".github", "copilot-instructions.md"), "utf-8"), + ).toContain("Copilot"); + expect(readFileSync(join(dir, "AGENTS.md"), "utf-8")).toContain( + "Codemap", + ); + expect(readFileSync(join(dir, "GEMINI.md"), "utf-8")).toContain( + "Codemap", + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("runAgentsInit with Windsurf copies .windsurf/rules", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); + try { + expect( + runAgentsInit({ + projectRoot: dir, + force: true, + targets: ["windsurf"], + linkMode: "copy", + }), + ).toBe(true); + expect( + readFileSync(join(dir, ".windsurf", "rules", "codemap.mdc"), "utf-8"), + ).toContain("codemap"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("runAgentsInit refuses Cursor wiring when .cursor/rules exists without force", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); + try { + mkdirSync(join(dir, ".cursor", "rules"), { recursive: true }); + writeFileSync(join(dir, ".cursor", "rules", "x.mdc"), "", "utf-8"); + expect(() => + runAgentsInit({ + projectRoot: dir, + force: false, + targets: ["cursor"], + linkMode: "copy", + }), + ).toThrow(/\.cursor\/rules already exists/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); }); diff --git a/src/agents-init.ts b/src/agents-init.ts index 0ce8cd19..9edb15fd 100644 --- a/src/agents-init.ts +++ b/src/agents-init.ts @@ -4,9 +4,11 @@ import { existsSync, mkdirSync, readFileSync, + rmSync, + symlinkSync, writeFileSync, } from "node:fs"; -import { dirname, join } from "node:path"; +import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; /** @@ -24,11 +26,81 @@ export function resolveAgentsTemplateDir(): string { /** Default DB basename `.codemap` plus SQLite sidecars (`.db`, `-wal`, `-shm`, …). */ const GITIGNORE_CODEMAP_PATTERN = ".codemap.*"; +/** + * Optional integrations after canonical `.agents/` is written. + * - Symlink/copy: `cursor`, `windsurf`, `continue`, `cline`, `amazon-q` (rules → `.agents/rules`; Cursor also maps skills). + * - Pointer files: `copilot`, `claude-md`, `agents-md`, `gemini-md`. + */ +export type AgentsInitTarget = + | "cursor" + | "claude-md" + | "copilot" + | "windsurf" + | "continue" + | "cline" + | "amazon-q" + | "agents-md" + | "gemini-md"; + +/** Targets that mirror `.agents/rules` (and Cursor also `.agents/skills`) via symlink or copy. */ +export const AGENTS_INIT_SYMLINK_TARGETS: readonly AgentsInitTarget[] = [ + "cursor", + "windsurf", + "continue", + "cline", + "amazon-q", +] as const; + +export function targetsNeedLinkMode(targets: AgentsInitTarget[]): boolean { + return targets.some((t) => AGENTS_INIT_SYMLINK_TARGETS.includes(t)); +} + +/** How symlink-style integrations receive `.agents` rules (and Cursor skills). */ +export type AgentsInitLinkMode = "symlink" | "copy"; + +const POINTER_BODY = `This project uses [Codemap](https://github.com/stainless-code/codemap) — a structural SQLite index for AI agents. + +- **Skill:** \`.agents/skills/codemap/SKILL.md\` +- **CLI:** \`codemap\` to index, \`codemap query "SELECT …"\` for SQL +- **Rules:** \`.agents/rules/\` + +`; + +const CLAUDE_MD_TEMPLATE = `# Codemap\n\n${POINTER_BODY}`; + +const AGENTS_MD_TEMPLATE = `# Agent instructions (Codemap) + +${POINTER_BODY} +Also referenced by **Zed**, **JetBrains AI**-style tools, **Aider**, and other agents that read \`AGENTS.md\` at the repo root. + +`; + +const GEMINI_MD_TEMPLATE = `# Codemap (Gemini) + +${POINTER_BODY} +Use this file if your **Gemini** CLI or IDE loads \`GEMINI.md\` at the repo root. + +`; + +const COPILOT_TEMPLATE = `# Codemap — GitHub Copilot custom instructions + +${POINTER_BODY} +See [GitHub Docs: custom instructions for Copilot](https://docs.github.com/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot). + +`; + export interface AgentsInitOptions { /** Project root (`.agents/` is created here). */ projectRoot: string; /** Overwrite existing files. */ force?: boolean; + /** Extra tool integrations (after `.agents/` is written). */ + targets?: AgentsInitTarget[]; + /** + * Used when any symlink-style target is selected (\`cursor\`, \`windsurf\`, \`continue\`, \`cline\`, \`amazon-q\`). + * Default \`symlink\`. + */ + linkMode?: AgentsInitLinkMode; } /** @@ -64,8 +136,206 @@ export function ensureGitignoreCodemapPattern(projectRoot: string): void { console.log(` Appended ${GITIGNORE_CODEMAP_PATTERN} to .gitignore`); } +function removePathForRewrite( + path: string, + force: boolean, + label: string, +): void { + if (!existsSync(path)) { + return; + } + if (!force) { + throw new Error( + `Codemap: ${label} already exists — use --force to replace, or remove it manually.`, + ); + } + rmSync(path, { recursive: true, force: true }); +} + +/** + * Map `.agents/rules` into a destination directory (symlink or copy). + */ +function wireAgentsRulesTo( + projectRoot: string, + destPath: string, + label: string, + linkMode: AgentsInitLinkMode, + force: boolean, +): void { + const agentsRules = join(projectRoot, ".agents", "rules"); + mkdirSync(dirname(destPath), { recursive: true }); + removePathForRewrite(destPath, force, label); + if (linkMode === "symlink") { + const rel = relative(dirname(destPath), agentsRules); + try { + symlinkSync(rel, destPath, "dir"); + } catch (err) { + throw new Error( + `Codemap: symlink failed for ${label} (${String(err)}). Try copy mode or check permissions on Windows.`, + { cause: err }, + ); + } + console.log(` Linked ${label} → .agents/rules`); + return; + } + cpSync(agentsRules, destPath, { recursive: true }); + console.log(` Copied .agents/rules → ${label}`); +} + /** - * Copy bundled rules and skills into `/.agents/`. + * Wire Cursor or other tools after `.agents/` exists. + */ +export function applyAgentsInitTargets( + projectRoot: string, + targets: AgentsInitTarget[], + linkMode: AgentsInitLinkMode, + force: boolean, +): void { + const agentsRules = join(projectRoot, ".agents", "rules"); + const agentsSkills = join(projectRoot, ".agents", "skills"); + if (!existsSync(agentsRules) || !existsSync(agentsSkills)) { + throw new Error( + "Codemap: .agents/rules and .agents/skills must exist before wiring integrations", + ); + } + + for (const t of targets) { + switch (t) { + case "cursor": + applyCursorIntegration(projectRoot, linkMode, force); + break; + case "windsurf": + wireAgentsRulesTo( + projectRoot, + join(projectRoot, ".windsurf", "rules"), + ".windsurf/rules", + linkMode, + force, + ); + break; + case "continue": + wireAgentsRulesTo( + projectRoot, + join(projectRoot, ".continue", "rules"), + ".continue/rules", + linkMode, + force, + ); + break; + case "cline": + wireAgentsRulesTo( + projectRoot, + join(projectRoot, ".clinerules"), + ".clinerules", + linkMode, + force, + ); + break; + case "amazon-q": + wireAgentsRulesTo( + projectRoot, + join(projectRoot, ".amazonq", "rules"), + ".amazonq/rules", + linkMode, + force, + ); + break; + case "claude-md": + writePointerFile( + join(projectRoot, "CLAUDE.md"), + CLAUDE_MD_TEMPLATE, + "CLAUDE.md", + force, + ); + break; + case "copilot": + mkdirSync(join(projectRoot, ".github"), { recursive: true }); + writePointerFile( + join(projectRoot, ".github", "copilot-instructions.md"), + COPILOT_TEMPLATE, + ".github/copilot-instructions.md", + force, + ); + break; + case "agents-md": + writePointerFile( + join(projectRoot, "AGENTS.md"), + AGENTS_MD_TEMPLATE, + "AGENTS.md", + force, + ); + break; + case "gemini-md": + writePointerFile( + join(projectRoot, "GEMINI.md"), + GEMINI_MD_TEMPLATE, + "GEMINI.md", + force, + ); + break; + } + } +} + +function writePointerFile( + path: string, + content: string, + label: string, + force: boolean, +): void { + if (existsSync(path) && !force) { + console.warn( + ` Skipped ${label} (file exists). Use --force to overwrite, or merge manually.`, + ); + return; + } + writeFileSync(path, content, "utf-8"); + console.log(` Wrote ${label} with Codemap pointers`); +} + +function applyCursorIntegration( + projectRoot: string, + linkMode: AgentsInitLinkMode, + force: boolean, +): void { + const agentsRules = join(projectRoot, ".agents", "rules"); + const agentsSkills = join(projectRoot, ".agents", "skills"); + const cursorRules = join(projectRoot, ".cursor", "rules"); + const cursorSkills = join(projectRoot, ".cursor", "skills"); + + mkdirSync(join(projectRoot, ".cursor"), { recursive: true }); + + if (linkMode === "symlink") { + removePathForRewrite(cursorRules, force, ".cursor/rules"); + removePathForRewrite(cursorSkills, force, ".cursor/skills"); + const relRules = relative(dirname(cursorRules), agentsRules); + const relSkills = relative(dirname(cursorSkills), agentsSkills); + try { + symlinkSync(relRules, cursorRules, "dir"); + symlinkSync(relSkills, cursorSkills, "dir"); + } catch (err) { + throw new Error( + `Codemap: symlink failed for Cursor integration (${String(err)}). Try re-running with copy mode or check permissions on Windows.`, + { cause: err }, + ); + } + console.log( + " Linked .cursor/rules → .agents/rules and .cursor/skills → .agents/skills", + ); + return; + } + + removePathForRewrite(cursorRules, force, ".cursor/rules"); + removePathForRewrite(cursorSkills, force, ".cursor/skills"); + cpSync(agentsRules, cursorRules, { recursive: true }); + cpSync(agentsSkills, cursorSkills, { recursive: true }); + console.log( + " Copied rules and skills into .cursor/rules and .cursor/skills", + ); +} + +/** + * Copy bundled rules and skills into `/.agents/`, optional integrations, `.gitignore` hint. * @returns `false` when `.agents/` exists and `--force` was not used. */ export function runAgentsInit(options: AgentsInitOptions): boolean { @@ -93,9 +363,22 @@ export function runAgentsInit(options: AgentsInitOptions): boolean { }); console.log(` Wrote agent templates to ${destRoot}`); + + const targets = options.targets ?? []; + const linkMode = options.linkMode ?? "symlink"; + if (targets.length > 0) { + applyAgentsInitTargets( + options.projectRoot, + targets, + linkMode, + !!options.force, + ); + } else { + console.log( + " Tip: run `codemap agents init --interactive` to wire editors (Cursor, Copilot, …) or add CLAUDE.md / AGENTS.md", + ); + } + ensureGitignoreCodemapPattern(options.projectRoot); - console.log( - ` Symlink into .cursor/ if needed — see https://github.com/stainless-code/codemap/blob/main/.github/CONTRIBUTING.md`, - ); return true; } diff --git a/src/cli/bootstrap.ts b/src/cli/bootstrap.ts index 7b177611..c45b26e0 100644 --- a/src/cli/bootstrap.ts +++ b/src/cli/bootstrap.ts @@ -14,7 +14,7 @@ Query: codemap query "" Agents: - codemap agents init [--force] + codemap agents init [--force] [--interactive|-i] Other: codemap version @@ -43,7 +43,7 @@ export function validateIndexModeArgs(rest: string[]): void { if (rest[0] === "agents") { if (rest[1] === "init") return; console.error( - `codemap: unknown agents command "${rest[1] ?? "(missing)"}". Expected: codemap agents init [--force]`, + `codemap: unknown agents command "${rest[1] ?? "(missing)"}". Expected: codemap agents init [--force] [--interactive|-i]`, ); process.exit(1); } diff --git a/src/cli/cmd-agents.ts b/src/cli/cmd-agents.ts index 7cd65186..61a68495 100644 --- a/src/cli/cmd-agents.ts +++ b/src/cli/cmd-agents.ts @@ -1,8 +1,20 @@ import { runAgentsInit } from "../agents-init"; -export function runAgentsInitCmd(opts: { +export async function runAgentsInitCmd(opts: { projectRoot: string; force: boolean; -}): boolean { + interactive: boolean; +}): Promise { + if (opts.interactive) { + if (!process.stdin.isTTY || !process.stdout.isTTY) { + console.error( + "codemap: --interactive requires an interactive terminal (TTY).", + ); + process.exit(1); + } + const { runAgentsInitInteractive } = + await import("../agents-init-interactive.js"); + return runAgentsInitInteractive(opts); + } return runAgentsInit(opts); } diff --git a/src/cli/main.ts b/src/cli/main.ts index 06788d28..1798ef0e 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -26,17 +26,34 @@ export async function main(): Promise { if (rest[0] === "agents" && rest[1] === "init") { if (rest.includes("--help") || rest.includes("-h")) { - console.log(`Usage: codemap agents init [--force] + console.log(`Usage: codemap agents init [--force] [--interactive|-i] Copies bundled agent templates into .agents/ under the project root. -Use --force to overwrite an existing .agents/ directory. + --force Overwrite an existing .agents/ directory + --interactive Pick IDEs (Cursor, Copilot, Windsurf, …) and symlink vs copy `); return; } + const initRest = rest.slice(2); + const knownInit = new Set([ + "--force", + "--interactive", + "-i", + "--help", + "-h", + ]); + for (const a of initRest) { + if (a.startsWith("-") && !knownInit.has(a)) { + console.error(`codemap: unknown option "${a}"`); + console.error("Run codemap agents init --help for usage."); + process.exit(1); + } + } const { runAgentsInitCmd } = await import("./cmd-agents.js"); - const ok = runAgentsInitCmd({ + const ok = await runAgentsInitCmd({ projectRoot: root, force: rest.includes("--force"), + interactive: rest.includes("--interactive") || rest.includes("-i"), }); if (!ok) process.exit(1); return; diff --git a/templates/agents/README.md b/templates/agents/README.md index 8e0c069b..ffaa2d51 100644 --- a/templates/agents/README.md +++ b/templates/agents/README.md @@ -1,5 +1,7 @@ # Bundled agent templates -These files are **copies** of the upstream [`.agents/`](../../.agents/) rules and skills shipped with `@stainless-code/codemap` for `codemap agents init`. +These files are **copies** of the upstream [`.agents/`](../../.agents/) rules and skills shipped with **`@stainless-code/codemap`** for **`codemap agents init`**. + +**Documentation:** [docs/agents.md](../../docs/agents.md) — interactive setup, **`.gitignore`**, and optional IDE wiring (Cursor, Copilot, …). After running the command, **edit** `.agents/` in your project (paths, SQL, team conventions). Treat updates here as a reference when refreshing your copy. From f630d81a4a741a2244553243c01035547721c2a3 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 7 Apr 2026 12:54:56 +0300 Subject: [PATCH 4/4] feat(agents): granular init, pointer upsert, per-file IDE symlinks - Template and IDE wiring use per-file copy/symlink; --force only removes template paths under .agents/rules|skills - CLAUDE.md/AGENTS.md/GEMINI.md/Copilot: managed codemap-pointer sections; merge, legacy migration, --force full replace - CLI: reject stray positional args on codemap agents init - Docs: hub index, single-source table, agents.md pointer section --- .changeset/agents-init-gitignore.md | 4 +- README.md | 2 +- docs/README.md | 37 ++-- docs/agents.md | 38 ++-- docs/architecture.md | 60 +++--- docs/benchmark.md | 8 +- docs/packaging.md | 7 +- docs/roadmap.md | 2 +- docs/why-codemap.md | 2 +- src/agents-init.test.ts | 189 +++++++++++++++++- src/agents-init.ts | 292 ++++++++++++++++++++++------ src/cli.test.ts | 7 + src/cli/main.ts | 13 +- 13 files changed, 516 insertions(+), 145 deletions(-) diff --git a/.changeset/agents-init-gitignore.md b/.changeset/agents-init-gitignore.md index e4a2bcd2..3f0f7967 100644 --- a/.changeset/agents-init-gitignore.md +++ b/.changeset/agents-init-gitignore.md @@ -2,6 +2,6 @@ "@stainless-code/codemap": patch --- -**`codemap agents init`:** For Git repos, ensure **`.codemap.*`** is in **`.gitignore`** (create the file or append the line once). **`--interactive` / `-i`** — pick IDE integrations (Cursor, GitHub Copilot, Windsurf, Continue, Cline, Amazon Q, **`CLAUDE.md`**, **`AGENTS.md`**, **`GEMINI.md`**) and symlink vs copy for rule mirrors; requires a TTY. Depends on **`@clack/prompts`**. +**`codemap agents init`:** For Git repos, ensure **`.codemap.*`** is in **`.gitignore`** (create the file or append the line once). **`--force`** removes only template file paths (same relpaths under **`.agents/rules/`** and **`.agents/skills/`** as **`templates/agents`**) before merging; other files under **`.agents/`**, **`rules/`**, or **`skills/`** are kept. **`--interactive` / `-i`** — pick IDE integrations (Cursor, GitHub Copilot, Windsurf, Continue, Cline, Amazon Q, **`CLAUDE.md`**, **`AGENTS.md`**, **`GEMINI.md`**) and symlink vs copy for rule mirrors; requires a TTY. Unknown positional arguments (e.g. `interactive` without `--interactive`) are rejected. Depends on **`@clack/prompts`**. -**Docs:** New hub section **[`docs/agents.md`](https://github.com/stainless-code/codemap/blob/main/docs/agents.md)**; **[`docs/README.md`](https://github.com/stainless-code/codemap/blob/main/docs/README.md)** index updated. Root **[`.gitignore`](https://github.com/stainless-code/codemap/blob/main/.gitignore)** uses a single **`.codemap.*`** line. +**Docs:** **[`docs/agents.md`](https://github.com/stainless-code/codemap/blob/main/docs/agents.md)**; **[`docs/README.md`](https://github.com/stainless-code/codemap/blob/main/docs/README.md)** index updated. Root **[`.gitignore`](https://github.com/stainless-code/codemap/blob/main/.gitignore)** uses a single **`.codemap.*`** line. diff --git a/README.md b/README.md index 4d6d9ff5..52a5974b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - **Not** full-text search or grep on arbitrary strings — use those when you need raw file-body search. - **Is** a fast, token-efficient way to navigate **structure**: definitions, imports, dependency direction, components, and other extracted facts. -**Documentation:** [docs/README.md](docs/README.md) indexes technical docs (architecture, **[`docs/agents.md`](docs/agents.md)** for `codemap agents init`, packaging, roadmap, benchmarks). **Bundled rules/skills:** [`.agents/rules/`](.agents/rules/), [`.agents/skills/codemap/SKILL.md`](.agents/skills/codemap/SKILL.md). **Consumers:** [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). +**Documentation:** [docs/README.md](docs/README.md) is the hub (topic index + single-source rules). Topics: [architecture](docs/architecture.md), [agents](docs/agents.md) (`codemap agents init`), [benchmark](docs/benchmark.md), [packaging](docs/packaging.md), [roadmap](docs/roadmap.md), [why Codemap](docs/why-codemap.md). **Bundled rules/skills:** [`.agents/rules/`](.agents/rules/), [`.agents/skills/codemap/SKILL.md`](.agents/skills/codemap/SKILL.md). **Consumers:** [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). --- diff --git a/docs/README.md b/docs/README.md index eddeec8b..9012be6d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +1,31 @@ # Codemap — documentation index -Technical docs for **[@stainless-code/codemap](https://github.com/stainless-code/codemap)**. Quick start: [../README.md](../README.md). +Technical docs for **[@stainless-code/codemap](https://github.com/stainless-code/codemap)**. -| File | Topic | -| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [architecture.md](./architecture.md) | Schema, layering, CLI, API, [**User config**](./architecture.md#user-config) (Zod), parsers, [Key Files](./architecture.md#key-files) | -| [agents.md](./agents.md) | **`codemap agents init`**, **`--interactive`**, **`.gitignore` / `.codemap.*`**, IDE wiring (Cursor, Copilot, Windsurf, …), **`templates/agents`** | -| [benchmark.md](./benchmark.md) | [**Indexing another project**](./benchmark.md#indexing-another-project) · [**Benchmark script**](./benchmark.md#the-benchmark-script) · [`fixtures/minimal/`](../fixtures/minimal/) | -| [packaging.md](./packaging.md) | **`CHANGELOG.md` / `dist/` / `templates/`** on npm, **engines**, [**Node vs Bun**](./packaging.md#node-vs-bun), [**Releases**](./packaging.md#releases) (Changesets) | -| [roadmap.md](./roadmap.md) | Forward-looking backlog (not a `src/` inventory) | -| [why-codemap.md](./why-codemap.md) | Why index + SQL for agents (speed, tokens, accuracy) | +**Start here:** [../README.md](../README.md) (install, CLI, API, dev commands). **This folder** is deeper reference — pick a row below. -**Cross-cutting:** Runtime splits (SQLite, workers, globs, JSON config I/O) — [packaging § Node vs Bun](./packaging.md#node-vs-bun) only (link it; don’t copy the table). **User config** shape/validation — [architecture § User config](./architecture.md#user-config) only. **Agent templates / `agents init`** — [agents.md](./agents.md) only (don’t duplicate the IDE table elsewhere). +| File | Topic | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [why-codemap.md](./why-codemap.md) | Why index + SQL for agents (speed, tokens, accuracy) — good first read after the readme | +| [architecture.md](./architecture.md) | Schema, layering, CLI internals, API, [**User config**](./architecture.md#user-config) (Zod), parsers, [Key Files](./architecture.md#key-files) | +| [agents.md](./agents.md) | **`codemap agents init`** — granular **`templates/agents`** → **`.agents/`**, per-file IDE symlink/copy, **[pointer files](./agents.md#pointer-files)** (`codemap-pointer` markers), **`--interactive`**, **`.gitignore` / `.codemap.*`** | +| [benchmark.md](./benchmark.md) | [**Indexing another project**](./benchmark.md#indexing-another-project) · [**Benchmark script**](./benchmark.md#the-benchmark-script) · [`fixtures/minimal/`](../fixtures/minimal/) | +| [packaging.md](./packaging.md) | **`CHANGELOG.md` / `dist/` / `templates/`** on npm, **engines**, [**Node vs Bun**](./packaging.md#node-vs-bun), [**Releases**](./packaging.md#releases) (Changesets) | +| [roadmap.md](./roadmap.md) | Forward-looking backlog (not a `src/` inventory) | -**Conventions:** One topic per file; relative links; avoid stale file/symbol counts in narrative docs (use `codemap query` / `bun run dev query` after indexing; methodology tables in [benchmark.md](./benchmark.md) are fine). **This repo:** `bun run dev` → `bun src/index.ts`; `bun run build` → tsdown → `dist/`; `bun run clean` / `bun run check-updates` — [.github/CONTRIBUTING.md](../.github/CONTRIBUTING.md). **Contributors:** branch + PR into **`main`** ([CI](../.github/workflows/ci.yml)), `bun run check`, JSDoc on public API. +## Single source of truth (do not duplicate) + +| Topic | Canonical doc | Elsewhere | +| ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| Runtime splits (SQLite, workers, globs, JSON config I/O) | [packaging § Node vs Bun](./packaging.md#node-vs-bun) — **the table lives here** | [architecture § Runtime](./architecture.md#runtime-and-database) links here; do not copy the table | +| **`codemap.config.*`** shape / Zod validation | [architecture § User config](./architecture.md#user-config) | Root [README § Configuration](../README.md#configuration) points here | +| **`codemap agents init`**: **`--force`** on **`.agents/`** (template file paths only), IDE matrix, per-file symlink/copy, **`templates/agents`** | [agents.md](./agents.md) | Link here; do not paste the integration table into README or packaging | +| **`CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / Copilot** — managed **`codemap-pointer`** sections, merge vs **`--force`** | [agents.md § Pointer files](./agents.md#pointer-files) | Link here; do not duplicate the situation table | +| End-user CLI (index, query, agents, flags, env) | [../README.md § CLI](../README.md#cli) | [architecture § CLI usage](./architecture.md#cli-usage) summarizes and links back | + +## Conventions + +- **One topic per file**; prefer relative links between these docs. +- **Avoid stale file/symbol counts** in narrative text — use `codemap query` / `bun run dev query` after indexing; methodology tables in [benchmark.md](./benchmark.md) are fine. +- **This repo:** `bun run dev` is **`bun src/index.ts`**; `bun run build` → tsdown → `dist/`; `bun run clean` / `bun run check-updates` — [.github/CONTRIBUTING.md](../.github/CONTRIBUTING.md). +- **Contributors:** branch + PR into **`main`** ([CI](../.github/workflows/ci.yml)), `bun run check`, JSDoc on public API. diff --git a/docs/agents.md b/docs/agents.md index e17b1b8d..143097ce 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -1,10 +1,10 @@ # Agent templates and `codemap agents init` -Hub: [README.md](./README.md). **Package layout:** [packaging.md](./packaging.md) (`templates/` on npm). **CLI layering:** [architecture.md § Key Files](./architecture.md#key-files). +**Doc index:** [README.md](./README.md). **Package layout:** [packaging.md](./packaging.md) (`templates/` on npm). **CLI layering:** [architecture.md § Key Files](./architecture.md#key-files). ## What it does -The published package ships **`templates/agents/`** (rules + skills; mirrored in this repo under [`.agents/`](../.agents/)). The command **`codemap agents init`** copies that tree into **`/.agents/`** — the **canonical** copy consumers edit (SQL, team conventions, paths). +The published package ships **`templates/agents/`** (rules + skills; mirrored in this repo under [`.agents/`](../.agents/)). The command **`codemap agents init`** writes each bundled file into **`/.agents/`** with per-file copies (not a wholesale directory sync) — the **canonical** copy consumers edit (SQL, team conventions, paths). ```bash codemap agents init @@ -12,7 +12,7 @@ codemap agents init --force codemap agents init --interactive # or -i; requires a TTY ``` -- **`--force`** — replace an existing **`.agents/`** directory. +- **`--force`** — if **`.agents/`** already exists, delete only the **same file paths** that ship in **`templates/agents`** (under **`rules/`** and **`skills/`**), then copy those files from the template. Any **other** files next to them (your custom rules, extra skill dirs, notes at **`.agents/`** root, etc.) are **not** removed. Use **`--interactive`**, not a bare **`interactive`** argument (unknown tokens are rejected). - **`--interactive`** — multiselect which tools to wire (see below); choose **symlink** vs **copy** for integrations that mirror **`.agents/rules`** (and Cursor also **`.agents/skills`**). Uses [**@clack/prompts**](https://github.com/bombshell-dev/clack); **non-TTY** runs exit with an error. ## Git and `.gitignore` @@ -30,30 +30,42 @@ All integrations reuse the **same** bundled content under **`.agents/`**. Symlin | Integration | What gets created | Notes | | ------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| **Cursor** | **`.cursor/rules`**, **`.cursor/skills`** → **`.agents/`** | Symlink or copy both trees. | +| **Cursor** | **`.cursor/rules`**, **`.cursor/skills`** → **`.agents/`** | Per-file symlink or copy (each rule/skill file, not a directory link). | | **Windsurf** | **`.windsurf/rules`** → **`.agents/rules`** | Rules only. | | **Continue** | **`.continue/rules`** → **`.agents/rules`** | [Continue rules](https://docs.continue.dev/customize/rules). | -| **Cline** | **`.clinerules`** → **`.agents/rules`** | Directory symlink/copy. | +| **Cline** | **`.clinerules`** → **`.agents/rules`** | Per-file symlink or copy. | | **Amazon Q** | **`.amazonq/rules`** → **`.agents/rules`** | [AWS rules](https://aws.amazon.com/blogs/devops/mastering-amazon-q-developer-with-rules/). | | **GitHub Copilot** | **`.github/copilot-instructions.md`** | Pointer + link to [GitHub Docs](https://docs.github.com/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot). | | **Claude Code** | **`CLAUDE.md`** | Root onboarding pointer. | | **Zed / JetBrains / Aider (generic)** | **`AGENTS.md`** | Many tools read root **`AGENTS.md`**; JetBrains/Aider have no single mandated path — this file is the shared hook. | | **Gemini** | **`GEMINI.md`** | For integrations that load **`GEMINI.md`**. | -Pointer files (**`CLAUDE.md`**, **`AGENTS.md`**, **`GEMINI.md`**, Copilot instructions) are **skipped** if the file already exists unless **`--force`** (then overwritten where applicable). +## Pointer files + +Root / Copilot **pointer** files (**`CLAUDE.md`**, **`AGENTS.md`**, **`GEMINI.md`**, **`.github/copilot-instructions.md`**) use a **managed section** between **``** and **``** (HTML comments — usually hidden in rendered Markdown): + +| Situation | Behavior | +| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| File missing | Write that section (with markers). | +| File exists, section present | **Replace only** that section — idempotent re-runs, no duplicate blocks; template updates fix **stale** text. | +| File exists, no section, but content looks like an **old** Codemap-only file | **Replace whole file** with the managed section (one-time migration). | +| File exists with other content (e.g. your team intro) | **Append** the managed section **once**. | +| **`--force`** | Replace the **entire file** with the latest managed section. | + +Append alone would duplicate on every run — markers + replace are what prevent duplicates and staleness. ## Implementation (for contributors) -| Source | Role | -| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`src/agents-init.ts`** | Copy **`templates/agents`** → **`.agents/`**, **`applyAgentsInitTargets`**, **`ensureGitignoreCodemapPattern`**, exported **`targetsNeedLinkMode`**. | -| **`src/agents-init-interactive.ts`** | **`@clack/prompts`** flow; calls **`runAgentsInit`**. | -| **`src/cli/cmd-agents.ts`** | Lazy-loaded from **`src/cli/main.ts`**. | +| Source | Role | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`src/agents-init.ts`** | **`runAgentsInit`**, **`upsertCodemapPointerFile`**, **`listRegularFilesRecursive`**, **`applyAgentsInitTargets`** (per-file **`copyFileSync`** / **`symlinkFilesGranular`**), **`ensureGitignoreCodemapPattern`**, **`targetsNeedLinkMode`**. | +| **`src/agents-init-interactive.ts`** | **`@clack/prompts`** flow; calls **`runAgentsInit`**. | +| **`src/cli/cmd-agents.ts`** | Lazy-loaded from **`src/cli/main.ts`**. | -Do **not** duplicate long IDE matrices in **README.md** or **packaging.md** — link **here** instead. +Do **not** duplicate long IDE matrices, **`--force`** / pointer behavior, or **`codemap-pointer`** details in **README.md** or **packaging.md** — link **here** instead. ## Related - [architecture.md](./architecture.md) — CLI chunks, layering. -- [.github/CONTRIBUTING.md](../.github/CONTRIBUTING.md) — Cursor symlink notes, **`main`** / PR workflow. +- [.github/CONTRIBUTING.md](../.github/CONTRIBUTING.md) — **`.agents/`** + **`.cursor/`** wiring, **`main`** / PR workflow. - [why-codemap.md](./why-codemap.md) — why SQL + index for agents. diff --git a/docs/architecture.md b/docs/architecture.md index d1995c15..c067e730 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -92,48 +92,34 @@ A local SQLite database (`.codemap.db`) indexes the project tree and stores stru ## Key Files -| File | Purpose | -| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| `index.ts` | Package entry — re-exports `api` / `config`, runs CLI when main | -| `cli/` | CLI — bootstrap argv, lazy command modules, `query` / `agents init` / index modes | -| `api.ts` | Programmatic API — `createCodemap`, `Codemap`, `runCodemapIndex` | -| `application/` | Indexing use cases and engine (`run-index`, `index-engine`, types) | -| `worker-pool.ts` | Parallel parse workers (Bun / Node) | -| `db.ts` | SQLite adapter — schema DDL, typed CRUD, connection management | -| `parser.ts` | TS/TSX/JS/JSX extraction via `oxc-parser` — symbols, imports, exports, components, markers | -| `css-parser.ts` | CSS extraction via `lightningcss` — custom properties, classes, keyframes, `@theme` blocks | -| `resolver.ts` | Import path resolution via `oxc-resolver` — respects `tsconfig` aliases, builds dependency graph | -| `constants.ts` | Shared constants — e.g. `LANG_MAP` | -| `glob-sync.ts` | Include globs — Bun `Glob` vs `fast-glob` on Node ([packaging § Node vs Bun](./packaging.md#node-vs-bun)) | -| `markers.ts` | Shared marker extraction (`TODO`/`FIXME`/`HACK`/`NOTE`) — used by all parsers | -| `parse-worker.ts` | Worker thread entry point — reads, parses, and extracts file data in parallel | -| `adapters/` | `LanguageAdapter` types and built-in TS/CSS/text implementations | -| `parsed-types.ts` | Shared `ParsedFile` shape for workers and adapters | -| `agents-init.ts` / `agents-init-interactive.ts` | `codemap agents init` — see [agents.md](./agents.md) (templates → `.agents/`, `--interactive`, `.gitignore`, IDE wiring) | -| `benchmark.ts` | SQL vs traditional timing script — see [benchmark.md § The benchmark script](./benchmark.md#the-benchmark-script) | -| `config.ts` | `codemap.config.*` load path, **Zod** user schema (`codemapUserConfigSchema`), `resolveCodemapConfig` | +| File | Purpose | +| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `index.ts` | Package entry — re-exports `api` / `config`, runs CLI when main | +| `cli/` | CLI — bootstrap argv, lazy command modules, `query` / `agents init` / index modes | +| `api.ts` | Programmatic API — `createCodemap`, `Codemap`, `runCodemapIndex` | +| `application/` | Indexing use cases and engine (`run-index`, `index-engine`, types) | +| `worker-pool.ts` | Parallel parse workers (Bun / Node) | +| `db.ts` | SQLite adapter — schema DDL, typed CRUD, connection management | +| `parser.ts` | TS/TSX/JS/JSX extraction via `oxc-parser` — symbols, imports, exports, components, markers | +| `css-parser.ts` | CSS extraction via `lightningcss` — custom properties, classes, keyframes, `@theme` blocks | +| `resolver.ts` | Import path resolution via `oxc-resolver` — respects `tsconfig` aliases, builds dependency graph | +| `constants.ts` | Shared constants — e.g. `LANG_MAP` | +| `glob-sync.ts` | Include globs — Bun `Glob` vs `fast-glob` on Node ([packaging § Node vs Bun](./packaging.md#node-vs-bun)) | +| `markers.ts` | Shared marker extraction (`TODO`/`FIXME`/`HACK`/`NOTE`) — used by all parsers | +| `parse-worker.ts` | Worker thread entry point — reads, parses, and extracts file data in parallel | +| `adapters/` | `LanguageAdapter` types and built-in TS/CSS/text implementations | +| `parsed-types.ts` | Shared `ParsedFile` shape for workers and adapters | +| `agents-init.ts` / `agents-init-interactive.ts` | `codemap agents init` — see [agents.md](./agents.md) (granular template + IDE writes, pointer upsert, **`--interactive`**, `.gitignore`) | +| `benchmark.ts` | SQL vs traditional timing script — see [benchmark.md § The benchmark script](./benchmark.md#the-benchmark-script) | +| `config.ts` | `codemap.config.*` load path, **Zod** user schema (`codemapUserConfigSchema`), `resolveCodemapConfig` | ## CLI usage -From an install: `codemap …`. From this repository: `bun src/index.ts …` (same flags). +**Commands and flags** (index, query, **`codemap agents init`**, **`--root`**, **`--config`**, environment): [../README.md § CLI](../README.md#cli). From this repository: **`bun run dev`** or **`bun src/index.ts`** (same flags). -```bash -# Targeted — re-index only listed paths (relative to project root) -codemap --files path/to/file1.tsx path/to/file2.ts +**Agent templates:** `codemap agents init` — full matrix [agents.md](./agents.md). -# Incremental — git-based change detection (or full rebuild when no safe baseline) -codemap - -# Full rebuild — drop and recreate index tables -codemap --full - -# Query the database (after indexing) -codemap query "SELECT name, file_path FROM symbols LIMIT 20" -``` - -**Agent templates:** `codemap agents init` — [agents.md](./agents.md). - -Timings and methodology: [benchmark.md](./benchmark.md). **Startup / Node vs Bun** (not the same as benchmark scenarios): [benchmark.md § CLI and runtime startup](./benchmark.md#cli-and-runtime-startup). +**Timings and methodology:** [benchmark.md](./benchmark.md). **Startup / Node vs Bun** (not the same as benchmark scenarios): [benchmark.md § CLI and runtime startup](./benchmark.md#cli-and-runtime-startup). ### Help, version, and invalid argv diff --git a/docs/benchmark.md b/docs/benchmark.md index 3559d440..085cbb5a 100644 --- a/docs/benchmark.md +++ b/docs/benchmark.md @@ -1,8 +1,8 @@ # Benchmarking & external project roots -See [README.md](./README.md) · [why-codemap.md](./why-codemap.md) (rationale for the index). +**Index:** [README.md](./README.md) · **Why an index:** [why-codemap.md](./why-codemap.md) -**Two different topics live here — pick the row that matches what you need:** +**Two topics — pick the row that matches what you need:** | You want to… | Read | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | @@ -47,7 +47,7 @@ Use **`CODEMAP_ROOT`** instead of **`CODEMAP_TEST_BENCH`** if you prefer; behavi 1. **Indexed** — single SQL query against `.codemap.db` 2. **Traditional** — glob (same implementation as the indexer — [packaging.md § Node vs Bun](./packaging.md#node-vs-bun)) → **`readFileSync`** → regex match (simulates what AI agent tools like Grep/Read/Glob do) -**OSS note:** For **repeatable** numbers, use **`fixtures/minimal/`** ([Fixtures](#fixtures)) or index your own app with **`CODEMAP_ROOT`**. Tables below may still use historical labels; methodology is the same. +For **repeatable** numbers, use **`fixtures/minimal/`** ([Fixtures](#fixtures)) or index your own app with **`CODEMAP_ROOT`** before running the script. ### Prerequisites @@ -184,4 +184,4 @@ bun run benchmark **CI:** the workflow **Benchmark (fixture)** runs the same steps with `CODEMAP_ROOT=$GITHUB_WORKSPACE/fixtures/minimal`. -Scenario **titles** in `src/benchmark.ts` are still generic (historical names); **indexed row counts** on the fixture are stable for a given schema. A second, larger fixture is optional — see [roadmap.md](./roadmap.md). +Scenario titles match the table above; **indexed row counts** on the fixture are stable for a given schema. A larger second fixture is optional — see [roadmap.md](./roadmap.md). diff --git a/docs/packaging.md b/docs/packaging.md index 8e13ecc0..9250cd5b 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -1,6 +1,6 @@ # Packaging -How **@stainless-code/codemap** is built and published. Hub: [README.md](./README.md). +How **@stainless-code/codemap** is built and published. **Doc index:** [README.md](./README.md). **Runtime comparison table:** [§ Node vs Bun](#node-vs-bun) — link from other docs; do not duplicate the table. ## Build & publish surface @@ -40,5 +40,6 @@ Releases use [**Changesets**](https://github.com/changesets/changesets). Repo co ## Related -- [architecture.md](./architecture.md) — schema, layering, API. -- [agents.md](./agents.md) — **`templates/agents`**, **`codemap agents init`**, published **`files`** surface above. +- [architecture.md](./architecture.md) — schema, layering, API, user config. +- [agents.md](./agents.md) — **`templates/agents`**, **`codemap agents init`** (CLI flags and file behavior documented there), published **`files`** surface above. +- [benchmark.md](./benchmark.md) — external roots, **`CODEMAP_ROOT`**, benchmark script. diff --git a/docs/roadmap.md b/docs/roadmap.md index ae95cfee..75979aa7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap -Forward-looking plans only — **not** a mirror of `src/`. **Hub:** [README.md](./README.md). **Design:** [architecture.md](./architecture.md), [packaging.md](./packaging.md). **Shipped features** (adapters, fixtures, `codemap agents init` — [agents.md](./agents.md)) live in `src/` and linked docs — not enumerated here. +Forward-looking plans only — **not** a mirror of `src/`. **Doc index:** [README.md](./README.md). **Design / ship:** [architecture.md](./architecture.md), [packaging.md](./packaging.md). **Shipped features** (adapters, fixtures, `codemap agents init` — [agents.md](./agents.md)) live in `src/` and linked docs — not enumerated here. --- diff --git a/docs/why-codemap.md b/docs/why-codemap.md index 4d511ee5..0c62185d 100644 --- a/docs/why-codemap.md +++ b/docs/why-codemap.md @@ -1,6 +1,6 @@ # Why Codemap -**Design:** [architecture.md](./architecture.md) · **Index:** [README.md](./README.md) +**Index:** [README.md](./README.md) · **Design:** [architecture.md](./architecture.md) ## The Problem diff --git a/src/agents-init.test.ts b/src/agents-init.test.ts index 89a3e988..55506f3a 100644 --- a/src/agents-init.test.ts +++ b/src/agents-init.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; import { existsSync, + lstatSync, mkdirSync, mkdtempSync, readFileSync, @@ -11,10 +12,14 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { + CODMAP_POINTER_BEGIN, + CODMAP_POINTER_END, ensureGitignoreCodemapPattern, + listRegularFilesRecursive, resolveAgentsTemplateDir, runAgentsInit, targetsNeedLinkMode, + upsertCodemapPointerFile, } from "./agents-init"; describe("runAgentsInit", () => { @@ -36,6 +41,51 @@ describe("runAgentsInit", () => { } }); + it("runAgentsInit with --force refreshes only template file paths; user files under rules/ and skills/ remain", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); + try { + mkdirSync(join(dir, ".agents", "rules"), { recursive: true }); + mkdirSync(join(dir, ".agents", "skills", "stale"), { recursive: true }); + writeFileSync(join(dir, ".agents", "USER_NOTES.md"), "keep me", "utf-8"); + writeFileSync( + join(dir, ".agents", "rules", "stale.txt"), + "user rule", + "utf-8", + ); + writeFileSync( + join(dir, ".agents", "skills", "stale", "SKILL.md"), + "user skill", + "utf-8", + ); + expect(runAgentsInit({ projectRoot: dir, force: true })).toBe(true); + expect(readFileSync(join(dir, ".agents", "USER_NOTES.md"), "utf-8")).toBe( + "keep me", + ); + expect( + readFileSync(join(dir, ".agents", "rules", "stale.txt"), "utf-8"), + ).toBe("user rule"); + expect( + readFileSync( + join(dir, ".agents", "skills", "stale", "SKILL.md"), + "utf-8", + ), + ).toBe("user skill"); + expect( + readFileSync(join(dir, ".agents", "rules", "codemap.mdc"), "utf-8"), + ).toContain("codemap"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("listRegularFilesRecursive matches bundled rules and skills files", () => { + const root = resolveAgentsTemplateDir(); + const rules = listRegularFilesRecursive(join(root, "rules")).sort(); + const skills = listRegularFilesRecursive(join(root, "skills")).sort(); + expect(rules).toContain("codemap.mdc"); + expect(skills).toContain("codemap/SKILL.md"); + }); + it("returns false when .agents exists without force", () => { const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); try { @@ -146,6 +196,46 @@ describe("runAgentsInit", () => { } }); + it("runAgentsInit with Cursor symlink creates per-file symlinks, not directory symlinks", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); + try { + expect( + runAgentsInit({ + projectRoot: dir, + force: true, + targets: ["cursor"], + linkMode: "symlink", + }), + ).toBe(true); + const rulesDir = join(dir, ".cursor", "rules"); + const skillsDir = join(dir, ".cursor", "skills"); + expect(lstatSync(rulesDir).isSymbolicLink()).toBe(false); + expect(lstatSync(skillsDir).isSymbolicLink()).toBe(false); + expect(lstatSync(rulesDir).isDirectory()).toBe(true); + expect(lstatSync(skillsDir).isDirectory()).toBe(true); + for (const rel of listRegularFilesRecursive( + join(dir, ".agents", "rules"), + )) { + expect( + lstatSync( + join(dir, ".cursor", "rules", ...rel.split("/")), + ).isSymbolicLink(), + ).toBe(true); + } + for (const rel of listRegularFilesRecursive( + join(dir, ".agents", "skills"), + )) { + expect( + lstatSync( + join(dir, ".cursor", "skills", ...rel.split("/")), + ).isSymbolicLink(), + ).toBe(true); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("runAgentsInit with claude-md writes CLAUDE.md", () => { const dir = mkdtempSync(join(tmpdir(), "codemap-agents-")); try { @@ -156,9 +246,10 @@ describe("runAgentsInit", () => { targets: ["claude-md"], }), ).toBe(true); - expect(readFileSync(join(dir, "CLAUDE.md"), "utf-8")).toContain( - "Codemap", - ); + const md = readFileSync(join(dir, "CLAUDE.md"), "utf-8"); + expect(md).toContain("Codemap"); + expect(md).toContain(CODMAP_POINTER_BEGIN); + expect(md).toContain(CODMAP_POINTER_END); } finally { rmSync(dir, { recursive: true, force: true }); } @@ -185,10 +276,10 @@ describe("runAgentsInit", () => { readFileSync(join(dir, ".github", "copilot-instructions.md"), "utf-8"), ).toContain("Copilot"); expect(readFileSync(join(dir, "AGENTS.md"), "utf-8")).toContain( - "Codemap", + CODMAP_POINTER_BEGIN, ); expect(readFileSync(join(dir, "GEMINI.md"), "utf-8")).toContain( - "Codemap", + CODMAP_POINTER_BEGIN, ); } finally { rmSync(dir, { recursive: true, force: true }); @@ -232,3 +323,91 @@ describe("runAgentsInit", () => { } }); }); + +/** Minimal inner block for pointer tests (matches legacy migration heuristic). */ +const POINTER_INNER_TEST = `# Codemap + +This project uses [Codemap](https://github.com/stainless-code/codemap) — test. + +- **Skill:** \`.agents/skills/codemap/SKILL.md\` +- **CLI:** \`codemap query "SELECT 1"\` for SQL +- **Rules:** \`.agents/rules/\` +`; + +function wrapPointerTest(inner: string): string { + return `${CODMAP_POINTER_BEGIN}\n${inner.trim()}\n${CODMAP_POINTER_END}\n`; +} + +describe("upsertCodemapPointerFile", () => { + it("appends managed section to existing non-Codemap file", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-")); + const p = join(dir, "AGENTS.md"); + try { + writeFileSync(p, "# Team\n\nOur project.\n", "utf-8"); + upsertCodemapPointerFile(p, POINTER_INNER_TEST, "AGENTS.md", false); + const out = readFileSync(p, "utf-8"); + expect(out).toContain("# Team"); + expect(out).toContain("Our project."); + expect(out).toContain(CODMAP_POINTER_BEGIN); + expect(out.indexOf("# Team")).toBeLessThan( + out.indexOf(CODMAP_POINTER_BEGIN), + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("replaces managed section in place on second run (no duplicate blocks)", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-")); + const p = join(dir, "NOTE.md"); + try { + writeFileSync(p, wrapPointerTest("FIRST"), "utf-8"); + upsertCodemapPointerFile( + p, + "SECOND\n\nstill https://github.com/stainless-code/codemap\n`.agents/skills/codemap`\n`codemap query`", + "NOTE.md", + false, + ); + const out = readFileSync(p, "utf-8"); + expect(out).toContain("SECOND"); + expect(out).not.toContain("FIRST"); + expect(out.match(new RegExp(CODMAP_POINTER_BEGIN, "g"))?.length).toBe(1); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("migrates legacy unmarked Codemap pointer file to managed section", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-")); + const p = join(dir, "CLAUDE.md"); + try { + writeFileSync(p, POINTER_INNER_TEST, "utf-8"); + upsertCodemapPointerFile( + p, + `${POINTER_INNER_TEST}\n\n## Extra\n\nMigrated.\n`, + "CLAUDE.md", + false, + ); + const out = readFileSync(p, "utf-8"); + expect(out).toContain(CODMAP_POINTER_BEGIN); + expect(out).toContain("Migrated."); + expect(out).not.toContain(`${POINTER_INNER_TEST}\n${POINTER_INNER_TEST}`); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("--force replaces entire file with managed section", () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-")); + const p = join(dir, "AGENTS.md"); + try { + writeFileSync(p, "# Keep me\n\nLots of custom content.\n", "utf-8"); + upsertCodemapPointerFile(p, POINTER_INNER_TEST, "AGENTS.md", true); + expect(readFileSync(p, "utf-8")).toBe( + wrapPointerTest(POINTER_INNER_TEST), + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/agents-init.ts b/src/agents-init.ts index 9edb15fd..7ded8a11 100644 --- a/src/agents-init.ts +++ b/src/agents-init.ts @@ -1,10 +1,12 @@ import { appendFileSync, - cpSync, + copyFileSync, existsSync, mkdirSync, + readdirSync, readFileSync, rmSync, + statSync, symlinkSync, writeFileSync, } from "node:fs"; @@ -23,12 +25,89 @@ export function resolveAgentsTemplateDir(): string { ); } +/** + * Every regular file path under `dir` relative to `dir` (POSIX-style `/`). + * Used for template paths (`--force` removal), template writes, and copy-mode IDE sync. + */ +export function listRegularFilesRecursive( + dir: string, + relPrefix = "", +): string[] { + const out: string[] = []; + if (!existsSync(dir)) { + return out; + } + for (const ent of readdirSync(dir, { withFileTypes: true })) { + const name = ent.name; + const rel = relPrefix ? `${relPrefix}/${name}` : name; + const full = join(dir, name); + if (ent.isDirectory()) { + out.push(...listRegularFilesRecursive(full, rel)); + } else if (ent.isFile()) { + out.push(rel); + } + } + return out; +} + +function relPathToAbsSegments(rel: string): string[] { + return rel.split("/").filter(Boolean); +} + +/** Copy only listed relative paths from `srcRoot` into `destRoot` (mkdir parents per file). */ +function copyFilesGranular( + srcRoot: string, + destRoot: string, + relPaths: string[], +): void { + for (const rel of relPaths) { + const from = join(srcRoot, ...relPathToAbsSegments(rel)); + const to = join(destRoot, ...relPathToAbsSegments(rel)); + mkdirSync(dirname(to), { recursive: true }); + copyFileSync(from, to); + } +} + +/** Symlink each file: `destRoot/` → relative path to `srcRoot/` (mkdir parents per file). */ +function symlinkFilesGranular( + srcRoot: string, + destRoot: string, + relPaths: string[], + labelForErrors: string, +): void { + mkdirSync(destRoot, { recursive: true }); + for (const rel of relPaths) { + const srcFile = join(srcRoot, ...relPathToAbsSegments(rel)); + const destFile = join(destRoot, ...relPathToAbsSegments(rel)); + mkdirSync(dirname(destFile), { recursive: true }); + const target = relative(dirname(destFile), srcFile); + try { + symlinkSync(target, destFile, "file"); + } catch (err) { + throw new Error( + `Codemap: symlink failed for ${labelForErrors} (${destFile}): ${String(err)}. Try copy mode or check permissions on Windows.`, + { cause: err }, + ); + } + } +} + +function removeBundledPathsIfExist(destBase: string, relPaths: string[]): void { + for (const rel of relPaths) { + const abs = join(destBase, ...relPathToAbsSegments(rel)); + if (!existsSync(abs)) { + continue; + } + rmSync(abs, { recursive: true, force: true }); + } +} + /** Default DB basename `.codemap` plus SQLite sidecars (`.db`, `-wal`, `-shm`, …). */ const GITIGNORE_CODEMAP_PATTERN = ".codemap.*"; /** * Optional integrations after canonical `.agents/` is written. - * - Symlink/copy: `cursor`, `windsurf`, `continue`, `cline`, `amazon-q` (rules → `.agents/rules`; Cursor also maps skills). + * - Symlink/copy: `cursor`, `windsurf`, `continue`, `cline`, `amazon-q` (per-file symlinks or copies from `.agents/rules`; Cursor also `.agents/skills`). * - Pointer files: `copilot`, `claude-md`, `agents-md`, `gemini-md`. */ export type AgentsInitTarget = @@ -42,7 +121,7 @@ export type AgentsInitTarget = | "agents-md" | "gemini-md"; -/** Targets that mirror `.agents/rules` (and Cursor also `.agents/skills`) via symlink or copy. */ +/** Targets that mirror `.agents/rules` (and Cursor also `.agents/skills`) via per-file symlink or copy. */ export const AGENTS_INIT_SYMLINK_TARGETS: readonly AgentsInitTarget[] = [ "cursor", "windsurf", @@ -55,7 +134,7 @@ export function targetsNeedLinkMode(targets: AgentsInitTarget[]): boolean { return targets.some((t) => AGENTS_INIT_SYMLINK_TARGETS.includes(t)); } -/** How symlink-style integrations receive `.agents` rules (and Cursor skills). */ +/** Per-file symlinks vs full file copies into IDE paths. */ export type AgentsInitLinkMode = "symlink" | "copy"; const POINTER_BODY = `This project uses [Codemap](https://github.com/stainless-code/codemap) — a structural SQLite index for AI agents. @@ -89,10 +168,96 @@ See [GitHub Docs: custom instructions for Copilot](https://docs.github.com/copil `; +/** HTML comments — invisible in most Markdown renderers; used to upsert without duplicating on re-run. */ +export const CODMAP_POINTER_BEGIN = ""; +export const CODMAP_POINTER_END = ""; + +function wrapCodemapPointerBlock(inner: string): string { + return `${CODMAP_POINTER_BEGIN}\n${inner.trim()}\n${CODMAP_POINTER_END}\n`; +} + +function escapeRegexChars(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function codemapPointerBlockRegex(): RegExp { + return new RegExp( + `${escapeRegexChars(CODMAP_POINTER_BEGIN)}\\s*[\\s\\S]*?${escapeRegexChars(CODMAP_POINTER_END)}`, + "m", + ); +} + +/** Heuristic: file looks like a prior Codemap pointer file before we added markers (upgrade → single managed block). */ +function looksLikeLegacyCodemapPointer(content: string): boolean { + const t = content.trim(); + if (t.length < 80) { + return false; + } + return ( + t.includes("stainless-code/codemap") && + t.includes(".agents/skills/codemap") && + t.includes("codemap query") + ); +} + +/** + * Create or merge a Codemap pointer file. Idempotent: managed section is between + * {@link CODMAP_POINTER_BEGIN} / {@link CODMAP_POINTER_END}; re-runs replace that section only. + * - **No file:** write managed block. + * - **Existing + markers:** replace inner section (updates stale template text). + * - **Existing, no markers, legacy Codemap content:** replace whole file with managed block. + * - **Existing, other content:** append managed block once. + * - **`force`:** replace entire file with the latest managed block (same as a fresh write). + */ +export function upsertCodemapPointerFile( + path: string, + innerTemplate: string, + label: string, + force: boolean, +): void { + const wrapped = wrapCodemapPointerBlock(innerTemplate); + + if (!existsSync(path)) { + writeFileSync(path, wrapped, "utf-8"); + console.log(` Wrote ${label} with Codemap pointers`); + return; + } + + if (force) { + writeFileSync(path, wrapped, "utf-8"); + console.log(` Replaced ${label} (--force)`); + return; + } + + const content = readFileSync(path, "utf-8"); + const re = codemapPointerBlockRegex(); + + if (content.match(re)) { + const next = content.replace(re, wrapped); + if (next === content) { + console.log(` Codemap section in ${label} already up to date`); + return; + } + writeFileSync(path, next, "utf-8"); + console.log(` Updated Codemap section in ${label}`); + return; + } + + if (looksLikeLegacyCodemapPointer(content)) { + writeFileSync(path, wrapped, "utf-8"); + console.log(` Migrated ${label} to managed Codemap section`); + return; + } + + const sep = content.endsWith("\n") ? "\n" : "\n\n"; + writeFileSync(path, content + sep + wrapped, "utf-8"); + console.log(` Appended Codemap section to ${label}`); +} + export interface AgentsInitOptions { /** Project root (`.agents/` is created here). */ projectRoot: string; - /** Overwrite existing files. */ + /** When `.agents/` exists, replace only files that ship in `templates/agents` (and allow integration overwrites per target). */ force?: boolean; /** Extra tool integrations (after `.agents/` is written). */ targets?: AgentsInitTarget[]; @@ -166,19 +331,15 @@ function wireAgentsRulesTo( mkdirSync(dirname(destPath), { recursive: true }); removePathForRewrite(destPath, force, label); if (linkMode === "symlink") { - const rel = relative(dirname(destPath), agentsRules); - try { - symlinkSync(rel, destPath, "dir"); - } catch (err) { - throw new Error( - `Codemap: symlink failed for ${label} (${String(err)}). Try copy mode or check permissions on Windows.`, - { cause: err }, - ); - } - console.log(` Linked ${label} → .agents/rules`); + const ruleFiles = listRegularFilesRecursive(agentsRules); + symlinkFilesGranular(agentsRules, destPath, ruleFiles, label); + console.log( + ` Linked each file under ${label} → .agents/rules (${ruleFiles.length} files)`, + ); return; } - cpSync(agentsRules, destPath, { recursive: true }); + const ruleFiles = listRegularFilesRecursive(agentsRules); + copyFilesGranular(agentsRules, destPath, ruleFiles); console.log(` Copied .agents/rules → ${label}`); } @@ -241,7 +402,7 @@ export function applyAgentsInitTargets( ); break; case "claude-md": - writePointerFile( + upsertCodemapPointerFile( join(projectRoot, "CLAUDE.md"), CLAUDE_MD_TEMPLATE, "CLAUDE.md", @@ -250,7 +411,7 @@ export function applyAgentsInitTargets( break; case "copilot": mkdirSync(join(projectRoot, ".github"), { recursive: true }); - writePointerFile( + upsertCodemapPointerFile( join(projectRoot, ".github", "copilot-instructions.md"), COPILOT_TEMPLATE, ".github/copilot-instructions.md", @@ -258,7 +419,7 @@ export function applyAgentsInitTargets( ); break; case "agents-md": - writePointerFile( + upsertCodemapPointerFile( join(projectRoot, "AGENTS.md"), AGENTS_MD_TEMPLATE, "AGENTS.md", @@ -266,7 +427,7 @@ export function applyAgentsInitTargets( ); break; case "gemini-md": - writePointerFile( + upsertCodemapPointerFile( join(projectRoot, "GEMINI.md"), GEMINI_MD_TEMPLATE, "GEMINI.md", @@ -277,22 +438,6 @@ export function applyAgentsInitTargets( } } -function writePointerFile( - path: string, - content: string, - label: string, - force: boolean, -): void { - if (existsSync(path) && !force) { - console.warn( - ` Skipped ${label} (file exists). Use --force to overwrite, or merge manually.`, - ); - return; - } - writeFileSync(path, content, "utf-8"); - console.log(` Wrote ${label} with Codemap pointers`); -} - function applyCursorIntegration( projectRoot: string, linkMode: AgentsInitLinkMode, @@ -308,34 +453,41 @@ function applyCursorIntegration( if (linkMode === "symlink") { removePathForRewrite(cursorRules, force, ".cursor/rules"); removePathForRewrite(cursorSkills, force, ".cursor/skills"); - const relRules = relative(dirname(cursorRules), agentsRules); - const relSkills = relative(dirname(cursorSkills), agentsSkills); - try { - symlinkSync(relRules, cursorRules, "dir"); - symlinkSync(relSkills, cursorSkills, "dir"); - } catch (err) { - throw new Error( - `Codemap: symlink failed for Cursor integration (${String(err)}). Try re-running with copy mode or check permissions on Windows.`, - { cause: err }, - ); - } + const ruleFiles = listRegularFilesRecursive(agentsRules); + const skillFiles = listRegularFilesRecursive(agentsSkills); + symlinkFilesGranular(agentsRules, cursorRules, ruleFiles, ".cursor/rules"); + symlinkFilesGranular( + agentsSkills, + cursorSkills, + skillFiles, + ".cursor/skills", + ); console.log( - " Linked .cursor/rules → .agents/rules and .cursor/skills → .agents/skills", + ` Linked ${ruleFiles.length} rule file(s) and ${skillFiles.length} skill file(s) under .cursor/ → .agents/`, ); return; } removePathForRewrite(cursorRules, force, ".cursor/rules"); removePathForRewrite(cursorSkills, force, ".cursor/skills"); - cpSync(agentsRules, cursorRules, { recursive: true }); - cpSync(agentsSkills, cursorSkills, { recursive: true }); + copyFilesGranular( + agentsRules, + cursorRules, + listRegularFilesRecursive(agentsRules), + ); + copyFilesGranular( + agentsSkills, + cursorSkills, + listRegularFilesRecursive(agentsSkills), + ); console.log( " Copied rules and skills into .cursor/rules and .cursor/skills", ); } /** - * Copy bundled rules and skills into `/.agents/`, optional integrations, `.gitignore` hint. + * Copy bundled `rules/` and `skills/` into `/.agents/`, optional integrations, `.gitignore` hint. + * **`--force`** deletes only template-backed files, then writes those files again with per-file copies — your other files under **`.agents/`**, **`rules/`**, or **`skills/`** stay. * @returns `false` when `.agents/` exists and `--force` was not used. */ export function runAgentsInit(options: AgentsInitOptions): boolean { @@ -346,21 +498,35 @@ export function runAgentsInit(options: AgentsInitOptions): boolean { ); } + const templateRules = join(templateRoot, "rules"); + const templateSkills = join(templateRoot, "skills"); + const bundledRuleFiles = listRegularFilesRecursive(templateRules); + const bundledSkillFiles = listRegularFilesRecursive(templateSkills); + const destRoot = join(options.projectRoot, ".agents"); - if (existsSync(destRoot) && !options.force) { - console.error( - ` .agents/ already exists at ${destRoot}. Re-run with --force to overwrite, or remove the directory.`, - ); - return false; + const destRules = join(destRoot, "rules"); + const destSkills = join(destRoot, "skills"); + + if (existsSync(destRoot)) { + if (!statSync(destRoot).isDirectory()) { + throw new Error( + `Codemap: ${destRoot} exists but is not a directory — remove or rename it, then retry.`, + ); + } + if (!options.force) { + console.error( + ` .agents/ already exists at ${destRoot}. Re-run with --force to refresh bundled template files under rules/ and skills/, or remove the directory.`, + ); + return false; + } + removeBundledPathsIfExist(destRules, bundledRuleFiles); + removeBundledPathsIfExist(destSkills, bundledSkillFiles); + } else { + mkdirSync(destRoot, { recursive: true }); } - mkdirSync(destRoot, { recursive: true }); - cpSync(join(templateRoot, "rules"), join(destRoot, "rules"), { - recursive: true, - }); - cpSync(join(templateRoot, "skills"), join(destRoot, "skills"), { - recursive: true, - }); + copyFilesGranular(templateRules, destRules, bundledRuleFiles); + copyFilesGranular(templateSkills, destSkills, bundledSkillFiles); console.log(` Wrote agent templates to ${destRoot}`); diff --git a/src/cli.test.ts b/src/cli.test.ts index 4568c91d..bfc53e42 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -76,4 +76,11 @@ describe("CLI unknown / invalid args", () => { expect(exitCode).toBe(1); expect(err).toContain("unexpected argument"); }); + + test("agents init rejects positional argument (use --interactive)", async () => { + const { exitCode, err } = await runCli(["agents", "init", "interactive"]); + expect(exitCode).toBe(1); + expect(err).toContain("unexpected argument"); + expect(err).toContain("interactive"); + }); }); diff --git a/src/cli/main.ts b/src/cli/main.ts index 1798ef0e..af090e45 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -29,7 +29,7 @@ export async function main(): Promise { console.log(`Usage: codemap agents init [--force] [--interactive|-i] Copies bundled agent templates into .agents/ under the project root. - --force Overwrite an existing .agents/ directory + --force Refresh only files that ship in templates/agents (merge into rules/ & skills/) --interactive Pick IDEs (Cursor, Copilot, Windsurf, …) and symlink vs copy `); return; @@ -43,11 +43,16 @@ Copies bundled agent templates into .agents/ under the project root. "-h", ]); for (const a of initRest) { - if (a.startsWith("-") && !knownInit.has(a)) { + if (knownInit.has(a)) { + continue; + } + if (a.startsWith("-")) { console.error(`codemap: unknown option "${a}"`); - console.error("Run codemap agents init --help for usage."); - process.exit(1); + } else { + console.error(`codemap: unexpected argument "${a}"`); } + console.error("Run codemap agents init --help for usage."); + process.exit(1); } const { runAgentsInitCmd } = await import("./cmd-agents.js"); const ok = await runAgentsInitCmd({