@dittowords/spec-cli 0.0.1-alpha.3 → 0.0.1-alpha.4

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
@@ -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:
@@ -46,7 +43,6 @@ rules:
46
43
  examples:
47
44
  - from: "Your settings"
48
45
  to: "Open settings"
49
- examples: []
50
46
  ---
51
47
  ```
52
48
 
@@ -57,8 +53,6 @@ A repo may have a single `workspace.ditto.md` somewhere under the CLI's configur
57
53
  ```yaml
58
54
  ---
59
55
  workspace: true
60
- description: >
61
- Workspace-wide content rules. Read alongside any component's index.ditto.md.
62
56
  # Managed by Ditto — do not edit below
63
57
  tags: [body, button, call-to-action, dialog-title, heading, nav]
64
58
  rules: []
@@ -67,9 +61,9 @@ rules: []
67
61
 
68
62
  ### Key concepts
69
63
 
70
- **Developer-owned keys**: `component`, `description`, `tags`, `surfaces`. Edit these freely.
64
+ **Developer-owned keys**: `component`, `tags`, `surfaces`. Edit these freely.
71
65
 
72
- **CLI-managed keys**: `rules`, `examples`, and workspace `tags`. Overwritten by `ditto-spec pull`. Do not edit by hand.
66
+ **CLI-managed keys**: `rules` and workspace `tags`. Overwritten by `ditto-spec pull`. Do not edit by hand.
73
67
 
74
68
  **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`).
75
69
 
@@ -150,7 +144,7 @@ When writing or editing user-facing text for a component:
150
144
  2. Read the component's `index.ditto.md`. Match each piece of text you're writing to a surface key.
151
145
  3. Respect `maxLength` — it's a layout invariant, not a suggestion.
152
146
  4. Follow all rules in `rules[]`. Entries without `surface` apply to every surface; entries with `surface` apply only to that surface.
153
- 5. Reference `examples[]` entries with `status: "approved"` as concrete tone/shape guidance.
147
+ 5. When a rule includes `examples`, reference them as concrete tone/shape guidance.
154
148
 
155
149
  ### Creating specs
156
150
 
@@ -174,7 +168,7 @@ surfaces:
174
168
 
175
169
  - 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`).
176
170
  - 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).
177
- - **Never write `rules` or `examples` by hand.** Run `ditto-spec pull` after adding surfaces to populate rules from the platform.
171
+ - **Never write `rules` by hand.** Run `ditto-spec pull` after adding surfaces to populate rules from the platform.
178
172
 
179
173
  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.
180
174
 
