@dittowords/spec-cli 0.0.1-alpha.0 → 0.0.1-alpha.10

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 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 (and `children`) that hold user-facing copy. The file is pure metadata; nothing imports it at runtime. It exists for three consumers:
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
- examples: []
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
- rules: []
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`, `description`, `tags`, `surfaces`. Edit these freely.
85
+ **Developer-owned keys**: `component`, `tags`, `surfaces`. Edit these freely.
70
86
 
71
- **CLI-managed keys**: `rules`, `examples`. Overwritten by `ditto-spec pull`. Do not edit by hand.
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** are prop paths on the component. Dot-notation works for nested props (e.g., `primaryAction.label`). Use `$children` for components whose text comes through the `children` prop.
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
- ### Three-level rule hierarchy
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,16 +109,33 @@ 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: appends a Ditto Content Specs section to `CLAUDE.md` or `.cursorrules` (creates the file if absent).
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`.
118
+
119
+ ### `ditto-spec scaffold <ComponentName>`
120
+
121
+ Creates a new `index.ditto.md` for a component with the correct YAML structure and empty managed keys.
122
+
123
+ ```
124
+ ditto-spec scaffold DialogueModal --path src/components/DialogueModal
125
+ ```
126
+
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.
94
128
 
95
129
  ### `ditto-spec pull`
96
130
 
97
131
  Syncs rules from the platform into spec files.
98
132
 
99
133
  1. Discovers all `.ditto.md` files under configured roots
100
- 2. Fetches all workspace rules from `GET /v2/rules/mcp`
101
- 3. Matches rules to specs by tag intersection (client-side)
102
- 4. Rewrites the `rules` key in each file's YAML frontmatter
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
103
139
 
104
140
  Use `--dry-run` to see what would change without writing.
105
141
 
@@ -111,6 +147,23 @@ Validates all spec files: YAML parses correctly, required keys are present, surf
111
147
 
112
148
  Prints an inventory of all component specs with their surfaces, tags, and constraints.
113
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
+ | `--tags` | Comma-separated tags to scope the rule (optional) |
163
+ | `--examples` | JSON array of `{from, to}` pairs (optional) |
164
+
165
+ After creating rules, run `ditto-spec pull` to sync them into spec files.
166
+
114
167
  ## Configuration
115
168
 
116
169
  Create `dittospec.config.json` at your repo root (or any ancestor directory):
@@ -128,21 +181,66 @@ Create `dittospec.config.json` at your repo root (or any ancestor directory):
128
181
  | `apiBase` | Ditto API base URL |
129
182
  | `workspaceId` | Your workspace ID |
130
183
  | `roots` | Repo-relative directories to search for `.ditto.md` files. Defaults to `["."]`. |
184
+ | `styleguides` | Optional list of style guide names to pull. Defaults to all. |
185
+ | `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. |
186
+ | `styleguideId` | Optional style guide name for `create-rule`. Defaults to the first guide returned by the API. |
131
187
 
132
188
  Set `DITTO_API_KEY` in your environment or in a `.env` file at the repo root.
133
189
 
190
+ ## Agent skills
191
+
192
+ 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.
193
+
194
+ | Skill | What it does |
195
+ |---|---|
196
+ | `/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. |
197
+ | `/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. |
198
+ | `/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`. |
199
+
200
+ Skills are written into your repo and committed alongside your specs and config. Every team member gets them automatically — no separate plugin install.
201
+
202
+ **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.
203
+
204
+ **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.
205
+
134
206
  ## Agent contract
135
207
 
136
- When writing or editing text props for a component:
208
+ When writing or editing user-facing text for a component:
137
209
 
138
210
  1. Read `workspace.ditto.md` (if it exists) for universal rules.
139
- 2. Read the component's `index.ditto.md`. Match each prop you're filling to a surface key.
211
+ 2. Read the component's `index.ditto.md`. Match each piece of text you're writing to a surface key.
140
212
  3. Respect `maxLength` — it's a layout invariant, not a suggestion.
141
213
  4. Follow all rules in `rules[]`. Entries without `surface` apply to every surface; entries with `surface` apply only to that surface.
142
- 5. Reference `examples[]` entries with `status: "approved"` as concrete tone/shape guidance.
214
+ 5. Rules come in two shapes:
215
+ - **Style rules** have `name`, `description`, and optional `examples` (before/after pairs). Use `examples` as concrete tone/shape guidance.
216
+ - **Terminology entries** have `term` and `disallowed`. Always use the `term` form; never use any of the `disallowed` alternatives.
217
+ 6. Each rule carries a `section` field (e.g. "Voice & Tone", "Terminology") providing context for how to interpret it.
218
+ 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.
143
219
 
144
220
  ### Creating specs
145
221
 
146
- When scaffolding a new design-system component with text-bearing props, create an `index.ditto.md` alongside it. Declare a surface for each prop that holds user-facing copy. Tag surfaces with relevant content categories (e.g., `heading`, `body`, `button`, `cta`) so platform rules propagate on the next `pull`.
222
+ When creating a new component that renders any user-facing text, scaffold a spec file:
223
+
224
+ ```
225
+ npx ditto-spec scaffold <ComponentName> --path <dir>
226
+ ```
227
+
228
+ Then edit the generated `index.ditto.md` to add surfaces — one entry per piece of user-facing text the component renders:
229
+
230
+ ```yaml
231
+ surfaces:
232
+ title:
233
+ tags: [heading]
234
+ maxLength: 60
235
+ $children:
236
+ tags: [button, cta]
237
+ maxLength: 30
238
+ ```
239
+
240
+ - 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`).
241
+ - 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).
242
+ - **Never write `rules` by hand.** Run `ditto-spec pull` after adding surfaces to populate rules from the platform.
243
+
244
+ 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.
147
245
 
