@dittowords/spec-cli
v0.0.1-alpha.22
Published
CLI for syncing .ditto.md content specs with the Ditto platform.
Maintainers
Keywords
Readme
@dittowords/spec-cli
Alpha — this package is in early development. The format and CLI interface may change between releases.
CLI for syncing Ditto spec files with the Ditto platform.
A .ditto.md Ditto spec file lives next to a component and declares its text surfaces — every piece of user-facing copy the component renders, whether passed as props, children, or hardcoded in the component itself. The CLI syncs matching style guide rules from the Ditto platform into each file, co-locating content governance directly in your design system alongside the components themselves. The file is pure metadata; nothing imports it at runtime. It exists for three consumers:
- Agents read it as fast-path context when writing or editing copy for the component.
- The CLI (
ditto-spec pull) syncs style guide rules from the platform whose tags match the Ditto spec's surface tags. - Humans review content decisions in PRs.
File format
Everything lives in YAML frontmatter. The markdown body below the closing --- is unused.
Component Ditto spec (<Component>/index.ditto.md)
---
component: DialogueModal
tags: [dialog, confirmation]
surfaces:
headline:
tags: [heading, dialog-title]
maxLength: 60
content:
tags: [body, dialog-body]
maxLength: 240
actionText:
tags: [call-to-action]
maxLength: 25
cancelText:
tags: [button]
maxLength: 25
# Managed by Ditto — do not edit below
rules:
- name: Confirmation dialogs should be direct
description: Keep confirmation copy terse and unambiguous
section: Voice & Tone
- surface: actionText
name: Calls to action should use active voice
description: Always lead with a verb
examples:
- from: "Your settings"
to: "Open settings"
section: Voice & Tone
- term: sign up
disallowed:
- signup
- sign-up
description: Always use as two words (verb form)
section: Terminology
locales:
de-DE:
- name: Use informal address
description: Use "Du" instead of "Sie" for all user-facing copy
section: Formality
---Workspace Ditto spec (workspace.ditto.md)
A repo may have a single workspace.ditto.md somewhere under the CLI's configured roots. It holds universal style guide rules that carry no tags — these apply to every surface in every component. It also carries an inventory of all tags available on the platform, populated by ditto-spec pull.
---
workspace: true
# Managed by Ditto — do not edit below
tags: [body, button, call-to-action, dialog-title, heading, nav]
rules:
- name: Write in active voice
description: Lead with verbs, avoid passive constructions
section: Voice & Tone
locales:
de-DE:
- name: Use informal address
description: Use "Du" instead of "Sie" for all user-facing copy
section: Formality
---Key concepts
Developer-owned keys: component, tags, surfaces. Edit these freely.
CLI-managed keys: rules, locales, and workspace tags. Overwritten by ditto-spec pull. Do not edit by hand. Style guide rules come in two shapes: style rules (name/description/examples) and terminology entries (term/disallowed/description).
Surface keys identify each distinct piece of user-facing text the component renders. For text passed as props, use the prop path as the key — dot-notation works for nested props (e.g., primaryAction.label). Use $children for text via the children prop. For hardcoded or internal strings, use a descriptive role name (e.g., headline, bodyText, submitLabel).
maxLength is a hard layout constraint, not a stylistic preference. Stylistic guidance belongs on the platform as rules.
Tagging
Ditto specs support two levels of tagging that mirror how design systems organize components:
- Component-level tags describe the component itself — its role in the design system. A
DialogueModaltagged[dialog, confirmation]pulls in style guide rules about how confirmation dialogs should read. These rules apply to every surface in the component. - Surface-level tags describe each individual piece of text — its function within the component. An
actionTextsurface tagged[call-to-action]pulls in rules specific to CTAs (e.g. "lead with a verb"). These rules apply only to that surface.
Both levels work together: in the DialogueModal example above, the headline surface inherits rules matched by dialog and confirmation (from the component) plus rules matched by heading and dialog-title (from the surface). If a rule matches both levels, it appears once at component level — broader scope wins.
Create style guide rules on the Ditto platform scoped to design system concepts (dialog, card, form, navigation) and they automatically cascade to every component tagged with that concept. Surface-level tags layer on more specific guidance.
Rule hierarchy
| Scope | Where | Applies to |
|---|---|---|
| Workspace | workspace.ditto.md rules[] | Every surface in every component |
| Component-level | Component's rules[], no surface field | Every surface in this component |
| Per-surface | Component's rules[], with surface: "<key>" | That one surface |
| Locale-scoped | locales.<code>[] (workspace or component) | Same hierarchy as above, but only when writing copy for that locale |
Base rules in rules always apply. Locale-scoped rules in locales apply only when writing copy for the matching locale — they never conflict with each other because each locale is a separate scope.
CLI commands
ditto-spec init
First-time setup. Creates dittospec.config.json and workspace.ditto.md in the current directory, then detects your agent environment (Claude Code, Cursor) and prints setup suggestions.
Use --agent to also write agent configuration directly:
- Claude Code: appends a Ditto Specs section to
CLAUDE.md(creates the file if absent) and writes skill files to.claude/commands/(see Agent skills below). - Cursor: appends a Ditto Specs section to
.cursorrules.
Re-running init --agent is safe — it updates skill files to the current CLI version and skips sections that already exist in CLAUDE.md.
ditto-spec scaffold <ComponentName>
Creates a new index.ditto.md Ditto spec file for a component with the correct YAML structure and empty managed keys.
ditto-spec scaffold DialogueModal --path src/components/DialogueModalUse --path <dir> to specify where the file is created (defaults to the current directory). After scaffolding, add a surface for each piece of user-facing text the component renders and run ditto-spec pull to populate rules.
ditto-spec pull
Syncs style guide rules from the platform into Ditto spec files.
- Discovers all
.ditto.mdfiles under configured roots - Fetches style guides from
GET /v2/styleguides - Flattens rules and wordlist entries across all guides (or only those named in
styleguidesconfig) - Separates base rules from locale-scoped rules (included when
localesis configured) - Matches rules to specs by tag intersection (client-side)
- Rewrites the
rulesandlocaleskeys in each file's YAML frontmatter
Use --dry-run to see what would change without writing.
ditto-spec check
Validates all Ditto spec files: YAML parses correctly, required keys are present, surfaces have tags arrays. Exits non-zero on any failure — useful for CI.
ditto-spec list
Prints an inventory of all component Ditto specs with their surfaces, tags, and constraints.
ditto-spec rules
Prints every rule on the platform, grouped by style guide and section (with each section's sectionId and kind). Pass --styleguide "<name-or-id>" to limit to one guide.
ditto-spec create-rules
Creates a batch of rules on the Ditto platform. Takes a JSON array of rules on stdin (or via --file <path>):
ditto-spec create-rules <<'EOF'
[
{"name": "Use active voice", "description": "Lead CTAs with a verb", "tags": ["button", "call-to-action"], "examples": [{"from": "Your settings", "to": "Open settings"}], "section": "UI Patterns"},
{"term": "sign up", "disallowed": ["signup", "sign-up"], "description": "Two words as a verb", "section": "Word List"}
]
EOFEach rule is one of two shapes:
| Shape | Fields |
|---|---|
| Style rule | name (required), description, examples (array of {from, to}), tags, section |
| Terminology entry | term (required), disallowed (array of strings), description, tags, section |
Shapes can be mixed in one batch. Each rule's optional section (name or ID, as shown by ditto-spec rules) maps it to an existing section of the style guide; the section's kind must match the rule's shape (rules sections for style rules, wordlist sections for terminology entries). Rules without a section go to the first section of the matching kind. One API call is made per target section.
| Flag | Description |
|---|---|
| --file <path> | Read the JSON array from a file instead of stdin |
| --styleguide | Target style guide name or ID (optional, overrides defaultStyleguide config) |
| --section | Default section for rules that don't specify their own (optional; applies only to rules matching the section's kind — others fall back to the first section of theirs) |
After creating rules, run ditto-spec pull to sync them into Ditto spec files.
Configuration
Create dittospec.config.json at your repo root (or any ancestor directory):
{
"apiBase": "https://api.dittowords.com",
"roots": ["design-system"]
}| Key | Description |
|---|---|
| apiBase | Ditto API base URL |
| roots | Repo-relative directories to search for .ditto.md files. Defaults to ["."]. |
| styleguides | Optional list of style guide names or IDs to pull. Defaults to all. |
| locales | Optional list of locale codes (e.g. ["de-DE", "fr-FR"]). Includes locale-scoped style guides matching these codes. Base (no-variant) guides are always included. |
| defaultStyleguide | Optional style guide name or ID for create-rules. Overridable with the --styleguide flag. Defaults to the first guide returned by the API. |
Set DITTO_TOKEN in your environment or in a .env file at the repo root.
Agent skills
When you run ditto-spec init --agent in a Claude Code project, the CLI writes three skill files into .claude/commands/. These are slash commands that give agents interactive, multi-step workflows for working with Ditto specs.
| Skill | What it does |
|---|---|
| /ditto-spec-component <Name> | Analyze a component's text surfaces, scaffold a Ditto spec file (or update an existing one), and sync style guide rules from the platform. Handles child components too. |
| /ditto-spec-audit [Name] | Audit copy in component instances against style guide rules. Reports violations with file locations and suggested corrections. Omit the name to audit all specced components. |
| /ditto-spec-gaps [Name] | Find copy patterns that should be style guide rules but aren't. Proposes new style rules and terminology entries, then creates approved ones on the platform via create-rules. |
Skills are written into your repo and committed alongside your Ditto specs and config. Every team member gets them automatically — no separate plugin install.
Updating skills: Re-run ditto-spec init --agent after updating the CLI to get the latest skill versions. Existing skills are overwritten; the CLAUDE.md Ditto Specs section is left untouched if already present.
Customizing skills: The skill files are plain markdown in .claude/commands/. You can edit them to add project-specific behavior (e.g. default tags, custom audit checks). Re-running init --agent will overwrite your changes, so commit customizations and manage updates deliberately.
Agent contract
When writing or editing user-facing text for a component:
- Read
workspace.ditto.md(if it exists) for universal style guide rules. - Read the component's
index.ditto.md. Match each piece of text you're writing to a surface key. - Respect
maxLength— it's a layout invariant, not a suggestion. - Follow all rules in
rules[]. Entries withoutsurfaceapply to every surface; entries withsurfaceapply only to that surface. - Rules come in two shapes:
- Style rules have
name,description, and optionalexamples(before/after pairs). Useexamplesas concrete tone/shape guidance. - Terminology entries have
termanddisallowed. Always use thetermform; never use any of thedisallowedalternatives.
- Style rules have
- Each rule carries a
sectionfield (e.g. "Voice & Tone", "Terminology") providing context for how to interpret it. - If a
localeskey is present, it contains locale-scoped rules keyed by locale code (e.g.de-DE). When writing copy for a specific locale, follow the matchinglocales.<code>rules in addition to the baserules. When writing for the default/base locale, only followrules. Locale-scoped rules never conflict — each locale is a separate boundary.
Creating Ditto specs
When creating a new component that renders any user-facing text, scaffold a Ditto spec file:
npx ditto-spec scaffold <ComponentName> --path <dir>Then edit the generated index.ditto.md to add surfaces — one entry per piece of user-facing text the component renders:
surfaces:
title:
tags: [heading]
maxLength: 60
$children:
tags: [button, cta]
maxLength: 30- Use
$childrenfor text via children. Use dot notation for nested props (primaryAction.label). For hardcoded or internal strings, use a descriptive role name (headline,bodyText,submitLabel). - Check the
tagskey inworkspace.ditto.mdfor tags available on the platform. Prefer reusing an existing tag over creating a new one — only a tag that exists on the platform will match rules. If no existing tag fits, create a new one following the convention of existing tags (lowercase, hyphenated). - Never write
rulesby hand. Runditto-spec pullafter adding surfaces to populate rules from the platform.
Parent and child specs both contribute rules. If your component passes a label to a child Button, add a surface in the parent's spec — the parent's rules (e.g., dialog-level tone) layer with the child's rules (e.g., button-level constraints). A child having its own spec does not exempt the parent from declaring surfaces for text it provides.
If a component lacks a Ditto spec and you'd have found one useful, propose creating one. If a Ditto spec lacks a rule you'd have wanted, propose adding the rule to the platform style guide with appropriate tags.
