diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5214e15..113d334 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - name: Security policy - url: https://github.com/backslash-ux/plane-cli-cli/blob/main/SECURITY.md + url: https://github.com/backslash-ux/plane-cli/blob/main/SECURITY.md about: Report suspected vulnerabilities privately instead of opening a public issue. - name: Contributing guide - url: https://github.com/backslash-ux/plane-cli-cli/blob/main/CONTRIBUTING.md + url: https://github.com/backslash-ux/plane-cli/blob/main/CONTRIBUTING.md about: Review contribution expectations, quality gates, and documentation requirements first. - name: Existing issues - url: https://github.com/backslash-ux/plane-cli-cli/issues + url: https://github.com/backslash-ux/plane-cli/issues about: Check open issues and feature requests before filing a new one. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b5185..43a8efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,25 @@ Earlier project history may predate this file. ## [Unreleased] +## 1.1.0 + +### Added + +- `plane modules create` with optional description, status, schedule, and lead resolution. +- `plane init --local` now prompts whether to import the SKILL.md CLI usage guide into AGENTS.md. First-time prompt defaults to `N`; subsequent runs (section already present) default to `Y`. The skill section is wrapped in idempotent HTML comment markers so repeated runs update it in place. + +### Changed + +- **Consistent project defaulting for create commands.** `issue create`, `modules create`, `labels create`, and `pages create` now use `--title`/`--name` options instead of positional args, so the project positional can be omitted to use the saved current project. +- `hasSkillSectionInAgentsFile` now requires both the start and end delimiters to be present before treating an existing skill section as complete, preventing duplicate sections in malformed files. + +### Validated + +- `plane init --local` skill import prompt exercised: accept (`y`), decline (empty/default N), and idempotent re-run paths all verified via tests. +- All 261 tests pass with line and function coverage above the 95% threshold. + +## 1.0.0 + ### Added - Public open-source repository baseline with contributor, governance, security, architecture, and release documentation. diff --git a/README.md b/README.md index 71dc7e2..0c9cef6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # plane -[](https://github.com/backslash-ux/plane-cli-cli/actions/workflows/ci.yml) [](./LICENSE) +[](https://github.com/backslash-ux/plane-cli/actions/workflows/ci.yml) [](./LICENSE) CLI for the [Plane](https://plane.so) project management API. @@ -103,8 +103,8 @@ plane issues list plane issues list PROJ plane issues list PROJ --state started plane issue get PROJ-29 -plane issue create PROJ "Title" -plane issue create @current "Title" +plane issue create --title "Title" +plane issue create --title "Title" PROJ plane issue update --state completed --priority high PROJ-29 plane issue delete PROJ-29 @@ -134,6 +134,7 @@ plane cycles issues add PROJ CYCLE_ID PROJ-29 # Modules plane modules list PROJ +plane modules create --name "Sprint 3" plane modules delete PROJ MODULE_ID plane modules issues list PROJ MODULE_ID plane modules issues add PROJ MODULE_ID PROJ-29 @@ -179,6 +180,8 @@ plane cycles list PROJ --json - `--description` for issue and page create or update commands is sent through to Plane as HTML in `description_html`. - `plane issue link add` accepts an optional link title via `--title`. - `plane labels delete` accepts either the label UUID or the exact label name returned by `plane labels list`. +- `plane modules create --lead` accepts a member display name, email, or UUID from `plane members list`. +- `plane modules create --status in_progress` is normalized to Plane's `in-progress` API value. - `plane modules delete` accepts either the module UUID or the exact module name returned by `plane modules list`. - `plane modules issues remove` expects the module-issue identifier returned by `plane modules issues list`, not an issue ref. - `plane members list` is workspace-scoped and does not take a project argument. @@ -197,7 +200,7 @@ bun update -g @backslash-ux/plane-cli ## Development ```bash -git clone https://github.com/backslash-ux/plane-cli-cli +git clone https://github.com/backslash-ux/plane-cli cd plane-cli bun install diff --git a/SKILL.md b/SKILL.md index d37a5d8..d8374cc 100644 --- a/SKILL.md +++ b/SKILL.md @@ -144,12 +144,12 @@ plane issue get PROJ-29 ### Create ```bash -plane issue create PROJ "Issue title" -plane issue create @current "Issue title" -plane issue create --priority high --state started PROJ "Fix lint pipeline" -plane issue create --description '
Detailed context
' PROJ "Add dark mode" -plane issue create --assignee "Jane Doe" PROJ "Onboarding bug" -plane issue create --label "bug" PROJ "Regression in login flow" +plane issue create --title "Issue title" +plane issue create --title "Issue title" PROJ +plane issue create --priority high --state started --title "Fix lint pipeline" +plane issue create --description 'Detailed context
' --title "Add dark mode" PROJ +plane issue create --assignee "Jane Doe" --title "Onboarding bug" PROJ +plane issue create --label "bug" --title "Regression in login flow" PROJ ``` ### Update @@ -233,8 +233,8 @@ State IDs are UUIDs unique per project. Always fetch live — never hardcode. plane labels list plane labels list PROJ plane labels list PROJ --xml -plane labels create PROJ "bug" -plane labels create --color "#ff0000" PROJ "critical" +plane labels create --name "bug" +plane labels create --name "critical" --color "#ff0000" PROJ plane labels delete PROJ bug ``` @@ -271,6 +271,7 @@ Cycle IDs are UUIDs. Fetch them from `plane cycles list PROJ`. plane modules list plane modules list PROJ plane modules list PROJ --xml +plane modules create --name "Sprint 3" plane modules delete PROJContent here
' PROJ plane pages update --name "New Title" PROJNew content
' PROJDetails
` when you want formatted output. +- `plane modules create --lead` accepts a member display name, email, or UUID from `plane members list`. +- `plane modules create --status in_progress` is normalized to Plane's `in-progress` API value. - Always fetch state/label/member IDs live — never hardcode UUIDs across workspaces. - `plane issue get PROJ-N` is the fastest way to inspect all fields on a single issue. diff --git a/package.json b/package.json index e0318f9..24db4e6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.0.0", + "version": "1.1.0", "description": "CLI for the Plane project management API", "author": "Gabriel Reynold and Contributors", "license": "MIT", @@ -33,12 +33,12 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/backslash-ux/plane-cli-cli.git" + "url": "git+https://github.com/backslash-ux/plane-cli.git" }, "bugs": { - "url": "https://github.com/backslash-ux/plane-cli-cli/issues" + "url": "https://github.com/backslash-ux/plane-cli/issues" }, - "homepage": "https://github.com/backslash-ux/plane-cli-cli#readme", + "homepage": "https://github.com/backslash-ux/plane-cli#readme", "engines": { "bun": ">=1.0.0" }, diff --git a/src/app.ts b/src/app.ts index 34a7c53..1fae5dc 100644 --- a/src/app.ts +++ b/src/app.ts @@ -36,8 +36,9 @@ QUICK START plane issues list List issues for the saved current project plane issues list PROJ List issues for a project plane issue get PROJ-29 Get full JSON for an issue - plane issue create PROJ "title" Create an issue - plane issue create @current "title" Create an issue in the saved current project + plane issue create --title "title" Create an issue in the saved current project + plane issue create --title "title" PROJ + plane modules create --name "Sprint 3" plane issue update --state done PROJ-29 plane issue comment PROJ-29 "text" Add a comment @@ -55,7 +56,7 @@ ALL SUBCOMMANDS issue get | create | update | delete | comment | activity | link | comments | worklogs cycles list | issues (list, add) - modules list | delete | issues (list, add, remove) + modules list | create | delete | issues (list, add, remove) intake list | accept | reject pages list | get | create | update | delete | archive | unarchive | lock | unlock | duplicate states list List workflow states for a project @@ -94,5 +95,5 @@ FOR AI AGENTS / BOTS export const cli = Command.run(plane, { name: "plane", - version: "1.0.0", + version: "1.1.0", }); diff --git a/src/commands/init.ts b/src/commands/init.ts index 98b5e4a..8356818 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -13,6 +13,9 @@ import { } from "../config.js"; import { getLocalAgentsFilePath, + hasSkillSectionInAgentsFile, + importSkillIntoAgentsFile, + readPackageSkillContent, writeLocalProjectAgentsFile, } from "../project-agents.js"; import { @@ -545,6 +548,34 @@ export function initHandler( yield* Console.log(" Estimate: disabled"); } yield* Console.log(`Local AGENTS.md updated at ${agentsPath}`); + + const skillContent = readPackageSkillContent(); + if (skillContent) { + const alreadyHasSkill = hasSkillSectionInAgentsFile(); + const skillPromptText = alreadyHasSkill + ? "Update SKILL.md (CLI usage guide) in AGENTS.md? [Y/n]: " + : "Import SKILL.md (CLI usage guide) into AGENTS.md? [y/N]: "; + const skillRl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + let skillAnswer: string; + try { + skillAnswer = yield* Effect.promise(() => + prompt(skillRl, skillPromptText), + ); + } finally { + skillRl.close(); + } + const trimmed = skillAnswer.trim().toLowerCase(); + const shouldImport = alreadyHasSkill + ? trimmed !== "n" && trimmed !== "no" + : trimmed === "y" || trimmed === "yes"; + if (shouldImport) { + importSkillIntoAgentsFile(skillContent); + yield* Console.log(" SKILL.md imported into AGENTS.md"); + } + } } else { yield* Console.log( `\nWarning: could not load project helper data for ${selectedProject.identifier}: ${projectHelper.left.message}`, @@ -569,6 +600,6 @@ export const localInit = Command.make("init", {}, () => initHandler({ global: false, local: true }, "local"), ).pipe( Command.withDescription( - "Interactive local setup. Saves overrides to ./.plane/config.json in the current directory, reports project feature flags, writes a local project helper snapshot for states, labels, and estimate points, and updates AGENTS.md with project-context guidance for AI agents.", + "Interactive local setup. Saves overrides to ./.plane/config.json in the current directory, reports project feature flags, writes a local project helper snapshot for states, labels, and estimate points, updates AGENTS.md with project-context guidance for AI agents, and optionally imports the SKILL.md CLI usage guide into AGENTS.md.", ), ); diff --git a/src/commands/issue.ts b/src/commands/issue.ts index 8f3efef..eb1c52c 100644 --- a/src/commands/issue.ts +++ b/src/commands/issue.ts @@ -204,13 +204,14 @@ export const issueComment = Command.make( ), ); // --- issue create --- -const titleArg = Args.text({ name: "title" }).pipe( - Args.withDescription("Issue title"), +const createTitleOption = Options.text("title").pipe( + Options.withDescription("Issue title"), ); -const projectRefArg = Args.text({ name: "project" }).pipe( +const createProjectArg = Args.text({ name: "project" }).pipe( Args.withDescription( - "Project identifier (e.g. PROJ). Use '@current' for the saved default project.", + "Project identifier (e.g. PROJ). Omit to use the saved current project.", ), + Args.withDefault(""), ); const createPriorityOption = Options.optional( @@ -285,13 +286,13 @@ export const issueCreate = Command.make( description: createDescriptionOption, assignee: createAssigneeOption, label: createLabelOption, - project: projectRefArg, - title: titleArg, + title: createTitleOption, + project: createProjectArg, }, issueCreateHandler, ).pipe( Command.withDescription( - 'Create a new issue in a project. Use @current to target the saved default project.\n\nExamples:\n plane issue create PROJ "Migrate Button component"\n plane issue create @current "Migrate Button component"\n plane issue create --priority high --state started PROJ "Fix lint pipeline"\n plane issue create --description "Detailed context here" PROJ "Add dark mode"\n plane issue create --assignee "Jane Doe" PROJ "Onboarding bug"', + 'Create a new issue in a project. Omit PROJECT to use the saved current project.\n\nExamples:\n plane issue create --title "Migrate Button component"\n plane issue create --title "Migrate Button component" PROJ\n plane issue create --priority high --state started --title "Fix lint pipeline"\n plane issue create --description "Detailed context here" --title "Add dark mode" PROJ\n plane issue create --assignee "Jane Doe" --title "Onboarding bug" PROJ', ), ); // --- issue activity --- diff --git a/src/commands/labels.ts b/src/commands/labels.ts index 55397d9..48e46ea 100644 --- a/src/commands/labels.ts +++ b/src/commands/labels.ts @@ -1,5 +1,5 @@ import { Args, Command, Options } from "@effect/cli"; -import { Console, Effect } from "effect"; +import { Console, Effect, Option } from "effect"; import { api, decodeOrFail } from "../api.js"; import { LabelSchema, LabelsResponseSchema } from "../config.js"; import { jsonMode, toXml, xmlMode } from "../output.js"; @@ -47,8 +47,8 @@ export const labelsList = Command.make( // --- labels create --- -const nameArg = Args.text({ name: "name" }).pipe( - Args.withDescription("Label name"), +const createNameOption = Options.text("name").pipe( + Options.withDescription("Label name"), ); const colorOption = Options.optional(Options.text("color")).pipe( Options.withDescription("Hex color e.g. #ff0000"), @@ -61,8 +61,12 @@ const labelArg = Args.text({ name: "label" }).pipe( export const labelsCreate = Command.make( "create", - { color: colorOption, project: projectArg, name: nameArg }, + { color: colorOption, project: listProjectArg, name: createNameOption }, labelsCreateHandler, +).pipe( + Command.withDescription( + 'Create a new label in a project. Omit PROJECT to use the saved current project.\n\nExamples:\n plane labels create --name bug\n plane labels create --name bug --color "#ff0000" PROJ', + ), ); export function labelsCreateHandler({ @@ -72,7 +76,7 @@ export function labelsCreateHandler({ }: { project: string; name: string; - color: { _tag: "Some"; value: string } | { _tag: "None" }; + color: Option.OptionRaw HTML
", - "ACME", + "--title", "HTML test", + "ACME", ]); expect((postedBody as { description_html?: string }).description_html).toBe( "Raw HTML
", diff --git a/tests/modules.test.ts b/tests/modules.test.ts index 5170c5a..2c68726 100644 --- a/tests/modules.test.ts +++ b/tests/modules.test.ts @@ -7,7 +7,9 @@ import { expect, it, } from "bun:test"; -import { Effect } from "effect"; +import { Command } from "@effect/cli"; +import { NodeContext } from "@effect/platform-node"; +import { Effect, Option } from "effect"; import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { _clearProjectCache } from "@/resolve"; @@ -41,6 +43,9 @@ const MODULES = [ { id: "mod1", name: "Sprint 1", status: "in_progress" }, { id: "mod2", name: "Sprint 2", status: "backlog" }, ]; +const MEMBERS = [ + { id: "mem1", display_name: "Jane Doe", email: "jane@example.com" }, +]; const MODULE_ISSUES = [ { id: "i1", @@ -59,6 +64,9 @@ const server = setupServer( http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/`, () => HttpResponse.json({ results: ISSUES }), ), + http.get(`${BASE}/api/v1/workspaces/${WS}/members/`, () => + HttpResponse.json(MEMBERS), + ), http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/`, () => HttpResponse.json({ results: MODULES }), ), @@ -83,8 +91,32 @@ afterEach(() => { delete process.env.PLANE_HOST; delete process.env.PLANE_WORKSPACE; delete process.env.PLANE_API_TOKEN; + delete process.env.PLANE_PROJECT; }); +async function runModulesCli(argv: string[]): Promise<{ logs: string[] }> { + const { modules } = await import("@/commands/modules"); + + const root = Command.make("plane").pipe(Command.withSubcommands([modules])); + const cli = Command.run(root, { name: "plane", version: "0.0.0" }); + + const logs: string[] = []; + const orig = console.log; + console.log = (...args: unknown[]) => logs.push(args.join(" ")); + + try { + await Effect.runPromise( + cli(["_", "_", ...argv]).pipe(Effect.provide(NodeContext.layer)), + ); + } catch (error) { + logs.push(`ERROR: ${String(error)}`); + } finally { + console.log = orig; + } + + return { logs }; +} + describe("modulesList", () => { it("lists modules for a project", async () => { const { modulesListHandler } = await import("@/commands/modules"); @@ -154,6 +186,177 @@ describe("modulesList", () => { }); }); +describe("modulesCreate", () => { + it("parses the public CLI entrypoint and maps create flags to the API payload", async () => { + let postedBody: unknown; + server.use( + http.post( + `${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/`, + async ({ request }) => { + postedBody = await request.json(); + return HttpResponse.json( + { + id: "mod-cli", + name: "Design System Rollout", + status: "in-progress", + description: "Ship tokens and docs", + }, + { status: 201 }, + ); + }, + ), + ); + + const { logs } = await runModulesCli([ + "modules", + "create", + "--name", + "Design System Rollout", + "--description", + "Ship tokens and docs", + "--status", + "in_progress", + "--start-date", + "2026-04-01", + "--target-date", + "2026-04-30", + "--lead", + "Jane Doe", + "ACME", + ]); + + expect(postedBody).toEqual({ + name: "Design System Rollout", + description: "Ship tokens and docs", + status: "in-progress", + start_date: "2026-04-01", + target_date: "2026-04-30", + lead: "mem1", + }); + expect(logs.join("\n")).toContain( + "Created module: Design System Rollout (mod-cli)", + ); + }); + + it("creates a module with only a name", async () => { + let postedBody: unknown; + server.use( + http.post( + `${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/`, + async ({ request }) => { + postedBody = await request.json(); + return HttpResponse.json( + { + id: "mod3", + name: "Sprint 3", + status: "planned", + identifier: "ACME", + created_at: "2026-03-31T00:00:00Z", + }, + { status: 201 }, + ); + }, + ), + ); + + const { modulesCreateHandler } = await import("@/commands/modules"); + const logs: string[] = []; + const orig = console.log; + console.log = (...args: unknown[]) => logs.push(args.join(" ")); + + try { + await Effect.runPromise( + modulesCreateHandler({ + project: "ACME", + name: "Sprint 3", + description: Option.none(), + status: Option.none(), + startDate: Option.none(), + targetDate: Option.none(), + lead: Option.none(), + }), + ); + } finally { + console.log = orig; + } + + expect(postedBody).toEqual({ name: "Sprint 3" }); + expect(logs.join("\n")).toContain("Created module: Sprint 3 (mod3)"); + }); + + it("creates a module with optional fields and resolves the lead", async () => { + let postedBody: unknown; + server.use( + http.post( + `${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/`, + async ({ request }) => { + postedBody = await request.json(); + return HttpResponse.json( + { + id: "mod4", + name: "Design System Rollout", + status: "in-progress", + description: "Ship tokens and docs", + }, + { status: 201 }, + ); + }, + ), + ); + + const { modulesCreateHandler } = await import("@/commands/modules"); + const logs: string[] = []; + const orig = console.log; + console.log = (...args: unknown[]) => logs.push(args.join(" ")); + + try { + await Effect.runPromise( + modulesCreateHandler({ + project: "ACME", + name: "Design System Rollout", + description: Option.some("Ship tokens and docs"), + status: Option.some("in_progress"), + startDate: Option.some("2026-04-01"), + targetDate: Option.some("2026-04-30"), + lead: Option.some("Jane Doe"), + }), + ); + } finally { + console.log = orig; + } + + expect(postedBody).toEqual({ + name: "Design System Rollout", + description: "Ship tokens and docs", + status: "in-progress", + start_date: "2026-04-01", + target_date: "2026-04-30", + lead: "mem1", + }); + expect(logs.join("\n")).toContain( + "Created module: Design System Rollout (mod4)", + ); + }); + + it("fails fast on invalid create dates before calling the API", async () => { + const { modulesCreateHandler } = await import("@/commands/modules"); + + await expect( + Effect.runPromise( + modulesCreateHandler({ + project: "ACME", + name: "Bad Dates", + description: Option.none(), + status: Option.none(), + startDate: Option.some("2026-02-31"), + targetDate: Option.none(), + lead: Option.none(), + }), + ), + ).rejects.toThrow("--start-date must be a valid date in YYYY-MM-DD format"); + }); +}); + describe("modulesDelete", () => { it("deletes a module by UUID", async () => { let deleted = false; diff --git a/tests/project-features.test.ts b/tests/project-features.test.ts index c11c1fc..72db6b1 100644 --- a/tests/project-features.test.ts +++ b/tests/project-features.test.ts @@ -279,3 +279,94 @@ describe("feature gates", () => { expect(agentsContent).toContain("plane projects current"); }); }); + +describe("SKILL.md import into AGENTS.md", () => { + it("imports SKILL.md when user answers 'y'", async () => { + const { initHandler } = await import("@/commands/init"); + const { getLocalAgentsFilePath } = await import("@/project-agents"); + const repoDir = path.join(tempHome, "repo"); + fs.mkdirSync(repoDir, { recursive: true }); + process.chdir(repoDir); + // host, workspace, token, project selection, skill import + promptResponses = ["", "", "", "1", "y"]; + await Effect.runPromise( + initHandler({ global: false, local: true }, "local"), + ); + const agentsPath = getLocalAgentsFilePath(repoDir); + const agentsContent = fs.readFileSync(agentsPath, "utf8"); + expect(agentsContent).toContain(""); + expect(agentsContent).toContain(""); + }); + + it("does not import SKILL.md when user declines (default N)", async () => { + const { initHandler } = await import("@/commands/init"); + const { getLocalAgentsFilePath } = await import("@/project-agents"); + const repoDir = path.join(tempHome, "repo"); + fs.mkdirSync(repoDir, { recursive: true }); + process.chdir(repoDir); + // empty response = default "N" + promptResponses = ["", "", "", "1", ""]; + await Effect.runPromise( + initHandler({ global: false, local: true }, "local"), + ); + const agentsPath = getLocalAgentsFilePath(repoDir); + const agentsContent = fs.readFileSync(agentsPath, "utf8"); + expect(agentsContent).not.toContain(""); + }); + + it("idempotently updates existing SKILL section on re-run when user confirms", async () => { + const { initHandler } = await import("@/commands/init"); + const { getLocalAgentsFilePath } = await import("@/project-agents"); + const repoDir = path.join(tempHome, "repo"); + fs.mkdirSync(repoDir, { recursive: true }); + process.chdir(repoDir); + // First run: import skill + promptResponses = ["", "", "", "1", "y"]; + await Effect.runPromise( + initHandler({ global: false, local: true }, "local"), + ); + // Second run: user confirms update (default Y when already present) + promptResponses = ["", "", "", "1", ""]; + await Effect.runPromise( + initHandler({ global: false, local: true }, "local"), + ); + const agentsPath = getLocalAgentsFilePath(repoDir); + const agentsContent = fs.readFileSync(agentsPath, "utf8"); + expect(agentsContent.match(//g)?.length).toBe( + 1, + ); + expect(agentsContent.match(//g)?.length).toBe( + 1, + ); + }); + + it("importSkillIntoAgentsFile creates section in a new file", async () => { + const { importSkillIntoAgentsFile, getLocalAgentsFilePath } = await import( + "@/project-agents" + ); + const repoDir = path.join(tempHome, "repo"); + fs.mkdirSync(repoDir, { recursive: true }); + importSkillIntoAgentsFile("# CLI Guide\nAll commands here.", repoDir); + const filePath = getLocalAgentsFilePath(repoDir); + const content = fs.readFileSync(filePath, "utf8"); + expect(content).toContain(""); + expect(content).toContain("# CLI Guide"); + expect(content).toContain(""); + }); + + it("hasSkillSectionInAgentsFile returns false before import", async () => { + const { hasSkillSectionInAgentsFile } = await import("@/project-agents"); + const repoDir = path.join(tempHome, "repo"); + fs.mkdirSync(repoDir, { recursive: true }); + expect(hasSkillSectionInAgentsFile(repoDir)).toBe(false); + }); + + it("hasSkillSectionInAgentsFile returns true after import", async () => { + const { importSkillIntoAgentsFile, hasSkillSectionInAgentsFile } = + await import("@/project-agents"); + const repoDir = path.join(tempHome, "repo"); + fs.mkdirSync(repoDir, { recursive: true }); + importSkillIntoAgentsFile("# CLI Guide", repoDir); + expect(hasSkillSectionInAgentsFile(repoDir)).toBe(true); + }); +});