148
246
  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,81 @@
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
+ name: string;
29
+ description: string;
30
+ variant: StyleguideVariant | null;
31
+ sections: StyleguideSection[];
32
+ }
33
+ export type FlatRule = {
34
+ kind: "style";
35
+ styleguide: string;
36
+ section: string;
37
+ localeCode: string | null;
38
+ name: string;
39
+ description: string;
40
+ examples: {
41
+ from: string;
42
+ to: string;
43
+ }[];
44
+ tags: string[];
45
+ } | {
46
+ kind: "wordlist";
47
+ styleguide: string;
48
+ section: string;
49
+ localeCode: string | null;
50
+ term: string;
51
+ disallowed: string[];
52
+ description: string;
53
+ tags: string[];
54
+ };
55
+ export declare function flattenStyleguides(guides: Styleguide[], filter?: string[]): FlatRule[];
56
+ export declare class DittoApi {
57
+ private readonly config;
58
+ private readonly apiKey;
59
+ constructor(config: Config, apiKey: string);
60
+ getStyleguides(): Promise<Styleguide[]>;
61
+ getStyleguideInfo(): Promise<{
62
+ styleguideId: string;
63
+ sectionId: string;
64
+ } | null>;
65
+ createRule(opts: {
66
+ styleguideId: string;
67
+ sectionId: string;
68
+ name: string;
69
+ description: string;
70
+ examples?: {
71
+ from: string;
72
+ to: string;
73
+ }[];
74
+ tags?: string[];
75
+ }): Promise<{
76
+ id: string;
77
+ name: string;
78
+ }>;
79
+ private fetch;
80
+ private headers;
81
+ }
package/dist/api.js ADDED
@@ -0,0 +1,101 @@
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 ? guides.filter((g) => filter.includes(g.name)) : guides;
7
+ const result = [];
8
+ for (const guide of selected) {
9
+ for (const section of guide.sections) {
10
+ if (section.kind === "rules") {
11
+ for (const rule of section.rules) {
12
+ result.push({
13
+ kind: "style",
14
+ styleguide: guide.name,
15
+ section: section.name,
16
+ localeCode: guide.variant?.localeCode ?? null,
17
+ name: rule.name,
18
+ description: rule.description,
19
+ examples: rule.examples,
20
+ tags: rule.tags,
21
+ });
22
+ }
23
+ }
24
+ else {
25
+ for (const entry of section.rules) {
26
+ result.push({
27
+ kind: "wordlist",
28
+ styleguide: guide.name,
29
+ section: section.name,
30
+ localeCode: guide.variant?.localeCode ?? null,
31
+ term: entry.term,
32
+ disallowed: entry.disallowed,
33
+ description: entry.description,
34
+ tags: entry.tags,
35
+ });
36
+ }
37
+ }
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ class DittoApi {
43
+ constructor(config, apiKey) {
44
+ this.config = config;
45
+ this.apiKey = apiKey;
46
+ }
47
+ async getStyleguides() {
48
+ const url = new URL("/v2/styleguides", this.config.apiBase);
49
+ const res = await this.fetch(url, { method: "GET" });
50
+ const data = (await res.json());
51
+ return data.styleguides;
52
+ }
53
+ async getStyleguideInfo() {
54
+ const guides = await this.getStyleguides();
55
+ if (guides.length === 0)
56
+ return null;
57
+ const guide = this.config.styleguideId
58
+ ? guides.find((g) => g.name === this.config.styleguideId) ?? guides[0]
59
+ : guides[0];
60
+ if (guide.sections.length === 0)
61
+ return null;
62
+ // Use the first rules section; the API requires a section ID for rule creation
63
+ const section = guide.sections.find((s) => s.kind === "rules") ?? guide.sections[0];
64
+ return { styleguideId: guide.name, sectionId: section.name };
65
+ }
66
+ async createRule(opts) {
67
+ const url = new URL(`/v2/styleguides/${encodeURIComponent(opts.styleguideId)}/rules`, this.config.apiBase);
68
+ const res = await this.fetch(url, {
69
+ method: "POST",
70
+ body: JSON.stringify({
71
+ sectionId: opts.sectionId,
72
+ name: opts.name,
73
+ description: opts.description,
74
+ examples: opts.examples,
75
+ tags: opts.tags,
76
+ }),
77
+ });
78
+ return (await res.json());
79
+ }
80
+ async fetch(url, init) {
81
+ const res = await fetch(url.toString(), {
82
+ ...init,
83
+ headers: { ...this.headers(init.method ?? "GET"), ...(init.headers ?? {}) },
84
+ });
85
+ if (!res.ok) {
86
+ const body = await res.text();
87
+ throw new Error(`${init.method} ${url} → ${res.status}: ${body}`);
88
+ }
89
+ return res;
90
+ }
91
+ headers(method) {
92
+ const h = {
93
+ authorization: this.apiKey, // Ditto API expects bare key, no Bearer prefix
94
+ workspace_id: this.config.workspaceId,
95
+ };
96
+ if (method !== "GET")
97
+ h["content-type"] = "application/json";
98
+ return h;
99
+ }
100
+ }
101
+ exports.DittoApi = DittoApi;
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./cli";
package/dist/bin.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ require("./cli");
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,86 @@
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>" [--tags "<t1,t2>"] [--examples \'<json>\']\n');
33
+ process.exit(2);
34
+ }
35
+ return (0, create_rule_1.createRule)({ name, description, 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
+ --tags "<t1,t2>" Comma-separated tags (optional)
57
+ --examples '<json>' JSON array of {from,to} pairs (optional)
58
+
59
+ Environment:
60
+ DITTO_API_KEY Workspace API key (required for pull).
61
+
62
+ Config:
63
+ Reads dittospec.config.json from the nearest ancestor directory:
64
+ { "apiBase": "https://...", "workspaceId": "...", "roots": ["design-system"] }
65
+ `;
66
+ async function main() {
67
+ const args = process.argv.slice(2);
68
+ const cmd = args[0];
69
+ if (!cmd || cmd === "--help" || cmd === "-h") {
70
+ process.stdout.write(HELP);
71
+ return;
72
+ }
73
+ const handler = COMMANDS[cmd];
74
+ if (!handler) {
75
+ process.stderr.write(`Unknown command: ${cmd}\n\n${HELP}`);
76
+ process.exit(2);
77
+ }
78
+ try {
79
+ await handler(args.slice(1));
80
+ }
81
+ catch (err) {
82
+ console.error(err instanceof Error ? err.message : err);
83
+ process.exit(1);
84
+ }
85
+ }
86
+ 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,8 @@
1
+ interface CreateRuleOptions {
2
+ name: string;
3
+ description: string;
4
+ tags?: string;
5
+ examples?: string;
6
+ }
7
+ export declare function createRule(opts: CreateRuleOptions): Promise<void>;
8
+ export {};
@@ -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();
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
+ }
@@ -0,0 +1,6 @@
1
+ interface InitOptions {
2
+ writeAgent?: boolean;
3
+ force?: boolean;
4
+ }
5
+ export declare function init(opts?: InitOptions): Promise<void>;
6
+ export {};