fix(skills): gitignore .env.local on the link/create path
Closes #289.
ai setup already gitignores .env.local through ensureLocalEnvIgnored(). link and create write credentials to the same file and never call it. This routes them through the same helper instead of adding a second implementation.
What changed
ensureLocalEnvIgnored and its isLocalEnvFile companion move from src/commands/ai/setup.ts into src/lib/env-writer.ts, next to upsertEnvFile. That pairs the two halves of the same job: upsertEnvFile writes the secret, ensureLocalEnvIgnored keeps it out of git.
The move is what makes the reuse possible. src/lib/ imports from src/commands/ nowhere in the tree today, so calling the helper from skills.ts at its old address would have inverted the layering. setup.ts imports it from the new location and re-exports it, so ./setup.js stays a valid import path and setup.test.ts needs no changes.
updateGitignore() in src/lib/skills.ts then calls ensureLocalEnvIgnored(process.cwd(), '.env.local') after its existing GITIGNORE_ENTRIES append. Two details there, both easy to get wrong:
- The early
returnwhen no agent entries are missing became anifblock. Without that, a project that already had all thirteen agent directories in.gitignorewould skip the env check entirely, which is exactly the repeat-linkcase. - The two writes stay separate, under their own headers (
# InsForge & AI agent skillsand# Local environment secrets). That matches what a user who runs bothlinkandai setupalready ends up with today.
updateGitignore is now exported. It was internal, and the behavior above is worth testing directly rather than through installSkills, which shells out to npx.
Why reuse instead of adding entries to GITIGNORE_ENTRIES
Adding .env.local to that array looked like the one-line version, and it is worse in two ways. updateGitignore matches with lines.has(entry), an exact match on trimmed lines, so a project whose .gitignore already says .env* or .env.* would get a redundant .env.local appended. And it would leave two different answers in the tree to the same question. ensureLocalEnvIgnored already checks .env*, .env.*, .env*.local, and .env.local before writing, and already refuses paths that resolve outside cwd.
Scope
Only .env*.local, which is what the helper already writes and what this CLI actually creates. Bare .env is left alone: some projects commit a non-secret one deliberately, and that call belongs to you rather than to this PR. Say the word if you want it included and I will add it here.
Tests
npm run lint passes: 830 tests, eslint clean, and npm run build succeeds.
The existing ensureLocalEnvIgnored coverage in src/commands/ai/setup.test.ts is untouched and still passing, which is the signal that the move is behavior-preserving. Four cases added to src/lib/skills.test.ts cover the new path:
.env.localis ignored alongside the agent directories- no second env pattern is added when
.env*already covers the file .env.localis still ignored when every agent entry is already present, the case the earlyreturnused to swallow- repeated runs are idempotent
Summary by cubic
Closes #289. link and create write credentials to .env.local but never gitignore it, unlike ai setup. This routes them through the same ensureLocalEnvIgnored helper so the file stays out of git.
- Moves
ensureLocalEnvIgnoredandisLocalEnvFiletosrc/lib/env-writer.ts, next to the writer that creates the file;setup.tsre-exports them to keep imports valid. updateGitignore()now calls the helper after appending agent entries, and its early return became anifso the env check runs even when all agent entries are already present — the repeat-linkcase.- Reuses the helper instead of adding
.env.localto the entries list: the exact-match check would append a redundant entry when.env*or.env.*already covers it. - Leaves bare
.envalone; only.env*.localfiles are handled. - Exports
updateGitignoreso the behavior is testable directly; tests verify the ignore rule, dedup, idempotency, and the all-agent-entries-present case.
Summary by CodeRabbit
-
Bug Fixes
- Local environment files are now consistently protected from accidental Git commits.
- Gitignore updates remain safe to run repeatedly without creating duplicate entries.
- Environment-file protections are added even when other required Gitignore entries already exist.
-
Tests
- Added coverage for Gitignore updates, including existing entries, missing entries, and repeated runs.