@dittowords/spec-cli 0.0.1-alpha.1 → 0.0.1-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -26
- package/dist/api.d.ts +82 -0
- package/dist/api.js +103 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +4 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +87 -0
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.js +57 -0
- package/dist/commands/create-rule.d.ts +9 -0
- package/dist/commands/create-rule.js +33 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +237 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +51 -0
- package/dist/commands/pull.d.ts +5 -0
- package/dist/commands/pull.js +168 -0
- package/dist/commands/scaffold.d.ts +6 -0
- package/dist/commands/scaffold.js +51 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +101 -0
- package/dist/discover.d.ts +5 -0
- package/dist/discover.js +24 -0
- package/dist/parse.d.ts +24 -0
- package/dist/parse.js +42 -0
- package/dist/serialize.d.ts +5 -0
- package/dist/serialize.js +60 -0
- package/dist/skill-content.d.ts +1 -0
- package/dist/skill-content.js +156 -0
- package/dist/skills.d.ts +4 -0
- package/dist/skills.js +284 -0
- package/package.json +19 -5
- package/src/api.ts +0 -45
- package/src/bin.js +0 -3
- package/src/cli.ts +0 -70
- package/src/commands/check.ts +0 -48
- package/src/commands/init.ts +0 -219
- package/src/commands/list.ts +0 -59
- package/src/commands/pull.ts +0 -140
- package/src/commands/scaffold.ts +0 -40
- package/src/config.ts +0 -55
- package/src/discover.ts +0 -23
- package/src/parse.ts +0 -45
- package/src/serialize.ts +0 -38
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
CLI for syncing `.ditto.md` content specs with the Ditto platform.
|
|
6
6
|
|
|
7
|
-
A `.ditto.md` file lives next to a component and declares its **text surfaces** — the props
|
|
7
|
+
A `.ditto.md` 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 file is pure metadata; nothing imports it at runtime. It exists for three consumers:
|
|
8
8
|
|
|
9
9
|
- **Agents** read it as fast-path context when writing or editing copy for the component.
|
|
10
10
|
- **The CLI** (`ditto-spec pull`) syncs style guide rules from the platform whose tags match the file's surface tags.
|
|
@@ -19,9 +19,6 @@ Everything lives in YAML frontmatter. The markdown body below the closing `---`
|
|
|
19
19
|
```yaml
|
|
20
20
|
---
|
|
21
21
|
component: DialogueModal
|
|
22
|
-
description: >
|
|
23
|
-
A two-button confirmation modal for actions that need explicit
|
|
24
|
-
acknowledgement. Used for routine confirms and destructive flows.
|
|
25
22
|
tags: [dialog, confirmation]
|
|
26
23
|
surfaces:
|
|
27
24
|
headline:
|
|
@@ -40,49 +37,71 @@ surfaces:
|
|
|
40
37
|
rules:
|
|
41
38
|
- name: Confirmation dialogs should be direct
|
|
42
39
|
description: Keep confirmation copy terse and unambiguous
|
|
40
|
+
section: Voice & Tone
|
|
43
41
|
- surface: actionText
|
|
44
42
|
name: Calls to action should use active voice
|
|
45
43
|
description: Always lead with a verb
|
|
46
44
|
examples:
|
|
47
45
|
- from: "Your settings"
|
|
48
46
|
to: "Open settings"
|
|
49
|
-
|
|
47
|
+
section: Voice & Tone
|
|
48
|
+
- term: sign up
|
|
49
|
+
disallowed:
|
|
50
|
+
- signup
|
|
51
|
+
- sign-up
|
|
52
|
+
description: Always use as two words (verb form)
|
|
53
|
+
section: Terminology
|
|
54
|
+
locales:
|
|
55
|
+
de-DE:
|
|
56
|
+
- name: Use informal address
|
|
57
|
+
description: Use "Du" instead of "Sie" for all user-facing copy
|
|
58
|
+
section: Formality
|
|
50
59
|
---
|
|
51
60
|
```
|
|
52
61
|
|
|
53
62
|
### Workspace spec (`workspace.ditto.md`)
|
|
54
63
|
|
|
55
|
-
A repo may have a single `workspace.ditto.md` somewhere under the CLI's configured `roots`. It holds universal rules from the workspace style guide that carry no tags — these apply to every surface in every component.
|
|
64
|
+
A repo may have a single `workspace.ditto.md` somewhere under the CLI's configured `roots`. It holds universal rules from the workspace style guide 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`.
|
|
56
65
|
|
|
57
66
|
```yaml
|
|
58
67
|
---
|
|
59
68
|
workspace: true
|
|
60
|
-
description: >
|
|
61
|
-
Workspace-wide content rules. Read alongside any component's index.ditto.md.
|
|
62
69
|
# Managed by Ditto — do not edit below
|
|
63
|
-
|
|
70
|
+
tags: [body, button, call-to-action, dialog-title, heading, nav]
|
|
71
|
+
rules:
|
|
72
|
+
- name: Write in active voice
|
|
73
|
+
description: Lead with verbs, avoid passive constructions
|
|
74
|
+
section: Voice & Tone
|
|
75
|
+
locales:
|
|
76
|
+
de-DE:
|
|
77
|
+
- name: Use informal address
|
|
78
|
+
description: Use "Du" instead of "Sie" for all user-facing copy
|
|
79
|
+
section: Formality
|
|
64
80
|
---
|
|
65
81
|
```
|
|
66
82
|
|
|
67
83
|
### Key concepts
|
|
68
84
|
|
|
69
|
-
**Developer-owned keys**: `component`, `
|
|
85
|
+
**Developer-owned keys**: `component`, `tags`, `surfaces`. Edit these freely.
|
|
70
86
|
|
|
71
|
-
**CLI-managed keys**: `rules`, `
|
|
87
|
+
**CLI-managed keys**: `rules`, `locales`, and workspace `tags`. Overwritten by `ditto-spec pull`. Do not edit by hand. Rules come in two shapes: style rules (`name`/`description`/`examples`) and terminology entries (`term`/`disallowed`/`description`).
|
|
72
88
|
|
|
73
|
-
**Surface keys**
|
|
89
|
+
**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`).
|
|
74
90
|
|
|
75
91
|
**Component-level `tags`** cause matching rules to apply to every surface in the component (emitted in `rules` with no `surface` field). Per-surface tags cause rules to emit with `surface: "<key>"`. If a rule matches both levels, it emits once at component level (broader scope wins).
|
|
76
92
|
|
|
77
93
|
**`maxLength` / `minLength`** are hard layout constraints, not stylistic preferences. Stylistic guidance belongs on the platform as rules.
|
|
78
94
|
|
|
79
|
-
###
|
|
95
|
+
### Rule hierarchy
|
|
80
96
|
|
|
81
97
|
| Scope | Where | Applies to |
|
|
82
98
|
|---|---|---|
|
|
83
99
|
| **Workspace** | `workspace.ditto.md` `rules[]` | Every surface in every component |
|
|
84
100
|
| **Component-level** | Component's `rules[]`, no `surface` field | Every surface in this component |
|
|
85
101
|
| **Per-surface** | Component's `rules[]`, with `surface: "<key>"` | That one surface |
|
|
102
|
+
| **Locale-scoped** | `locales.<code>[]` (workspace or component) | Same hierarchy as above, but only when writing copy for that locale |
|
|
103
|
+
|
|
104
|
+
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.
|
|
86
105
|
|
|
87
106
|
## CLI commands
|
|
88
107
|
|
|
@@ -90,7 +109,12 @@ rules: []
|
|
|
90
109
|
|
|
91
110
|
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.
|
|
92
111
|
|
|
93
|
-
Use `--agent` to also write agent configuration directly:
|
|
112
|
+
Use `--agent` to also write agent configuration directly:
|
|
113
|
+
|
|
114
|
+
- **Claude Code**: appends a Ditto Content Specs section to `CLAUDE.md` (creates the file if absent) and writes skill files to `.claude/commands/` (see [Agent skills](#agent-skills) below).
|
|
115
|
+
- **Cursor**: appends a Ditto Content Specs section to `.cursorrules`.
|
|
116
|
+
|
|
117
|
+
Re-running `init --agent` is safe — it updates skill files to the current CLI version and skips sections that already exist in `CLAUDE.md`.
|
|
94
118
|
|
|
95
119
|
### `ditto-spec scaffold <ComponentName>`
|
|
96
120
|
|
|
@@ -100,16 +124,18 @@ Creates a new `index.ditto.md` for a component with the correct YAML structure a
|
|
|
100
124
|
ditto-spec scaffold DialogueModal --path src/components/DialogueModal
|
|
101
125
|
```
|
|
102
126
|
|
|
103
|
-
Use `--path <dir>` to specify where the file is created (defaults to the current directory). After scaffolding, add
|
|
127
|
+
Use `--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.
|
|
104
128
|
|
|
105
129
|
### `ditto-spec pull`
|
|
106
130
|
|
|
107
131
|
Syncs rules from the platform into spec files.
|
|
108
132
|
|
|
109
133
|
1. Discovers all `.ditto.md` files under configured roots
|
|
110
|
-
2. Fetches
|
|
111
|
-
3.
|
|
112
|
-
4.
|
|
134
|
+
2. Fetches style guides from `GET /v2/styleguides`
|
|
135
|
+
3. Flattens rules and wordlist entries across all guides (or only those named in `styleguides` config)
|
|
136
|
+
4. Separates base rules from locale-scoped rules (included when `locales` is configured)
|
|
137
|
+
5. Matches rules to specs by tag intersection (client-side)
|
|
138
|
+
6. Rewrites the `rules` and `locales` keys in each file's YAML frontmatter
|
|
113
139
|
|
|
114
140
|
Use `--dry-run` to see what would change without writing.
|
|
115
141
|
|
|
@@ -121,6 +147,24 @@ Validates all spec files: YAML parses correctly, required keys are present, surf
|
|
|
121
147
|
|
|
122
148
|
Prints an inventory of all component specs with their surfaces, tags, and constraints.
|
|
123
149
|
|
|
150
|
+
### `ditto-spec create-rule`
|
|
151
|
+
|
|
152
|
+
Creates a new rule on the Ditto platform.
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
ditto-spec create-rule --name "Use active voice" --description "Lead CTAs with a verb" --tags "button,call-to-action" --examples '[{"from":"Your settings","to":"Open settings"}]'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Flag | Description |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `--name` | Rule name (required) |
|
|
161
|
+
| `--description` | What the rule enforces (required) |
|
|
162
|
+
| `--styleguide` | Target style guide name or ID (optional, overrides `defaultStyleguide` config) |
|
|
163
|
+
| `--tags` | Comma-separated tags to scope the rule (optional) |
|
|
164
|
+
| `--examples` | JSON array of `{from, to}` pairs (optional) |
|
|
165
|
+
|
|
166
|
+
After creating rules, run `ditto-spec pull` to sync them into spec files.
|
|
167
|
+
|
|
124
168
|
## Configuration
|
|
125
169
|
|
|
126
170
|
Create `dittospec.config.json` at your repo root (or any ancestor directory):
|
|
@@ -138,28 +182,51 @@ Create `dittospec.config.json` at your repo root (or any ancestor directory):
|
|
|
138
182
|
| `apiBase` | Ditto API base URL |
|
|
139
183
|
| `workspaceId` | Your workspace ID |
|
|
140
184
|
| `roots` | Repo-relative directories to search for `.ditto.md` files. Defaults to `["."]`. |
|
|
185
|
+
| `styleguides` | Optional list of style guide names or IDs to pull. Defaults to all. |
|
|
186
|
+
| `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. |
|
|
187
|
+
| `defaultStyleguide` | Optional style guide name or ID for `create-rule`. Overridable with the `--styleguide` flag. Defaults to the first guide returned by the API. |
|
|
141
188
|
|
|
142
189
|
Set `DITTO_API_KEY` in your environment or in a `.env` file at the repo root.
|
|
143
190
|
|
|
191
|
+
## Agent skills
|
|
192
|
+
|
|
193
|
+
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.
|
|
194
|
+
|
|
195
|
+
| Skill | What it does |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `/spec-component <Name>` | Analyze a component's text surfaces, scaffold a `.ditto.md` spec (or update an existing one), and sync rules from the platform. Handles child components too. |
|
|
198
|
+
| `/spec-audit [Name]` | Audit copy in component instances against spec rules. Reports violations with file locations and suggested corrections. Omit the name to audit all specced components. |
|
|
199
|
+
| `/spec-gaps [Name]` | Find copy patterns that should be rules but aren't. Proposes new style rules and terminology entries, then creates approved ones on the platform via `create-rule`. |
|
|
200
|
+
|
|
201
|
+
Skills are written into your repo and committed alongside your specs and config. Every team member gets them automatically — no separate plugin install.
|
|
202
|
+
|
|
203
|
+
**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 section is left untouched if already present.
|
|
204
|
+
|
|
205
|
+
**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.
|
|
206
|
+
|
|
144
207
|
## Agent contract
|
|
145
208
|
|
|
146
|
-
When writing or editing text
|
|
209
|
+
When writing or editing user-facing text for a component:
|
|
147
210
|
|
|
148
211
|
1. Read `workspace.ditto.md` (if it exists) for universal rules.
|
|
149
|
-
2. Read the component's `index.ditto.md`. Match each
|
|
212
|
+
2. Read the component's `index.ditto.md`. Match each piece of text you're writing to a surface key.
|
|
150
213
|
3. Respect `maxLength` — it's a layout invariant, not a suggestion.
|
|
151
214
|
4. Follow all rules in `rules[]`. Entries without `surface` apply to every surface; entries with `surface` apply only to that surface.
|
|
152
|
-
5.
|
|
215
|
+
5. Rules come in two shapes:
|
|
216
|
+
- **Style rules** have `name`, `description`, and optional `examples` (before/after pairs). Use `examples` as concrete tone/shape guidance.
|
|
217
|
+
- **Terminology entries** have `term` and `disallowed`. Always use the `term` form; never use any of the `disallowed` alternatives.
|
|
218
|
+
6. Each rule carries a `section` field (e.g. "Voice & Tone", "Terminology") providing context for how to interpret it.
|
|
219
|
+
7. If a `locales` key is present, it contains locale-scoped rules keyed by locale code (e.g. `de-DE`). When writing copy for a specific locale, follow the matching `locales.<code>` rules **in addition to** the base `rules`. When writing for the default/base locale, only follow `rules`. Locale-scoped rules never conflict — each locale is a separate boundary.
|
|
153
220
|
|
|
154
221
|
### Creating specs
|
|
155
222
|
|
|
156
|
-
When creating a new component
|
|
223
|
+
When creating a new component that renders any user-facing text, scaffold a spec file:
|
|
157
224
|
|
|
158
225
|
```
|
|
159
226
|
npx ditto-spec scaffold <ComponentName> --path <dir>
|
|
160
227
|
```
|
|
161
228
|
|
|
162
|
-
Then edit the generated `index.ditto.md` to add surfaces — one entry per
|
|
229
|
+
Then edit the generated `index.ditto.md` to add surfaces — one entry per piece of user-facing text the component renders:
|
|
163
230
|
|
|
164
231
|
```yaml
|
|
165
232
|
surfaces:
|
|
@@ -171,8 +238,10 @@ surfaces:
|
|
|
171
238
|
maxLength: 30
|
|
172
239
|
```
|
|
173
240
|
|
|
174
|
-
- Use `$children` for text via children. Use dot notation for nested props (`primaryAction.label`).
|
|
175
|
-
-
|
|
176
|
-
- **Never write `rules`
|
|
241
|
+
- Use `$children` for text via children. Use dot notation for nested props (`primaryAction.label`). For hardcoded or internal strings, use a descriptive role name (`headline`, `bodyText`, `submitLabel`).
|
|
242
|
+
- Check the `tags` key in `workspace.ditto.md` for 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).
|
|
243
|
+
- **Never write `rules` by hand.** Run `ditto-spec pull` after adding surfaces to populate rules from the platform.
|
|
244
|
+
|
|
245
|
+
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.
|
|
177
246
|
|
|
178
247
|
If a component lacks a spec and you'd have found one useful, propose creating one. If a spec lacks a rule you'd have wanted, propose adding the rule to the platform style guide with appropriate tags.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Config } from "./config";
|
|
2
|
+
export interface StyleguideVariant {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
localeCode: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface StyleRule {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
examples: {
|
|
11
|
+
from: string;
|
|
12
|
+
to: string;
|
|
13
|
+
}[];
|
|
14
|
+
tags: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface WordlistEntry {
|
|
17
|
+
term: string;
|
|
18
|
+
disallowed: string[];
|
|
19
|
+
description: string;
|
|
20
|
+
tags: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface StyleguideSection {
|
|
23
|
+
name: string;
|
|
24
|
+
kind: "rules" | "wordlist";
|
|
25
|
+
rules: StyleRule[] | WordlistEntry[];
|
|
26
|
+
}
|
|
27
|
+
export interface Styleguide {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
variant: StyleguideVariant | null;
|
|
32
|
+
sections: StyleguideSection[];
|
|
33
|
+
}
|
|
34
|
+
export type FlatRule = {
|
|
35
|
+
kind: "style";
|
|
36
|
+
styleguide: string;
|
|
37
|
+
section: string;
|
|
38
|
+
localeCode: string | null;
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
examples: {
|
|
42
|
+
from: string;
|
|
43
|
+
to: string;
|
|
44
|
+
}[];
|
|
45
|
+
tags: string[];
|
|
46
|
+
} | {
|
|
47
|
+
kind: "wordlist";
|
|
48
|
+
styleguide: string;
|
|
49
|
+
section: string;
|
|
50
|
+
localeCode: string | null;
|
|
51
|
+
term: string;
|
|
52
|
+
disallowed: string[];
|
|
53
|
+
description: string;
|
|
54
|
+
tags: string[];
|
|
55
|
+
};
|
|
56
|
+
export declare function flattenStyleguides(guides: Styleguide[], filter?: string[]): FlatRule[];
|
|
57
|
+
export declare class DittoApi {
|
|
58
|
+
private readonly config;
|
|
59
|
+
private readonly apiKey;
|
|
60
|
+
constructor(config: Config, apiKey: string);
|
|
61
|
+
getStyleguides(): Promise<Styleguide[]>;
|
|
62
|
+
getStyleguideInfo(styleguideOverride?: string): Promise<{
|
|
63
|
+
styleguideId: string;
|
|
64
|
+
sectionId: string;
|
|
65
|
+
} | null>;
|
|
66
|
+
createRule(opts: {
|
|
67
|
+
styleguideId: string;
|
|
68
|
+
sectionId: string;
|
|
69
|
+
name: string;
|
|
70
|
+
description: string;
|
|
71
|
+
examples?: {
|
|
72
|
+
from: string;
|
|
73
|
+
to: string;
|
|
74
|
+
}[];
|
|
75
|
+
tags?: string[];
|
|
76
|
+
}): Promise<{
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
}>;
|
|
80
|
+
private fetch;
|
|
81
|
+
private headers;
|
|
82
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DittoApi = void 0;
|
|
4
|
+
exports.flattenStyleguides = flattenStyleguides;
|
|
5
|
+
function flattenStyleguides(guides, filter) {
|
|
6
|
+
const selected = filter?.length
|
|
7
|
+
? guides.filter((g) => filter.includes(g.name) || filter.includes(g.id))
|
|
8
|
+
: guides;
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const guide of selected) {
|
|
11
|
+
for (const section of guide.sections) {
|
|
12
|
+
if (section.kind === "rules") {
|
|
13
|
+
for (const rule of section.rules) {
|
|
14
|
+
result.push({
|
|
15
|
+
kind: "style",
|
|
16
|
+
styleguide: guide.name,
|
|
17
|
+
section: section.name,
|
|
18
|
+
localeCode: guide.variant?.localeCode ?? null,
|
|
19
|
+
name: rule.name,
|
|
20
|
+
description: rule.description,
|
|
21
|
+
examples: rule.examples,
|
|
22
|
+
tags: rule.tags,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const entry of section.rules) {
|
|
28
|
+
result.push({
|
|
29
|
+
kind: "wordlist",
|
|
30
|
+
styleguide: guide.name,
|
|
31
|
+
section: section.name,
|
|
32
|
+
localeCode: guide.variant?.localeCode ?? null,
|
|
33
|
+
term: entry.term,
|
|
34
|
+
disallowed: entry.disallowed,
|
|
35
|
+
description: entry.description,
|
|
36
|
+
tags: entry.tags,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
class DittoApi {
|
|
45
|
+
constructor(config, apiKey) {
|
|
46
|
+
this.config = config;
|
|
47
|
+
this.apiKey = apiKey;
|
|
48
|
+
}
|
|
49
|
+
async getStyleguides() {
|
|
50
|
+
const url = new URL("/v2/styleguides", this.config.apiBase);
|
|
51
|
+
const res = await this.fetch(url, { method: "GET" });
|
|
52
|
+
const data = (await res.json());
|
|
53
|
+
return data.styleguides;
|
|
54
|
+
}
|
|
55
|
+
async getStyleguideInfo(styleguideOverride) {
|
|
56
|
+
const guides = await this.getStyleguides();
|
|
57
|
+
if (guides.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
const target = styleguideOverride ?? this.config.defaultStyleguide;
|
|
60
|
+
const guide = target
|
|
61
|
+
? guides.find((g) => g.name === target || g.id === target) ?? guides[0]
|
|
62
|
+
: guides[0];
|
|
63
|
+
if (guide.sections.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const section = guide.sections.find((s) => s.kind === "rules") ?? guide.sections[0];
|
|
66
|
+
return { styleguideId: guide.name, sectionId: section.name };
|
|
67
|
+
}
|
|
68
|
+
async createRule(opts) {
|
|
69
|
+
const url = new URL(`/v2/styleguides/${encodeURIComponent(opts.styleguideId)}/rules`, this.config.apiBase);
|
|
70
|
+
const res = await this.fetch(url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
sectionId: opts.sectionId,
|
|
74
|
+
name: opts.name,
|
|
75
|
+
description: opts.description,
|
|
76
|
+
examples: opts.examples,
|
|
77
|
+
tags: opts.tags,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
return (await res.json());
|
|
81
|
+
}
|
|
82
|
+
async fetch(url, init) {
|
|
83
|
+
const res = await fetch(url.toString(), {
|
|
84
|
+
...init,
|
|
85
|
+
headers: { ...this.headers(init.method ?? "GET"), ...(init.headers ?? {}) },
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
const body = await res.text();
|
|
89
|
+
throw new Error(`${init.method} ${url} → ${res.status}: ${body}`);
|
|
90
|
+
}
|
|
91
|
+
return res;
|
|
92
|
+
}
|
|
93
|
+
headers(method) {
|
|
94
|
+
const h = {
|
|
95
|
+
authorization: this.apiKey, // Ditto API expects bare key, no Bearer prefix
|
|
96
|
+
workspace_id: this.config.workspaceId,
|
|
97
|
+
};
|
|
98
|
+
if (method !== "GET")
|
|
99
|
+
h["content-type"] = "application/json";
|
|
100
|
+
return h;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.DittoApi = DittoApi;
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const check_1 = require("./commands/check");
|
|
4
|
+
const create_rule_1 = require("./commands/create-rule");
|
|
5
|
+
const init_1 = require("./commands/init");
|
|
6
|
+
const list_1 = require("./commands/list");
|
|
7
|
+
const pull_1 = require("./commands/pull");
|
|
8
|
+
const scaffold_1 = require("./commands/scaffold");
|
|
9
|
+
const COMMANDS = {
|
|
10
|
+
init: async (args) => (0, init_1.init)({ writeAgent: args.includes("--agent"), force: args.includes("--force") }),
|
|
11
|
+
pull: async (args) => (0, pull_1.pull)({ dryRun: args.includes("--dry-run") }),
|
|
12
|
+
check: async () => (0, check_1.check)(),
|
|
13
|
+
list: async () => (0, list_1.list)(),
|
|
14
|
+
scaffold: async (args) => {
|
|
15
|
+
const name = args.find((a) => !a.startsWith("--"));
|
|
16
|
+
if (!name) {
|
|
17
|
+
process.stderr.write("Usage: ditto-spec scaffold <ComponentName> [--path <dir>]\n");
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
const pathIdx = args.indexOf("--path");
|
|
21
|
+
const targetDir = pathIdx !== -1 && args[pathIdx + 1] ? args[pathIdx + 1] : process.cwd();
|
|
22
|
+
return (0, scaffold_1.scaffold)({ componentName: name, targetDir });
|
|
23
|
+
},
|
|
24
|
+
"create-rule": async (args) => {
|
|
25
|
+
function getFlag(flag) {
|
|
26
|
+
const idx = args.indexOf(flag);
|
|
27
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
|
|
28
|
+
}
|
|
29
|
+
const name = getFlag("--name");
|
|
30
|
+
const description = getFlag("--description");
|
|
31
|
+
if (!name || !description) {
|
|
32
|
+
process.stderr.write('Usage: ditto-spec create-rule --name "<name>" --description "<desc>" [--styleguide "<name-or-id>"] [--tags "<t1,t2>"] [--examples \'<json>\']\n');
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
return (0, create_rule_1.createRule)({ name, description, styleguide: getFlag("--styleguide"), tags: getFlag("--tags"), examples: getFlag("--examples") });
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const HELP = `ditto-spec — sync .ditto.md content specs with the Ditto platform.
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
ditto-spec <command> [options]
|
|
42
|
+
|
|
43
|
+
Commands:
|
|
44
|
+
init Set up ditto specs: creates config, workspace spec, and prints agent setup.
|
|
45
|
+
init --agent Also writes agent configuration (CLAUDE.md, .cursorrules, etc.).
|
|
46
|
+
init --force Overwrite locally modified skill files in .claude/commands/.
|
|
47
|
+
scaffold <Name> Create a new index.ditto.md for a component.
|
|
48
|
+
scaffold <Name> --path <dir> Create the spec in a specific directory.
|
|
49
|
+
pull Pull rules from the platform into the managed keys of each spec.
|
|
50
|
+
pull --dry-run Show which files would change without writing.
|
|
51
|
+
check Parse every spec file; exit non-zero on any malformed file.
|
|
52
|
+
list Print every component spec with its surfaces and tags.
|
|
53
|
+
create-rule Create a rule on the platform.
|
|
54
|
+
--name "<name>" Rule name (required)
|
|
55
|
+
--description "<desc>" Rule description (required)
|
|
56
|
+
--styleguide "<name-or-id>" Target style guide (optional, overrides config)
|
|
57
|
+
--tags "<t1,t2>" Comma-separated tags (optional)
|
|
58
|
+
--examples '<json>' JSON array of {from,to} pairs (optional)
|
|
59
|
+
|
|
60
|
+
Environment:
|
|
61
|
+
DITTO_API_KEY Workspace API key (required for pull).
|
|
62
|
+
|
|
63
|
+
Config:
|
|
64
|
+
Reads dittospec.config.json from the nearest ancestor directory:
|
|
65
|
+
{ "apiBase": "https://...", "workspaceId": "...", "roots": ["design-system"] }
|
|
66
|
+
`;
|
|
67
|
+
async function main() {
|
|
68
|
+
const args = process.argv.slice(2);
|
|
69
|
+
const cmd = args[0];
|
|
70
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
71
|
+
process.stdout.write(HELP);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const handler = COMMANDS[cmd];
|
|
75
|
+
if (!handler) {
|
|
76
|
+
process.stderr.write(`Unknown command: ${cmd}\n\n${HELP}`);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await handler(args.slice(1));
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.error(err instanceof Error ? err.message : err);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function check(): Promise<void>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.check = check;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
const discover_1 = require("../discover");
|
|
10
|
+
const parse_1 = require("../parse");
|
|
11
|
+
async function check() {
|
|
12
|
+
const { config, root } = (0, config_1.loadConfig)();
|
|
13
|
+
const files = (0, discover_1.discover)(root, config.roots);
|
|
14
|
+
if (files.length === 0) {
|
|
15
|
+
console.log("No .ditto.md files found.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let failed = 0;
|
|
19
|
+
for (const f of files) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = (0, parse_1.parseSpecFile)(f.abs);
|
|
22
|
+
const specLike = parsed.spec;
|
|
23
|
+
if (parsed.kind === "workspace") {
|
|
24
|
+
if (specLike.tags !== undefined && !Array.isArray(specLike.tags)) {
|
|
25
|
+
throw new Error("workspace spec 'tags' must be an array");
|
|
26
|
+
}
|
|
27
|
+
if (specLike.rules !== undefined && !Array.isArray(specLike.rules)) {
|
|
28
|
+
throw new Error("workspace spec 'rules' must be an array");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (parsed.kind === "component") {
|
|
32
|
+
const surfaces = specLike.surfaces;
|
|
33
|
+
if (!surfaces || typeof surfaces !== "object") {
|
|
34
|
+
throw new Error("component spec missing 'surfaces' object");
|
|
35
|
+
}
|
|
36
|
+
for (const [key, val] of Object.entries(surfaces)) {
|
|
37
|
+
if (!val || typeof val !== "object") {
|
|
38
|
+
throw new Error(`surface '${key}' must be an object`);
|
|
39
|
+
}
|
|
40
|
+
const surface = val;
|
|
41
|
+
if (!Array.isArray(surface.tags)) {
|
|
42
|
+
throw new Error(`surface '${key}' missing 'tags' array`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
console.log(`✓ ${path_1.default.relative(root, f.abs)}`);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
failed++;
|
|
50
|
+
console.error(`× ${path_1.default.relative(root, f.abs)}: ${err instanceof Error ? err.message : err}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (failed > 0) {
|
|
54
|
+
console.error(`\n${failed} file(s) failed validation.`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRule = createRule;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const config_1 = require("../config");
|
|
6
|
+
async function createRule(opts) {
|
|
7
|
+
const { config } = (0, config_1.loadConfig)();
|
|
8
|
+
const apiKey = (0, config_1.getApiKey)();
|
|
9
|
+
const api = new api_1.DittoApi(config, apiKey);
|
|
10
|
+
const info = await api.getStyleguideInfo(opts.styleguide);
|
|
11
|
+
if (!info) {
|
|
12
|
+
throw new Error("Could not discover style guide sections. Check that your workspace has at least one style guide with rules.");
|
|
13
|
+
}
|
|
14
|
+
let examples;
|
|
15
|
+
if (opts.examples) {
|
|
16
|
+
try {
|
|
17
|
+
examples = JSON.parse(opts.examples);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('--examples must be valid JSON, e.g. \'[{"from":"bad","to":"good"}]\'');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()).filter(Boolean) : undefined;
|
|
24
|
+
const result = await api.createRule({
|
|
25
|
+
styleguideId: info.styleguideId,
|
|
26
|
+
sectionId: info.sectionId,
|
|
27
|
+
name: opts.name,
|
|
28
|
+
description: opts.description,
|
|
29
|
+
examples,
|
|
30
|
+
tags,
|
|
31
|
+
});
|
|
32
|
+
console.log(`✓ Created rule "${result.name}" (${result.id})`);
|
|
33
|
+
}
|