@dittowords/spec-cli 0.0.1-alpha.2 → 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
@@ -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:
@@ -46,31 +43,29 @@ rules:
46
43
  examples:
47
44
  - from: "Your settings"
48
45
  to: "Open settings"
49
- examples: []
50
46
  ---
51
47
  ```
52
48
 
53
49
  ### Workspace spec (`workspace.ditto.md`)
54
50
 
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.
51
+ 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
52
 
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
57
+ tags: [body, button, call-to-action, dialog-title, heading, nav]
63
58
  rules: []
64
59
  ---
65
60
  ```
66
61
 
67
62
  ### Key concepts
68
63
 
69
- **Developer-owned keys**: `component`, `description`, `tags`, `surfaces`. Edit these freely.
64
+ **Developer-owned keys**: `component`, `tags`, `surfaces`. Edit these freely.
70
65
 
71
- **CLI-managed keys**: `rules`, `examples`. 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.
72
67
 
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.
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`).
74
69
 
75
70
  **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
71
 
@@ -100,7 +95,7 @@ Creates a new `index.ditto.md` for a component with the correct YAML structure a
100
95
  ditto-spec scaffold DialogueModal --path src/components/DialogueModal
101
96
  ```
102
97
 
103
- Use `--path <dir>` to specify where the file is created (defaults to the current directory). After scaffolding, add surfaces for the component's text-bearing props and run `ditto-spec pull` to populate rules.
98
+ 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
99
 
105
100
  ### `ditto-spec pull`
106
101
 
@@ -143,23 +138,23 @@ Set `DITTO_API_KEY` in your environment or in a `.env` file at the repo root.
143
138
 
144
139
  ## Agent contract
145
140
 
146
- When writing or editing text props for a component:
141
+ When writing or editing user-facing text for a component:
147
142
 
148
143
  1. Read `workspace.ditto.md` (if it exists) for universal rules.
149
- 2. Read the component's `index.ditto.md`. Match each prop you're filling to a surface key.
144
+ 2. Read the component's `index.ditto.md`. Match each piece of text you're writing to a surface key.
150
145
  3. Respect `maxLength` — it's a layout invariant, not a suggestion.
151
146
  4. Follow all rules in `rules[]`. Entries without `surface` apply to every surface; entries with `surface` apply only to that surface.
152
- 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.
153
148
 
154
149
  ### Creating specs
155
150
 
156
- When creating a new component with text-bearing props, scaffold a spec file:
151
+ When creating a new component that renders any user-facing text, scaffold a spec file:
157
152
 
158
153
  ```
159
154
  npx ditto-spec scaffold <ComponentName> --path <dir>
160
155
  ```
161
156
 
162
- Then edit the generated `index.ditto.md` to add surfaces — one entry per text-bearing prop:
157
+ Then edit the generated `index.ditto.md` to add surfaces — one entry per piece of user-facing text the component renders:
163
158
 
164
159
  ```yaml
165
160
  surfaces:
@@ -171,8 +166,10 @@ surfaces:
171
166
  maxLength: 30
172
167
  ```
173
168
 
174
- - Use `$children` for text via children. Use dot notation for nested props (`primaryAction.label`).
175
- - Choose `tags` from content categories like `heading`, `body`, `button`, `cta`, `dialog-title`, `call-to-action`.
176
- - **Never write `rules` or `examples` by hand.** Run `ditto-spec pull` after adding surfaces to populate rules from the platform.
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`).
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).
171
+ - **Never write `rules` by hand.** Run `ditto-spec pull` after adding surfaces to populate rules from the platform.
172
+
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.
177
174
 
178
175
  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,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 {};