package/dist/api.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Config } from "./config";
2
+ export interface RuleResponse {
3
+ name: string;
4
+ type: "style" | "wordlist";
5
+ description: string;
6
+ examples: {
7
+ from: string;
8
+ to: string;
9
+ }[];
10
+ tags: string[];
11
+ }
12
+ export declare class DittoApi {
13
+ private readonly config;
14
+ private readonly apiKey;
15
+ constructor(config: Config, apiKey: string);
16
+ getRules(): Promise<RuleResponse[]>;
17
+ private fetch;
18
+ private headers;
19
+ }
package/dist/api.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DittoApi = void 0;
4
+ class DittoApi {
5
+ constructor(config, apiKey) {
6
+ this.config = config;
7
+ this.apiKey = apiKey;
8
+ }
9
+ async getRules() {
10
+ const url = new URL("/v2/rules/mcp", this.config.apiBase);
11
+ const res = await this.fetch(url, { method: "GET" });
12
+ const data = (await res.json());
13
+ return data.workspaceRules;
14
+ }
15
+ async fetch(url, init) {
16
+ const res = await fetch(url.toString(), {
17
+ ...init,
18
+ headers: { ...this.headers(init.method ?? "GET"), ...(init.headers ?? {}) },
19
+ });
20
+ if (!res.ok) {
21
+ const body = await res.text();
22
+ throw new Error(`${init.method} ${url} → ${res.status}: ${body}`);
23
+ }
24
+ return res;
25
+ }
26
+ headers(method) {
27
+ const h = {
28
+ authorization: this.apiKey, // Ditto API expects bare key, no Bearer prefix
29
+ workspace_id: this.config.workspaceId,
30
+ };
31
+ if (method !== "GET")
32
+ h["content-type"] = "application/json";
33
+ return h;
34
+ }
35
+ }
36
+ 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,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const check_1 = require("./commands/check");
4
+ const init_1 = require("./commands/init");
5
+ const list_1 = require("./commands/list");
6
+ const pull_1 = require("./commands/pull");
7
+ const scaffold_1 = require("./commands/scaffold");
8
+ const COMMANDS = {
9
+ init: async (args) => (0, init_1.init)({ writeAgent: args.includes("--agent") }),
10
+ pull: async (args) => (0, pull_1.pull)({ dryRun: args.includes("--dry-run") }),
11
+ check: async () => (0, check_1.check)(),
12
+ list: async () => (0, list_1.list)(),
13
+ scaffold: async (args) => {
14
+ const name = args.find((a) => !a.startsWith("--"));
15
+ if (!name) {
16
+ process.stderr.write("Usage: ditto-spec scaffold <ComponentName> [--path <dir>]\n");
17
+ process.exit(2);
18
+ }
19
+ const pathIdx = args.indexOf("--path");
20
+ const targetDir = pathIdx !== -1 && args[pathIdx + 1] ? args[pathIdx + 1] : process.cwd();
21
+ return (0, scaffold_1.scaffold)({ componentName: name, targetDir });
22
+ },
23
+ };
24
+ const HELP = `ditto-spec — sync .ditto.md content specs with the Ditto platform.
25
+
26
+ Usage:
27
+ ditto-spec <command> [options]
28
+
29
+ Commands:
30
+ init Set up ditto specs: creates config, workspace spec, and prints agent setup.
31
+ init --agent Also writes agent configuration (CLAUDE.md, .cursorrules, etc.).
32
+ scaffold <Name> Create a new index.ditto.md for a component.
33
+ scaffold <Name> --path <dir> Create the spec in a specific directory.
34
+ pull Pull rules from the platform into the managed keys of each spec.
35
+ pull --dry-run Show which files would change without writing.
36
+ check Parse every spec file; exit non-zero on any malformed file.
37
+ list Print every component spec with its surfaces and tags.
38
+
39
+ Environment:
40
+ DITTO_API_KEY Workspace API key (required for pull).
41
+
42
+ Config:
43
+ Reads dittospec.config.json from the nearest ancestor directory:
44
+ { "apiBase": "https://...", "workspaceId": "...", "roots": ["design-system"] }
45
+ `;
46
+ async function main() {
47
+ const args = process.argv.slice(2);
48
+ const cmd = args[0];
49
+ if (!cmd || cmd === "--help" || cmd === "-h") {
50
+ process.stdout.write(HELP);
51
+ return;
52
+ }
53
+ const handler = COMMANDS[cmd];
54
+ if (!handler) {
55
+ process.stderr.write(`Unknown command: ${cmd}\n\n${HELP}`);
56
+ process.exit(2);
57
+ }
58
+ try {
59
+ await handler(args.slice(1));
60
+ }
61
+ catch (err) {
62
+ console.error(err instanceof Error ? err.message : err);
63
+ process.exit(1);
64
+ }
65
+ }
66
+ 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,5 @@
1
+ interface InitOptions {
2
+ writeAgent?: boolean;
3
+ }
4
+ export declare function init(opts?: InitOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,199 @@
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.init = init;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const config_1 = require("../config");
10
+ const CONFIG_NAME = "dittospec.config.json";
11
+ const WORKSPACE_SPEC_NAME = "workspace.ditto.md";
12
+ const DEFAULT_CONFIG = JSON.stringify({
13
+ apiBase: "https://api.dittowords.com",
14
+ workspaceId: "TODO: your Ditto workspace ID",
15
+ roots: ["."],
16
+ }, null, 2);
17
+ const WORKSPACE_SPEC = `---
18
+ workspace: true
19
+ # Managed by Ditto — do not edit below
20
+ tags: []
21
+ rules: []
22
+ ---
23
+ `;
24
+ function agentDocs(format) {
25
+ const md = format === "claude";
26
+ const h2 = md ? "##" : "#";
27
+ const h3 = md ? "###" : "##";
28
+ const c = (s) => (md ? `\`${s}\`` : s);
29
+ const neverRules = md
30
+ ? `**Never write \`rules\` by hand.**`
31
+ : "NEVER write rules by hand.";
32
+ return `
33
+ ${h2} Ditto Content Specs
34
+
35
+ This project uses ${c(".ditto.md")} files to declare text surfaces on components. Rules are synced from the Ditto platform — never written by hand.
36
+
37
+ ${h3} Reading specs
38
+
39
+ When writing or editing text props for a component:
40
+
41
+ 1. Check for ${c("workspace.ditto.md")} at the project root for universal content rules.
42
+ 2. Check for ${c("index.ditto.md")} next to the component for surface-specific rules and constraints.
43
+ 3. Respect ${c("maxLength")} — it's a layout constraint, not a suggestion.
44
+ 4. Follow all rules in the ${c("rules")} key. Rules without a ${c("surface")} field apply to every surface; rules with ${c("surface")} apply only to that surface.
45
+
46
+ ${h3} Creating specs
47
+
48
+ When creating a new component that renders any user-facing text, scaffold a spec file:
49
+
50
+ npx ditto-spec scaffold <ComponentName> --path <dir>
51
+
52
+ Then edit the generated ${c("index.ditto.md")} to add surfaces — one entry per piece of user-facing text the component renders:
53
+
54
+ surfaces:
55
+ title:
56
+ tags: [heading]
57
+ maxLength: 60
58
+ $children:
59
+ tags: [button, cta]
60
+ maxLength: 30
61
+
62
+ - Use ${c("$children")} for text via children. Use dot notation for nested props (${c("primaryAction.label")}). For hardcoded or internal strings, use a descriptive role name (${c("headline")}, ${c("bodyText")}, ${c("submitLabel")}).
63
+ - Check the ${c("tags")} key in ${c("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).
64
+ - ${neverRules} Run ${c("npx ditto-spec pull")} after adding surfaces to populate rules from the platform.
65
+
66
+ 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.
67
+
68
+ Run ${c("npx ditto-spec list")} to see all existing specs.
69
+ `;
70
+ }
71
+ const AGENTS_MD_ROW = "| Ditto component specs | `.ditto.md` content specs — read `workspace.ditto.md` and component `index.ditto.md` before writing copy |";
72
+ async function init(opts = {}) {
73
+ const cwd = process.cwd();
74
+ const configExistsHere = fs_1.default.existsSync(path_1.default.join(cwd, CONFIG_NAME));
75
+ if (configExistsHere && !opts.writeAgent) {
76
+ console.log(`${CONFIG_NAME} already exists. Nothing to do.`);
77
+ console.log("Run with --agent to add agent configuration (CLAUDE.md, .cursorrules, etc.).");
78
+ return;
79
+ }
80
+ if (!configExistsHere) {
81
+ const ancestorConfig = findAncestorConfig(cwd);
82
+ if (ancestorConfig) {
83
+ console.log(`⚠ Found existing ${CONFIG_NAME} at ${ancestorConfig}`);
84
+ console.log(" Creating a new config here will shadow it for commands run in this directory.");
85
+ }
86
+ fs_1.default.writeFileSync(path_1.default.join(cwd, CONFIG_NAME), DEFAULT_CONFIG + "\n");
87
+ console.log(`✓ Created ${CONFIG_NAME}`);
88
+ fs_1.default.writeFileSync(path_1.default.join(cwd, WORKSPACE_SPEC_NAME), WORKSPACE_SPEC);
89
+ console.log(`✓ Created ${WORKSPACE_SPEC_NAME}`);
90
+ }
91
+ const env = detectAgentEnv(cwd);
92
+ if (opts.writeAgent) {
93
+ writeAgentConfig(cwd, env);
94
+ }
95
+ else {
96
+ printAgentSuggestions(env);
97
+ }
98
+ if (!configExistsHere) {
99
+ printNextSteps();
100
+ }
101
+ }
102
+ function findAncestorConfig(cwd) {
103
+ try {
104
+ const { root } = (0, config_1.loadConfig)(cwd);
105
+ if (path_1.default.resolve(root) !== path_1.default.resolve(cwd)) {
106
+ return path_1.default.join(root, CONFIG_NAME);
107
+ }
108
+ }
109
+ catch (err) {
110
+ if (!(err instanceof config_1.ConfigNotFoundError))
111
+ throw err;
112
+ }
113
+ return null;
114
+ }
115
+ function detectAgentEnv(cwd) {
116
+ if (fs_1.default.existsSync(path_1.default.join(cwd, ".claude")) || fs_1.default.existsSync(path_1.default.join(cwd, "CLAUDE.md"))) {
117
+ return "claude";
118
+ }
119
+ if (fs_1.default.existsSync(path_1.default.join(cwd, ".cursor")) || fs_1.default.existsSync(path_1.default.join(cwd, ".cursorrules"))) {
120
+ return "cursor";
121
+ }
122
+ return "none";
123
+ }
124
+ function writeAgentConfig(cwd, env) {
125
+ if (env === "claude") {
126
+ const claudeMd = path_1.default.join(cwd, "CLAUDE.md");
127
+ const block = agentDocs("claude");
128
+ if (fs_1.default.existsSync(claudeMd)) {
129
+ const existing = fs_1.default.readFileSync(claudeMd, "utf8");
130
+ if (existing.includes("Ditto Content Specs")) {
131
+ console.log(" CLAUDE.md already has Ditto Content Specs section — skipped.");
132
+ }
133
+ else {
134
+ fs_1.default.appendFileSync(claudeMd, block);
135
+ console.log("✓ Appended Ditto Content Specs section to CLAUDE.md");
136
+ }
137
+ }
138
+ else {
139
+ fs_1.default.writeFileSync(claudeMd, `# CLAUDE.md${block}`);
140
+ console.log("✓ Created CLAUDE.md with Ditto Content Specs section");
141
+ }
142
+ const agentsMd = path_1.default.join(cwd, "AGENTS.md");
143
+ if (fs_1.default.existsSync(agentsMd)) {
144
+ const existing = fs_1.default.readFileSync(agentsMd, "utf8");
145
+ if (existing.includes("Ditto component specs")) {
146
+ console.log(" AGENTS.md already has Ditto row — skipped.");
147
+ }
148
+ else {
149
+ fs_1.default.appendFileSync(agentsMd, "\n" + AGENTS_MD_ROW + "\n");
150
+ console.log("✓ Appended Ditto row to AGENTS.md");
151
+ }
152
+ }
153
+ return;
154
+ }
155
+ if (env === "cursor") {
156
+ const cursorrules = path_1.default.join(cwd, ".cursorrules");
157
+ const block = agentDocs("cursor");
158
+ if (fs_1.default.existsSync(cursorrules)) {
159
+ const existing = fs_1.default.readFileSync(cursorrules, "utf8");
160
+ if (existing.includes("Ditto Content Specs")) {
161
+ console.log(" .cursorrules already has Ditto section — skipped.");
162
+ }
163
+ else {
164
+ fs_1.default.appendFileSync(cursorrules, block);
165
+ console.log("✓ Appended Ditto section to .cursorrules");
166
+ }
167
+ }
168
+ else {
169
+ fs_1.default.writeFileSync(cursorrules, block.trimStart());
170
+ console.log("✓ Created .cursorrules with Ditto section");
171
+ }
172
+ return;
173
+ }
174
+ console.log("\nNo agent environment detected. See next steps for manual setup.");
175
+ }
176
+ function printAgentSuggestions(env) {
177
+ if (env === "claude") {
178
+ console.log("\nDetected Claude Code environment.");
179
+ console.log("Add this to your CLAUDE.md (or run `ditto-spec init --agent` to do it automatically):\n");
180
+ console.log(agentDocs("claude").trim());
181
+ return;
182
+ }
183
+ if (env === "cursor") {
184
+ console.log("\nDetected Cursor environment.");
185
+ console.log("Add this to your .cursorrules (or run `ditto-spec init --agent` to do it automatically):\n");
186
+ console.log(agentDocs("cursor").trim());
187
+ return;
188
+ }
189
+ console.log("\nTo help your AI agent use ditto specs, add the agent contract from the README");
190
+ console.log("to your project's agent configuration file (CLAUDE.md, .cursorrules, etc.).");
191
+ }
192
+ function printNextSteps() {
193
+ console.log(`
194
+ Next steps:
195
+ 1. Fill in your workspaceId in ${CONFIG_NAME}
196
+ 2. Set DITTO_API_KEY in .env or your shell
197
+ 3. Create your first component spec (index.ditto.md next to a component)
198
+ 4. Run \`ditto-spec pull\` to sync rules from the platform`);
199
+ }
@@ -0,0 +1 @@
1
+ export declare function list(): Promise<void>;
@@ -0,0 +1,51 @@
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.list = list;
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 list() {
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
+ for (const f of files) {
19
+ let parsed;
20
+ try {
21
+ parsed = (0, parse_1.parseSpecFile)(f.abs);
22
+ }
23
+ catch (err) {
24
+ console.error(`× ${path_1.default.relative(root, f.abs)}: ${err instanceof Error ? err.message : err}`);
25
+ continue;
26
+ }
27
+ if (parsed.kind === "workspace") {
28
+ const rules = Array.isArray(parsed.spec.rules) ? parsed.spec.rules : [];
29
+ console.log(`\n[workspace] (${path_1.default.relative(root, f.abs)})`);
30
+ console.log(` ${rules.length} rule${rules.length === 1 ? "" : "s"}`);
31
+ continue;
32
+ }
33
+ console.log(`\n${parsed.name} (${path_1.default.relative(root, f.abs)})`);
34
+ const specLike = parsed.spec;
35
+ const componentTags = specLike.tags ?? [];
36
+ if (componentTags.length > 0) {
37
+ console.log(` component tags: [${componentTags.join(", ")}]`);
38
+ }
39
+ const surfaces = specLike.surfaces ?? {};
40
+ const entries = Object.entries(surfaces);
41
+ if (entries.length === 0) {
42
+ console.log(" (no surfaces)");
43
+ continue;
44
+ }
45
+ for (const [key, surface] of entries) {
46
+ const tags = (surface.tags ?? []).join(", ");
47
+ const maxLen = surface.maxLength != null ? ` max ${surface.maxLength}` : "";
48
+ console.log(` ${key.padEnd(28)} [${tags}]${maxLen}`);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,5 @@
1
+ interface PullOptions {
2
+ dryRun?: boolean;
3
+ }
4
+ export declare function pull(opts?: PullOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,125 @@
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.pull = pull;
7
+ const path_1 = __importDefault(require("path"));
8
+ const api_1 = require("../api");
9
+ const config_1 = require("../config");
10
+ const discover_1 = require("../discover");
11
+ const parse_1 = require("../parse");
12
+ const serialize_1 = require("../serialize");
13
+ async function pull(opts = {}) {
14
+ const { config, root } = (0, config_1.loadConfig)();
15
+ const apiKey = (0, config_1.getApiKey)();
16
+ const api = new api_1.DittoApi(config, apiKey);
17
+ const files = (0, discover_1.discover)(root, config.roots);
18
+ if (files.length === 0) {
19
+ console.log("No .ditto.md files found.");
20
+ return;
21
+ }
22
+ console.log(`Found ${files.length} spec file${files.length === 1 ? "" : "s"}.`);
23
+ const componentFiles = [];
24
+ const workspaceFiles = [];
25
+ let failed = 0;
26
+ for (const f of files) {
27
+ let parsed;
28
+ try {
29
+ parsed = (0, parse_1.parseSpecFile)(f.abs);
30
+ }
31
+ catch (err) {
32
+ failed++;
33
+ console.error(`× ${f.rel}: ${err instanceof Error ? err.message : err}`);
34
+ continue;
35
+ }
36
+ if (parsed.kind === "workspace") {
37
+ workspaceFiles.push({ file: f, parsed });
38
+ }
39
+ else {
40
+ componentFiles.push({ file: f, parsed });
41
+ }
42
+ }
43
+ if (workspaceFiles.length > 1) {
44
+ const paths = workspaceFiles.map((w) => path_1.default.relative(root, w.file.abs)).join(", ");
45
+ throw new Error(`Found multiple workspace specs (${paths}). Expected at most one.`);
46
+ }
47
+ const allRules = await api.getRules();
48
+ console.log(`Fetched ${allRules.length} rule${allRules.length === 1 ? "" : "s"} from workspace.`);
49
+ const platformTags = [...new Set(allRules.flatMap((r) => r.tags))].filter(Boolean).sort();
50
+ let written = 0;
51
+ let unchanged = 0;
52
+ if (workspaceFiles.length === 1) {
53
+ const { file, parsed } = workspaceFiles[0];
54
+ const universalRules = allRules.filter((r) => r.tags.length === 0).map(toRuleObj);
55
+ if (rulesMatch(parsed.spec.rules, universalRules) && tagsMatch(parsed.spec.tags, platformTags)) {
56
+ unchanged++;
57
+ }
58
+ else if (opts.dryRun) {
59
+ console.log(`~ ${path_1.default.relative(root, file.abs)} (would write ${universalRules.length} workspace rule(s), ${platformTags.length} tag(s))`);
60
+ }
61
+ else {
62
+ (0, serialize_1.rewriteManagedKeys)(file.abs, { tags: platformTags, rules: universalRules });
63
+ written++;
64
+ console.log(`✓ ${path_1.default.relative(root, file.abs)} — ${universalRules.length} workspace rule(s), ${platformTags.length} tag(s)`);
65
+ }
66
+ }
67
+ for (const { file, parsed } of componentFiles) {
68
+ const specLike = parsed.spec;
69
+ const componentTags = specLike.tags ?? [];
70
+ const surfaces = specLike.surfaces ?? {};
71
+ const componentMatches = rulesForTags(allRules, componentTags);
72
+ const consumed = new Set(componentMatches);
73
+ const matchedRules = [];
74
+ for (const rule of componentMatches) {
75
+ matchedRules.push(toRuleObj(rule));
76
+ }
77
+ for (const [surfaceKey, surface] of Object.entries(surfaces)) {
78
+ const matches = rulesForTags(allRules, surface.tags ?? []).filter((r) => !consumed.has(r));
79
+ for (const rule of matches) {
80
+ matchedRules.push(toSurfaceRuleObj(surfaceKey, rule));
81
+ }
82
+ }
83
+ if (rulesMatch(parsed.spec.rules, matchedRules)) {
84
+ unchanged++;
85
+ continue;
86
+ }
87
+ if (opts.dryRun) {
88
+ console.log(`~ ${path_1.default.relative(root, file.abs)} (would write ${matchedRules.length} rule(s))`);
89
+ continue;
90
+ }
91
+ (0, serialize_1.rewriteManagedKeys)(file.abs, { rules: matchedRules });
92
+ written++;
93
+ console.log(`✓ ${path_1.default.relative(root, file.abs)} — ${matchedRules.length} rule(s)`);
94
+ }
95
+ console.log(`\nDone. ${written} updated, ${unchanged} unchanged.`);
96
+ if (failed > 0) {
97
+ console.error(`${failed} file(s) failed to parse.`);
98
+ process.exit(1);
99
+ }
100
+ }
101
+ function rulesForTags(rules, tags) {
102
+ if (tags.length === 0)
103
+ return [];
104
+ const tagSet = new Set(tags);
105
+ return rules.filter((r) => r.tags.length > 0 && r.tags.some((t) => tagSet.has(t)));
106
+ }
107
+ function toRuleObj(rule) {
108
+ const out = {
109
+ name: rule.name,
110
+ description: rule.description,
111
+ };
112
+ if (rule.examples.length > 0) {
113
+ out.examples = rule.examples.map((ex) => ({ from: ex.from, to: ex.to }));
114
+ }
115
+ return out;
116
+ }
117
+ function toSurfaceRuleObj(surface, rule) {
118
+ return { surface, ...toRuleObj(rule) };
119
+ }
120
+ function rulesMatch(existing, incoming) {
121
+ return JSON.stringify(existing ?? []) === JSON.stringify(incoming);
122
+ }
123
+ function tagsMatch(existing, incoming) {
124
+ return JSON.stringify(existing ?? []) === JSON.stringify(incoming);
125
+ }
@@ -0,0 +1,6 @@
1
+ interface ScaffoldOptions {
2
+ componentName: string;
3
+ targetDir: string;
4
+ }
5
+ export declare function scaffold(opts: ScaffoldOptions): Promise<void>;
6
+ export {};