From 35652d45056b91b5c30228bc831ba9295afbcb3f Mon Sep 17 00:00:00 2001 From: Urata Daiki <7nohe@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:07:20 +0900 Subject: [PATCH 1/2] feat: enhance `figdeck init` command to support AI agent rule generation and file creation options --- README.ja.md | 36 ++++ README.md | 29 ++- packages/cli/src/cli.test.ts | 189 ++++++++++++++++- packages/cli/src/index.ts | 200 +++++++++++++++--- packages/cli/src/md.d.ts | 5 + packages/cli/src/templates.ts | 33 +++ packages/cli/templates/agents.md | 6 + packages/cli/templates/ai-rules-body.md | 119 +++++++++++ packages/cli/templates/claude.md | 84 ++++++++ packages/cli/templates/copilot.md | 12 ++ packages/cli/templates/cursor.mdc | 13 ++ packages/cli/tsup.config.ts | 1 + .../docs/src/content/docs/en/api-reference.md | 26 ++- .../docs/src/content/docs/ja/api-reference.md | 22 +- 14 files changed, 742 insertions(+), 33 deletions(-) create mode 100644 packages/cli/templates/agents.md create mode 100644 packages/cli/templates/ai-rules-body.md create mode 100644 packages/cli/templates/claude.md create mode 100644 packages/cli/templates/copilot.md create mode 100644 packages/cli/templates/cursor.mdc diff --git a/README.ja.md b/README.ja.md index fd79786..5ec080e 100644 --- a/README.ja.md +++ b/README.ja.md @@ -150,6 +150,42 @@ color: "#ffffff" ## CLI コマンド +### `init` - テンプレート作成 + +```bash +figdeck init [options] + +Options: + -o, --out 出力ファイルパス (default: "slides.md") + -f, --force 既存ファイルを上書き + --ai-rules [targets] AI エージェントルールを生成 (agents,claude,cursor,copilot または all) + --no-slides slides.md の生成をスキップ + -h, --help ヘルプ表示 +``` + +サポートされている全ての Markdown 記法の例を含む `slides.md` テンプレートを作成します。 + +オプションで AI エージェントルールファイルを生成できます: + +```bash +# 全ての AI エージェントルールを生成 +figdeck init --ai-rules all + +# 特定のルールのみ生成 +figdeck init --ai-rules claude,cursor + +# 既存プロジェクトにルールを追加(slides.md はそのまま) +figdeck init --ai-rules all --no-slides +``` + +生成されるファイル: +| ターゲット | ファイル | 対象ツール | +|------------|----------|------------| +| `agents` | `AGENTS.md` | Codex CLI, Cursor (AGENTS.md) | +| `claude` | `.claude/rules/figdeck.md` | Claude Code | +| `cursor` | `.cursor/rules/figdeck.mdc` | Cursor | +| `copilot` | `.github/instructions/figdeck.instructions.md` | GitHub Copilot | + ### `build` - JSON 出力 ```bash diff --git a/README.md b/README.md index b8b3776..1e66870 100644 --- a/README.md +++ b/README.md @@ -174,13 +174,36 @@ Per-slide frontmatter > Global frontmatter figdeck init [options] Options: - -o, --out Output file path (default: "slides.md") - -f, --force Overwrite existing file - -h, --help Show help + -o, --out Output file path (default: "slides.md") + -f, --force Overwrite existing files + --ai-rules [targets] Generate AI agent rules (agents,claude,cursor,copilot or all) + --no-slides Skip generating slides.md + -h, --help Show help ``` Creates a template `slides.md` with examples of all supported Markdown syntax. +Optionally generate AI agent rule files with `--ai-rules`: + +```bash +# Generate all AI agent rules +figdeck init --ai-rules all + +# Generate specific rules only +figdeck init --ai-rules claude,cursor + +# Add rules to existing project (keep existing slides.md) +figdeck init --ai-rules all --no-slides +``` + +Generated files: +| Target | File | Tool | +|--------|------|------| +| `agents` | `AGENTS.md` | Codex CLI, Cursor (AGENTS.md) | +| `claude` | `.claude/rules/figdeck.md` | Claude Code | +| `cursor` | `.cursor/rules/figdeck.mdc` | Cursor | +| `copilot` | `.github/instructions/figdeck.instructions.md` | GitHub Copilot | + ### `build` - JSON Output ```bash diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts index 07cbd52..2cae5e5 100644 --- a/packages/cli/src/cli.test.ts +++ b/packages/cli/src/cli.test.ts @@ -1,5 +1,12 @@ -import { describe, expect, it } from "bun:test"; -import { existsSync, readFileSync, unlinkSync } from "node:fs"; +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { + existsSync, + mkdirSync, + readFileSync, + rmSync, + unlinkSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; import { $ } from "bun"; @@ -105,4 +112,182 @@ describe("CLI", () => { expect(result).toContain("-o"); }); }); + + describe("init command", () => { + const TMP_DIR = join(import.meta.dir, ".tmp-init-test"); + + beforeEach(() => { + if (existsSync(TMP_DIR)) { + rmSync(TMP_DIR, { recursive: true, force: true }); + } + mkdirSync(TMP_DIR, { recursive: true }); + }); + + afterEach(() => { + if (existsSync(TMP_DIR)) { + rmSync(TMP_DIR, { recursive: true, force: true }); + } + }); + + it("should show --ai-rules and --no-slides in help", async () => { + const result = await $`bun ${CLI_PATH} init --help`.text(); + + expect(result).toContain("--ai-rules"); + expect(result).toContain("--no-slides"); + expect(result).toContain("agents,claude,cursor,copilot"); + }); + + it("should create only slides.md by default", async () => { + const outPath = join(TMP_DIR, "slides.md"); + await $`bun ${CLI_PATH} init -o ${outPath}`.text(); + + expect(existsSync(outPath)).toBe(true); + expect(existsSync(join(TMP_DIR, "AGENTS.md"))).toBe(false); + expect(existsSync(join(TMP_DIR, ".claude/rules/figdeck.md"))).toBe(false); + }); + + it("should create AGENTS.md with --ai-rules agents", async () => { + const outPath = join(TMP_DIR, "slides.md"); + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules agents`.text(); + + expect(existsSync(outPath)).toBe(true); + expect(existsSync(join(TMP_DIR, "AGENTS.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".claude/rules/figdeck.md"))).toBe(false); + }); + + it("should create .claude/rules/figdeck.md with --ai-rules claude", async () => { + const outPath = join(TMP_DIR, "slides.md"); + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules claude`.text(); + + expect(existsSync(outPath)).toBe(true); + expect(existsSync(join(TMP_DIR, ".claude/rules/figdeck.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, "AGENTS.md"))).toBe(false); + }); + + it("should create .cursor/rules/figdeck.mdc with --ai-rules cursor", async () => { + const outPath = join(TMP_DIR, "slides.md"); + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules cursor`.text(); + + expect(existsSync(outPath)).toBe(true); + expect(existsSync(join(TMP_DIR, ".cursor/rules/figdeck.mdc"))).toBe(true); + }); + + it("should create .github/instructions/figdeck.instructions.md with --ai-rules copilot", async () => { + const outPath = join(TMP_DIR, "slides.md"); + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules copilot`.text(); + + expect(existsSync(outPath)).toBe(true); + expect( + existsSync( + join(TMP_DIR, ".github/instructions/figdeck.instructions.md"), + ), + ).toBe(true); + }); + + it("should create two files with --ai-rules claude,cursor", async () => { + const outPath = join(TMP_DIR, "slides.md"); + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules claude,cursor`.text(); + + expect(existsSync(outPath)).toBe(true); + expect(existsSync(join(TMP_DIR, ".claude/rules/figdeck.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".cursor/rules/figdeck.mdc"))).toBe(true); + expect(existsSync(join(TMP_DIR, "AGENTS.md"))).toBe(false); + expect( + existsSync( + join(TMP_DIR, ".github/instructions/figdeck.instructions.md"), + ), + ).toBe(false); + }); + + it("should create all files with --ai-rules all", async () => { + const outPath = join(TMP_DIR, "slides.md"); + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules all`.text(); + + expect(existsSync(outPath)).toBe(true); + expect(existsSync(join(TMP_DIR, "AGENTS.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".claude/rules/figdeck.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".cursor/rules/figdeck.mdc"))).toBe(true); + expect( + existsSync( + join(TMP_DIR, ".github/instructions/figdeck.instructions.md"), + ), + ).toBe(true); + }); + + it("should create all files with --ai-rules (no value)", async () => { + const outPath = join(TMP_DIR, "slides.md"); + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules`.text(); + + expect(existsSync(outPath)).toBe(true); + expect(existsSync(join(TMP_DIR, "AGENTS.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".claude/rules/figdeck.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".cursor/rules/figdeck.mdc"))).toBe(true); + expect( + existsSync( + join(TMP_DIR, ".github/instructions/figdeck.instructions.md"), + ), + ).toBe(true); + }); + + it("should fail with invalid --ai-rules value", async () => { + const outPath = join(TMP_DIR, "slides.md"); + const result = + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules invalid` + .text() + .catch((e) => e.stderr?.toString() || "error"); + + expect(result).toContain("Invalid --ai-rules targets"); + expect(existsSync(outPath)).toBe(false); + }); + + it("should fail without --force when file exists", async () => { + const outPath = join(TMP_DIR, "slides.md"); + writeFileSync(outPath, "existing content", "utf-8"); + + const result = await $`bun ${CLI_PATH} init -o ${outPath}` + .text() + .catch((e) => e.stderr?.toString() || "error"); + + expect(result).toContain("already exist"); + // Existing content should be preserved + expect(readFileSync(outPath, "utf-8")).toBe("existing content"); + }); + + it("should overwrite with --force", async () => { + const outPath = join(TMP_DIR, "slides.md"); + writeFileSync(outPath, "existing content", "utf-8"); + + await $`bun ${CLI_PATH} init -o ${outPath} --force`.text(); + + expect(existsSync(outPath)).toBe(true); + // Content should be overwritten + expect(readFileSync(outPath, "utf-8")).not.toBe("existing content"); + }); + + it("should skip slides.md with --no-slides", async () => { + const outPath = join(TMP_DIR, "slides.md"); + writeFileSync(outPath, "existing content", "utf-8"); + + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules agents --no-slides`.text(); + + // slides.md should not be changed + expect(readFileSync(outPath, "utf-8")).toBe("existing content"); + // AGENTS.md should be created + expect(existsSync(join(TMP_DIR, "AGENTS.md"))).toBe(true); + }); + + it("should replace {{slidesPath}} placeholder in templates", async () => { + const outPath = join(TMP_DIR, "deck/my-slides.md"); + mkdirSync(join(TMP_DIR, "deck"), { recursive: true }); + + await $`bun ${CLI_PATH} init -o ${outPath} --ai-rules claude`.text(); + + const claudeContent = readFileSync( + join(TMP_DIR, "deck/.claude/rules/figdeck.md"), + "utf-8", + ); + expect(claudeContent).toContain("deck/my-slides.md"); + expect(claudeContent).not.toContain("{{slidesPath}}"); + }); + }); }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 246335f..52107e3 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,9 +1,21 @@ -import { existsSync, readFileSync, watchFile, writeFileSync } from "node:fs"; -import { dirname, resolve } from "node:path"; +import { + existsSync, + mkdirSync, + readFileSync, + watchFile, + writeFileSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { Command } from "commander"; import { parseMarkdown } from "./markdown.js"; -import { getInitTemplate } from "./templates.js"; +import { + getAgentsTemplate, + getClaudeTemplate, + getCopilotTemplate, + getCursorTemplate, + getInitTemplate, +} from "./templates.js"; import { generateSecret, isLoopbackHost, startServer } from "./ws-server.js"; // Debounce delay for file watch (ms) @@ -22,33 +34,173 @@ program .description("Convert Markdown to Figma Slides") .version(CLI_VERSION); -// init: create template slides.md +// Valid AI rules targets +const AI_RULES_TARGETS = ["agents", "claude", "cursor", "copilot"] as const; +type AiRulesTarget = (typeof AI_RULES_TARGETS)[number]; + +type FileToWrite = { + absPath: string; + content: string; + displayPath: string; +}; + +const AI_RULES_FILES: Record< + AiRulesTarget, + { + displayPath: string; + pathParts: string[]; + template: () => string; + } +> = { + agents: { + displayPath: "AGENTS.md", + pathParts: ["AGENTS.md"], + template: getAgentsTemplate, + }, + claude: { + displayPath: ".claude/rules/figdeck.md", + pathParts: [".claude", "rules", "figdeck.md"], + template: getClaudeTemplate, + }, + cursor: { + displayPath: ".cursor/rules/figdeck.mdc", + pathParts: [".cursor", "rules", "figdeck.mdc"], + template: getCursorTemplate, + }, + copilot: { + displayPath: ".github/instructions/figdeck.instructions.md", + pathParts: [".github", "instructions", "figdeck.instructions.md"], + template: getCopilotTemplate, + }, +}; + +function parseAiRulesTargets( + aiRules: boolean | string | undefined, +): { targets: AiRulesTarget[]; invalidTargets: string[] } { + if (aiRules === true || aiRules === "all") { + return { targets: [...AI_RULES_TARGETS], invalidTargets: [] }; + } + if (typeof aiRules !== "string") { + return { targets: [], invalidTargets: [] }; + } + + const raw = aiRules + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + const invalidTargets = raw.filter( + (t) => !AI_RULES_TARGETS.includes(t as AiRulesTarget), + ); + if (invalidTargets.length) { + return { targets: [], invalidTargets }; + } + + const targets = Array.from(new Set(raw)) as AiRulesTarget[]; + return { targets, invalidTargets: [] }; +} + +// init: create template slides.md and optional AI rules files program .command("init") - .description("Create a template slides.md file") + .description("Create a template slides.md file and optional AI agent rules") .option("-o, --out ", "Output file path", "slides.md") - .option("-f, --force", "Overwrite existing file") - .action((options: { out: string; force?: boolean }) => { - try { - const outputPath = resolve(options.out); + .option("-f, --force", "Overwrite existing files") + .option( + "--ai-rules [targets]", + "Generate AI agent rules (agents,claude,cursor,copilot or all)", + ) + .option("--no-slides", "Skip generating slides.md") + .action( + (options: { + out: string; + force?: boolean; + aiRules?: boolean | string; + slides: boolean; + }) => { + try { + const outputPath = resolve(options.out); + const outputDir = dirname(outputPath); + const slidesPath = options.out; + + // Parse --ai-rules targets + const { targets, invalidTargets } = parseAiRulesTargets(options.aiRules); + if (invalidTargets.length) { + console.error( + `Error: Invalid --ai-rules targets: ${invalidTargets.join(", ")}`, + ); + console.error(`Valid targets: ${AI_RULES_TARGETS.join(", ")}, all`); + process.exit(1); + } + + // Format template by replacing {{slidesPath}} placeholder + const format = (content: string) => + content.replaceAll("{{slidesPath}}", slidesPath); + + // Build list of files to write + const filesToWrite: FileToWrite[] = []; + + if (options.slides !== false) { + filesToWrite.push({ + absPath: outputPath, + content: getInitTemplate(), + displayPath: options.out, + }); + } + for (const target of targets) { + const file = AI_RULES_FILES[target]; + filesToWrite.push({ + absPath: join(outputDir, ...file.pathParts), + content: format(file.template()), + displayPath: file.displayPath, + }); + } - if (!options.force && existsSync(outputPath)) { - console.error(`Error: File already exists: ${options.out}`); - console.error("Use --force to overwrite."); + if (filesToWrite.length === 0) { + console.error("Error: No files to generate."); + console.error( + "Use --ai-rules to generate AI agent rules, or remove --no-slides.", + ); + process.exit(1); + } + + // Check for existing files + const existing = filesToWrite.filter((f) => existsSync(f.absPath)); + if (existing.length && !options.force) { + console.error("Error: The following files already exist:"); + for (const f of existing) { + console.error(` - ${f.displayPath}`); + } + console.error("\nTo resolve:"); + console.error(" - Use --force to overwrite existing files"); + if (existing.some((f) => f.displayPath === options.out)) { + console.error(" - Use --no-slides to skip slides.md generation"); + } + process.exit(1); + } + + // Create necessary directories + for (const f of filesToWrite) { + mkdirSync(dirname(f.absPath), { recursive: true }); + } + + // Write all files + for (const f of filesToWrite) { + writeFileSync(f.absPath, f.content, "utf-8"); + console.log(`Created ${f.displayPath}`); + } + + // Show next steps if slides.md was generated + if (options.slides !== false) { + console.log(`\nNext steps:`); + console.log(` 1. Run: figdeck serve ${options.out}`); + console.log(` 2. Connect the Figma plugin to generate slides`); + } + } catch (error) { + console.error("Error:", (error as Error).message); process.exit(1); } - - const template = getInitTemplate(); - writeFileSync(outputPath, template, "utf-8"); - console.log(`Created ${options.out}`); - console.log(`\nNext steps:`); - console.log(` 1. Run: figdeck serve ${options.out}`); - console.log(` 2. Connect the Figma plugin to generate slides`); - } catch (error) { - console.error("Error:", (error as Error).message); - process.exit(1); - } - }); + }, + ); // build: one-shot parse and output JSON program diff --git a/packages/cli/src/md.d.ts b/packages/cli/src/md.d.ts index c94d67b..0956757 100644 --- a/packages/cli/src/md.d.ts +++ b/packages/cli/src/md.d.ts @@ -2,3 +2,8 @@ declare module "*.md" { const content: string; export default content; } + +declare module "*.mdc" { + const content: string; + export default content; +} diff --git a/packages/cli/src/templates.ts b/packages/cli/src/templates.ts index df879a6..073fa2e 100644 --- a/packages/cli/src/templates.ts +++ b/packages/cli/src/templates.ts @@ -1,4 +1,9 @@ import type { TitlePrefixConfig } from "@figdeck/shared"; +import agentsTemplate from "../templates/agents.md"; +import aiRulesBodyTemplate from "../templates/ai-rules-body.md"; +import claudeTemplate from "../templates/claude.md"; +import copilotTemplate from "../templates/copilot.md"; +import cursorTemplate from "../templates/cursor.mdc"; import initTemplate from "../templates/init.md"; /** @@ -54,3 +59,31 @@ export function getRegisteredTemplates(): string[] { export function getInitTemplate(): string { return initTemplate; } + +/** + * Get the AGENTS.md template for Codex CLI and general AI agents + */ +export function getAgentsTemplate(): string { + return agentsTemplate.replaceAll("{{aiRulesBody}}", aiRulesBodyTemplate); +} + +/** + * Get the Claude Code template (.claude/rules/figdeck.md) + */ +export function getClaudeTemplate(): string { + return claudeTemplate; +} + +/** + * Get the Cursor template (.cursor/rules/figdeck.mdc) + */ +export function getCursorTemplate(): string { + return cursorTemplate.replaceAll("{{aiRulesBody}}", aiRulesBodyTemplate); +} + +/** + * Get the GitHub Copilot template (.github/instructions/figdeck.instructions.md) + */ +export function getCopilotTemplate(): string { + return copilotTemplate.replaceAll("{{aiRulesBody}}", aiRulesBodyTemplate); +} diff --git a/packages/cli/templates/agents.md b/packages/cli/templates/agents.md new file mode 100644 index 0000000..0c1c9b5 --- /dev/null +++ b/packages/cli/templates/agents.md @@ -0,0 +1,6 @@ +# AGENTS.md + +This repository is a **figdeck (Markdown → Figma Slides)** slide deck. +AI agents should edit `{{slidesPath}}` using **only Markdown syntax that figdeck supports**. + +{{aiRulesBody}} diff --git a/packages/cli/templates/ai-rules-body.md b/packages/cli/templates/ai-rules-body.md new file mode 100644 index 0000000..86fadc4 --- /dev/null +++ b/packages/cli/templates/ai-rules-body.md @@ -0,0 +1,119 @@ +## Files +- `{{slidesPath}}`: the slide deck Markdown +- `images/` (optional): local images + +## Local validation (recommended) +- Generate JSON and validate structure: `npx figdeck build {{slidesPath}}` +- Generate via the plugin: `npx figdeck serve {{slidesPath}}` → Figma Desktop → Plugins → figdeck + +> WebSocket integration requires **Figma Desktop** (the browser version cannot connect). + +## figdeck Markdown rules (important) + +### Slide separators +- A single line containing `---` (thematic break) separates slides +- If a slide is getting long, split it (rule of thumb: 3–6 bullets or ≤2 short paragraphs per slide) + +### YAML frontmatter (global vs per-slide) +- **Global settings**: frontmatter at the very beginning of the file applies to all slides +- **Per-slide settings**: frontmatter at the beginning of a slide overrides only that slide + - After the first slide, per-slide frontmatter appears immediately after a slide separator, so you'll see two `---` blocks back-to-back + +Example: +```md +--- +background: "#111827" +color: "#ffffff" +transition: dissolve +--- + +# Title Slide + +--- + +--- +background: "#ffffff" +color: "#111827" +--- + +## Content Slide +``` + +Common settings: +- `background`, `color` (prefer hex like `"#RRGGBB"`) +- `gradient: "#000:0%,#fff:100%@45"` +- `backgroundImage` (local path or URL) +- `align: left|center|right`, `valign: top|middle|bottom` +- `transition` (e.g. `dissolve`, `slide-from-right 0.5`, or the detailed object form) +- `fonts` (falls back to Inter if unavailable) +- `slideNumber` (show/position/startFrom) + +### Heading roles +- `#` (H1): **title slide** +- `##` (H2): **content slide** +- `###` / `####`: sub-headings inside a slide + +### Lists (nested lists use 2 spaces) +```md +- Level 0 + - Level 1 (2 spaces) + - Level 2 +``` + +### Common supported elements +- Paragraphs, **bold**, *italic*, ~~strike~~, inline `code`, [links](https://example.com) +- Bullet lists (ordered/unordered, nested) +- Blockquotes `>`, tables (GFM) +- Code blocks (```lang) +- Images (local/remote) +- Footnotes (GFM) + +## Directives (`:::`) +`:::` is reserved for figdeck directives. **Unknown directive names are ignored.** + +### `:::columns` (2–4 columns) +```md +:::columns gap=32 width=1fr/2fr +:::column +Left column +:::column +Right column +::: +``` +- `gap`: default 32px (max 200) +- `width`: `fr` / `%` / `px` values separated by `/` + +### `:::figma` (Figma node link cards) +```md +:::figma +link=https://www.figma.com/design/xxx?node-id=1234-5678 +text.title=Cart Feature +text.body=Use **bold** and *italic* in overrides. +::: +``` +- Properties are `key=value` +- `link` is required (must include `node-id`) +- `text.*` overrides text layers (rich text supported; code blocks are not) + +## Images (important: format limits) +- Local images: `![alt](./images/pic.png)` (resolved relative to the Markdown file) + - Supported: `.jpg`, `.jpeg`, `.png`, `.gif` + - **WebP/SVG are not supported** + - Files over 5MB are skipped (warning) +- Remote images: `https://...` (fetched by the plugin; PNG/JPEG/GIF) + +Marp-compatible size/position syntax (in alt text): +```md +![w:400](./image.png) +![h:300](./image.png) +![w:300 x:100 y:200 Logo](./image.png) +``` +- Percentages are based on 1920×1080 (e.g. `x:50%` → 960px) + +## Change checklist +- Slide separators `---` are intact +- Frontmatter appears only at the start of the file or the start of a slide +- New slides begin with `#` or `##` +- Nested lists are indented with 2 spaces +- `:::columns` / `:::figma` blocks are properly closed with `:::` +- Images use supported formats (png/jpg/gif) and valid paths diff --git a/packages/cli/templates/claude.md b/packages/cli/templates/claude.md new file mode 100644 index 0000000..c73637f --- /dev/null +++ b/packages/cli/templates/claude.md @@ -0,0 +1,84 @@ +--- +paths: "{{slidesPath}}, **/*.md" +--- + +# figdeck Slide Authoring Rules + +This repository is a **figdeck (Markdown → Figma Slides)** slide deck. The main file to edit is `{{slidesPath}}`. + +Goal: help an AI agent understand figdeck's authoring rules and produce output that renders reliably in Figma Slides. + +## Hard rules (common failure points) +- Slide separators are a single line: `---` +- `#` creates a **title slide**, `##` creates a **content slide** +- In-slide headings should use `###` / `####` +- Nested lists must use **2 spaces** indentation +- `:::` directives are only `:::columns` and `:::figma` (other names are ignored) +- Images must be **png/jpg/gif** (WebP/SVG unsupported; local images > 5MB are skipped) + +## Editing workflow (recommended) +1. Read `{{slidesPath}}` and match existing tone, structure, and heading hierarchy +2. If a slide gets long, split it by adding `---` +3. Avoid destructive rewrites; keep diffs minimal and scoped +4. Validate with `npx figdeck build {{slidesPath}}` + +Plugin validation: +- Run `npx figdeck serve {{slidesPath}}` +- Figma Desktop → Plugins → figdeck (browser cannot connect via WebSocket) + +## figdeck quick reference + +### YAML frontmatter +- At the top of the file: global defaults +- At the start of a slide: per-slide overrides (after the first slide, it appears right after `---`) + +Example: +```md +--- +background: "#111827" +color: "#ffffff" +transition: dissolve +--- + +# Title + +--- + +--- +background: "#ffffff" +color: "#111827" +--- + +## Agenda +``` + +### `:::columns` (2–4 columns) +```md +:::columns gap=32 width=1fr/2fr +:::column +Left +:::column +Right +::: +``` + +### `:::figma` (Figma node link cards) +```md +:::figma +link=https://www.figma.com/design/xxx?node-id=1234-5678 +text.title=Title override +text.body=Supports **bold**, *italic*, ~~strike~~, [link](https://example.com) +::: +``` + +### Images +```md +![Local](./images/photo.png) +![w:400 x:100 y:200 Logo](./images/logo.png) +``` + +## Tips to avoid layout issues +- Keep each slide small; split if content grows +- Keep tables/code/quotes short (split or summarize if needed) +- Specify image size (`w:` / `h:`) for more stable layout +- Use `align` / `valign` to make title slides look consistent (e.g. `align: center`, `valign: middle`) diff --git a/packages/cli/templates/copilot.md b/packages/cli/templates/copilot.md new file mode 100644 index 0000000..3094021 --- /dev/null +++ b/packages/cli/templates/copilot.md @@ -0,0 +1,12 @@ +--- +name: figdeck +description: figdeck Markdown slide authoring rules +applyTo: "{{slidesPath}},**/*.md" +--- + +# figdeck Slide Authoring Instructions + +This repository is a **figdeck (Markdown → Figma Slides)** slide deck. +Edit `{{slidesPath}}` using **only Markdown syntax that figdeck supports**. + +{{aiRulesBody}} diff --git a/packages/cli/templates/cursor.mdc b/packages/cli/templates/cursor.mdc new file mode 100644 index 0000000..25f7032 --- /dev/null +++ b/packages/cli/templates/cursor.mdc @@ -0,0 +1,13 @@ +--- +description: figdeck Markdown slide authoring rules +globs: + - "{{slidesPath}}" + - "**/*.md" +--- + +# figdeck Slide Authoring Rules + +This repository is a **figdeck (Markdown → Figma Slides)** slide deck. +AI agents should edit `{{slidesPath}}` using **only Markdown syntax that figdeck supports**. + +{{aiRulesBody}} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index a61cc38..bde305d 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ outDir: "dist", loader: { ".md": "text", + ".mdc": "text", }, // Bundle @figdeck/shared into the output (not published to npm) noExternal: ["@figdeck/shared"], diff --git a/packages/docs/src/content/docs/en/api-reference.md b/packages/docs/src/content/docs/en/api-reference.md index 369106a..dd52f19 100644 --- a/packages/docs/src/content/docs/en/api-reference.md +++ b/packages/docs/src/content/docs/en/api-reference.md @@ -8,7 +8,7 @@ title: API Reference #### `init` - Create Template -Creates a template `slides.md` with examples of all supported Markdown syntax. +Creates a template `slides.md` with examples of all supported Markdown syntax. Optionally generates AI agent rule files for various coding assistants. ```bash figdeck init [options] @@ -17,9 +17,20 @@ figdeck init [options] | Option | Description | Default | |--------|-------------|---------| | `-o, --out ` | Output file path | `slides.md` | -| `-f, --force` | Overwrite existing file | - | +| `-f, --force` | Overwrite existing files | - | +| `--ai-rules [targets]` | Generate AI agent rules (agents,claude,cursor,copilot or all) | - | +| `--no-slides` | Skip generating slides.md | - | | `-h, --help` | Show help | - | +**AI Rules Targets:** + +| Target | Generated File | Tool | +|--------|----------------|------| +| `agents` | `AGENTS.md` | Codex CLI, Cursor (AGENTS.md) | +| `claude` | `.claude/rules/figdeck.md` | Claude Code | +| `cursor` | `.cursor/rules/figdeck.mdc` | Cursor | +| `copilot` | `.github/instructions/figdeck.instructions.md` | GitHub Copilot | + **Examples:** ```bash @@ -29,8 +40,17 @@ figdeck init # Create with custom filename figdeck init -o presentation.md -# Overwrite existing file +# Overwrite existing files figdeck init --force + +# Generate all AI agent rules +figdeck init --ai-rules all + +# Generate specific rules only +figdeck init --ai-rules claude,cursor + +# Add rules to existing project (keep existing slides.md) +figdeck init --ai-rules all --no-slides ``` #### `build` - JSON Output diff --git a/packages/docs/src/content/docs/ja/api-reference.md b/packages/docs/src/content/docs/ja/api-reference.md index b478574..65a682f 100644 --- a/packages/docs/src/content/docs/ja/api-reference.md +++ b/packages/docs/src/content/docs/ja/api-reference.md @@ -8,7 +8,7 @@ title: API リファレンス #### `init` - テンプレート作成 -サポートされている全ての Markdown 記法の例を含む `slides.md` テンプレートを作成します。 +サポートされている全ての Markdown 記法の例を含む `slides.md` テンプレートを作成します。オプションで各種コーディングアシスタント向けの AI エージェントルールファイルも生成できます。 ```bash figdeck init [options] @@ -18,8 +18,19 @@ figdeck init [options] |------------|------|-----------| | `-o, --out ` | 出力ファイルパス | `slides.md` | | `-f, --force` | 既存ファイルを上書き | - | +| `--ai-rules [targets]` | AI エージェントルールを生成 (agents,claude,cursor,copilot または all) | - | +| `--no-slides` | slides.md の生成をスキップ | - | | `-h, --help` | ヘルプ表示 | - | +**AI ルールのターゲット:** + +| ターゲット | 生成ファイル | 対象ツール | +|------------|--------------|------------| +| `agents` | `AGENTS.md` | Codex CLI, Cursor (AGENTS.md) | +| `claude` | `.claude/rules/figdeck.md` | Claude Code | +| `cursor` | `.cursor/rules/figdeck.mdc` | Cursor | +| `copilot` | `.github/instructions/figdeck.instructions.md` | GitHub Copilot | + **例:** ```bash @@ -31,6 +42,15 @@ figdeck init -o presentation.md # 既存ファイルを上書き figdeck init --force + +# 全ての AI エージェントルールを生成 +figdeck init --ai-rules all + +# 特定のルールのみ生成 +figdeck init --ai-rules claude,cursor + +# 既存プロジェクトにルールを追加(slides.md はそのまま) +figdeck init --ai-rules all --no-slides ``` #### `build` - JSON 出力 From e728655ab9fbbc282345ed9f839f57021c8c0af5 Mon Sep 17 00:00:00 2001 From: Urata Daiki <7nohe@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:38:45 +0900 Subject: [PATCH 2/2] refactor: improve formatting of `parseAiRulesTargets` function for better readability --- packages/cli/src/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 52107e3..50d4cf9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -74,9 +74,10 @@ const AI_RULES_FILES: Record< }, }; -function parseAiRulesTargets( - aiRules: boolean | string | undefined, -): { targets: AiRulesTarget[]; invalidTargets: string[] } { +function parseAiRulesTargets(aiRules: boolean | string | undefined): { + targets: AiRulesTarget[]; + invalidTargets: string[]; +} { if (aiRules === true || aiRules === "all") { return { targets: [...AI_RULES_TARGETS], invalidTargets: [] }; } @@ -123,7 +124,9 @@ program const slidesPath = options.out; // Parse --ai-rules targets - const { targets, invalidTargets } = parseAiRulesTargets(options.aiRules); + const { targets, invalidTargets } = parseAiRulesTargets( + options.aiRules, + ); if (invalidTargets.length) { console.error( `Error: Invalid --ai-rules targets: ${invalidTargets.join(", ")}`,