@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 +17 -20
- package/dist/api.d.ts +19 -0
- package/dist/api.js +36 -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 +66 -0
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.js +57 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +199 -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 +125 -0
- package/dist/commands/scaffold.d.ts +6 -0
- package/dist/commands/scaffold.js +51 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +72 -0
- package/dist/discover.d.ts +5 -0
- package/dist/discover.js +19 -0
- package/dist/parse.d.ts +24 -0
- package/dist/parse.js +42 -0
- package/dist/serialize.d.ts +4 -0
- package/dist/serialize.js +43 -0
- package/package.json +18 -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 -226
- 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:
|
|
@@ -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`, `
|
|
64
|
+
**Developer-owned keys**: `component`, `tags`, `surfaces`. Edit these freely.
|
|
70
65
|
|
|
71
|
-
**CLI-managed keys**: `rules
|
|
66
|
+
**CLI-managed keys**: `rules` and workspace `tags`. Overwritten by `ditto-spec pull`. Do not edit by hand.
|
|
72
67
|
|
|
73
|
-
**Surface keys**
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
-
|
|
176
|
-
- **Never write `rules`
|
|
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
package/dist/bin.js
ADDED
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,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
|
+
}
|