@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 +4 -10
- 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 -231
- package/src/commands/list.ts +0 -59
- package/src/commands/pull.ts +0 -152
- 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 -45
|
@@ -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.scaffold = scaffold;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const SPEC_FILENAME = "index.ditto.md";
|
|
12
|
+
async function scaffold(opts) {
|
|
13
|
+
const dest = path_1.default.join(opts.targetDir, SPEC_FILENAME);
|
|
14
|
+
if (fs_1.default.existsSync(dest)) {
|
|
15
|
+
console.log(`${SPEC_FILENAME} already exists at ${opts.targetDir}. Nothing to do.`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const nameYaml = js_yaml_1.default.dump({ component: opts.componentName }, { lineWidth: -1 }).trimEnd();
|
|
19
|
+
const content = `---
|
|
20
|
+
${nameYaml}
|
|
21
|
+
tags: []
|
|
22
|
+
surfaces: {}
|
|
23
|
+
# Managed by Ditto — do not edit below
|
|
24
|
+
rules: []
|
|
25
|
+
---
|
|
26
|
+
`;
|
|
27
|
+
fs_1.default.mkdirSync(opts.targetDir, { recursive: true });
|
|
28
|
+
fs_1.default.writeFileSync(dest, content);
|
|
29
|
+
console.log(`✓ Created ${path_1.default.relative(process.cwd(), dest)}`);
|
|
30
|
+
warnIfOutsideRoots(opts.targetDir);
|
|
31
|
+
console.log(`
|
|
32
|
+
Next steps:
|
|
33
|
+
1. Add a surface key for each piece of user-facing text the component renders
|
|
34
|
+
2. Tag each surface with content categories (heading, body, button, cta, etc.)
|
|
35
|
+
3. Run \`ditto-spec pull\` to populate rules from the platform`);
|
|
36
|
+
}
|
|
37
|
+
function warnIfOutsideRoots(targetDir) {
|
|
38
|
+
try {
|
|
39
|
+
const { config, root } = (0, config_1.loadConfig)();
|
|
40
|
+
const roots = config.roots ?? ["."];
|
|
41
|
+
const absTarget = path_1.default.resolve(targetDir);
|
|
42
|
+
const inside = roots.some((r) => absTarget.startsWith(path_1.default.resolve(root, r)));
|
|
43
|
+
if (!inside) {
|
|
44
|
+
console.log(`\n⚠ Target directory is outside configured roots (${roots.join(", ")}). This file won't be found by pull, check, or list.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (!(err instanceof config_1.ConfigNotFoundError))
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
apiBase: string;
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
roots?: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare class ConfigNotFoundError extends Error {
|
|
7
|
+
constructor(cwd: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function loadConfig(cwd?: string): {
|
|
10
|
+
config: Config;
|
|
11
|
+
root: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function getApiKey(): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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.ConfigNotFoundError = void 0;
|
|
7
|
+
exports.loadConfig = loadConfig;
|
|
8
|
+
exports.getApiKey = getApiKey;
|
|
9
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const DEFAULT_CONFIG_NAME = "dittospec.config.json";
|
|
13
|
+
class ConfigNotFoundError extends Error {
|
|
14
|
+
constructor(cwd) {
|
|
15
|
+
super(`No ${DEFAULT_CONFIG_NAME} found from ${cwd} up to filesystem root.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ConfigNotFoundError = ConfigNotFoundError;
|
|
19
|
+
function loadConfig(cwd = process.cwd()) {
|
|
20
|
+
let dir = path_1.default.resolve(cwd);
|
|
21
|
+
while (true) {
|
|
22
|
+
const candidate = path_1.default.join(dir, DEFAULT_CONFIG_NAME);
|
|
23
|
+
if (fs_1.default.existsSync(candidate)) {
|
|
24
|
+
const raw = fs_1.default.readFileSync(candidate, "utf8");
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error(`${candidate}: invalid JSON. Check for syntax errors.`);
|
|
31
|
+
}
|
|
32
|
+
validate(parsed, candidate);
|
|
33
|
+
return { config: parsed, root: dir };
|
|
34
|
+
}
|
|
35
|
+
const parent = path_1.default.dirname(dir);
|
|
36
|
+
if (parent === dir)
|
|
37
|
+
break;
|
|
38
|
+
dir = parent;
|
|
39
|
+
}
|
|
40
|
+
throw new ConfigNotFoundError(cwd);
|
|
41
|
+
}
|
|
42
|
+
function validate(c, source) {
|
|
43
|
+
if (!c || typeof c !== "object")
|
|
44
|
+
throw new Error(`${source}: must be a JSON object.`);
|
|
45
|
+
const obj = c;
|
|
46
|
+
if (typeof obj.apiBase !== "string")
|
|
47
|
+
throw new Error(`${source}: missing string "apiBase".`);
|
|
48
|
+
if (typeof obj.workspaceId !== "string")
|
|
49
|
+
throw new Error(`${source}: missing string "workspaceId".`);
|
|
50
|
+
if (obj.roots !== undefined) {
|
|
51
|
+
if (!Array.isArray(obj.roots))
|
|
52
|
+
throw new Error(`${source}: "roots" must be an array of strings if present.`);
|
|
53
|
+
if (obj.roots.some((r) => typeof r !== "string")) {
|
|
54
|
+
throw new Error(`${source}: every entry in "roots" must be a string.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getApiKey() {
|
|
59
|
+
try {
|
|
60
|
+
const { root } = loadConfig();
|
|
61
|
+
dotenv_1.default.config({ path: path_1.default.join(root, ".env") });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (!(err instanceof ConfigNotFoundError))
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
const key = process.env.DITTO_API_KEY;
|
|
68
|
+
if (!key) {
|
|
69
|
+
throw new Error("DITTO_API_KEY is not set. Add it to .env at the repo root, or `export DITTO_API_KEY=...` in your shell.");
|
|
70
|
+
}
|
|
71
|
+
return key;
|
|
72
|
+
}
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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.discover = discover;
|
|
7
|
+
const glob_1 = require("glob");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const SPEC_GLOB = "**/*.ditto.md";
|
|
10
|
+
const IGNORE = ["**/node_modules/**", "**/dist*/**", "**/.git/**", "**/.next/**", "**/coverage/**"];
|
|
11
|
+
function discover(repoRoot, roots = ["."]) {
|
|
12
|
+
const patterns = roots.map((r) => path_1.default.posix.join(r, SPEC_GLOB));
|
|
13
|
+
const results = (0, glob_1.globSync)(patterns, {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
ignore: IGNORE,
|
|
16
|
+
absolute: true,
|
|
17
|
+
});
|
|
18
|
+
return results.map((abs) => ({ abs, rel: path_1.default.relative(repoRoot, abs) })).sort((a, b) => a.rel.localeCompare(b.rel));
|
|
19
|
+
}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SurfaceLike {
|
|
2
|
+
tags?: string[];
|
|
3
|
+
maxLength?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface SpecLike {
|
|
6
|
+
tags?: string[];
|
|
7
|
+
surfaces?: Record<string, SurfaceLike>;
|
|
8
|
+
}
|
|
9
|
+
export interface ParsedSpec {
|
|
10
|
+
kind: "component" | "workspace";
|
|
11
|
+
name?: string;
|
|
12
|
+
spec: Record<string, unknown>;
|
|
13
|
+
filePath: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class ParseError extends Error {
|
|
16
|
+
filePath: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
constructor(filePath: string, reason: string);
|
|
19
|
+
}
|
|
20
|
+
export declare function parseSpecFile(filePath: string): ParsedSpec;
|
|
21
|
+
export declare function extractFrontmatter(source: string, filePath: string): {
|
|
22
|
+
yaml: string;
|
|
23
|
+
afterFrontmatter: string;
|
|
24
|
+
};
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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.ParseError = void 0;
|
|
7
|
+
exports.parseSpecFile = parseSpecFile;
|
|
8
|
+
exports.extractFrontmatter = extractFrontmatter;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
class ParseError extends Error {
|
|
12
|
+
constructor(filePath, reason) {
|
|
13
|
+
super(`${filePath}: ${reason}`);
|
|
14
|
+
this.filePath = filePath;
|
|
15
|
+
this.reason = reason;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ParseError = ParseError;
|
|
19
|
+
function parseSpecFile(filePath) {
|
|
20
|
+
const source = fs_1.default.readFileSync(filePath, "utf8");
|
|
21
|
+
const { yaml: frontmatter } = extractFrontmatter(source, filePath);
|
|
22
|
+
const raw = js_yaml_1.default.load(frontmatter);
|
|
23
|
+
if (!raw || typeof raw !== "object") {
|
|
24
|
+
throw new ParseError(filePath, "frontmatter must be a YAML object");
|
|
25
|
+
}
|
|
26
|
+
const spec = raw;
|
|
27
|
+
if (spec.workspace === true) {
|
|
28
|
+
return { kind: "workspace", spec, filePath };
|
|
29
|
+
}
|
|
30
|
+
if (typeof spec.component !== "string" || !spec.component) {
|
|
31
|
+
throw new ParseError(filePath, "missing required 'component' key (string)");
|
|
32
|
+
}
|
|
33
|
+
return { kind: "component", name: spec.component, spec, filePath };
|
|
34
|
+
}
|
|
35
|
+
function extractFrontmatter(source, filePath) {
|
|
36
|
+
const cleaned = source.replace(/^\uFEFF/, "");
|
|
37
|
+
const match = cleaned.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
38
|
+
if (!match) {
|
|
39
|
+
throw new ParseError(filePath, "no YAML frontmatter found (expected --- delimiters)");
|
|
40
|
+
}
|
|
41
|
+
return { yaml: match[1], afterFrontmatter: cleaned.slice(match[0].length) };
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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.rewriteManagedKeys = rewriteManagedKeys;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
9
|
+
const parse_1 = require("./parse");
|
|
10
|
+
const MANAGED_COMMENT = "# Managed by Ditto — do not edit below";
|
|
11
|
+
function rewriteManagedKeys(filePath, updates) {
|
|
12
|
+
const source = fs_1.default.readFileSync(filePath, "utf8");
|
|
13
|
+
const { yaml: frontmatterYaml, afterFrontmatter } = (0, parse_1.extractFrontmatter)(source, filePath);
|
|
14
|
+
const spec = js_yaml_1.default.load(frontmatterYaml);
|
|
15
|
+
if (updates.tags !== undefined) {
|
|
16
|
+
const savedRules = spec.rules;
|
|
17
|
+
delete spec.rules;
|
|
18
|
+
spec.tags = updates.tags;
|
|
19
|
+
spec.rules = savedRules;
|
|
20
|
+
}
|
|
21
|
+
if (updates.rules !== undefined)
|
|
22
|
+
spec.rules = updates.rules;
|
|
23
|
+
let dumped = js_yaml_1.default
|
|
24
|
+
.dump(spec, {
|
|
25
|
+
lineWidth: -1,
|
|
26
|
+
noRefs: true,
|
|
27
|
+
sortKeys: false,
|
|
28
|
+
quotingType: '"',
|
|
29
|
+
})
|
|
30
|
+
.trimEnd();
|
|
31
|
+
dumped = insertManagedComment(dumped, updates.tags !== undefined);
|
|
32
|
+
fs_1.default.writeFileSync(filePath, `---\n${dumped}\n---${afterFrontmatter}`);
|
|
33
|
+
}
|
|
34
|
+
function insertManagedComment(dumped, managedTagsPresent) {
|
|
35
|
+
const targetKey = managedTagsPresent ? "tags" : "rules";
|
|
36
|
+
const idx = dumped.search(new RegExp(`^${targetKey}:`, "m"));
|
|
37
|
+
if (idx === -1)
|
|
38
|
+
return dumped;
|
|
39
|
+
const before = dumped.slice(0, idx);
|
|
40
|
+
if (before.includes(MANAGED_COMMENT))
|
|
41
|
+
return dumped;
|
|
42
|
+
return before + MANAGED_COMMENT + "\n" + dumped.slice(idx);
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dittowords/spec-cli",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.4",
|
|
4
4
|
"description": "CLI for syncing .ditto.md content specs with the Ditto platform.",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ditto-spec": "
|
|
7
|
+
"ditto-spec": "dist/bin.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
9
12
|
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
10
15
|
"ditto-spec": "tsx src/cli.ts"
|
|
11
16
|
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
12
20
|
"dependencies": {
|
|
13
21
|
"dotenv": "^16.0.0",
|
|
14
22
|
"glob": "^11.0.1",
|
|
15
|
-
"js-yaml": "^4.1.0"
|
|
16
|
-
|
|
23
|
+
"js-yaml": "^4.1.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsx": "^4.0.0",
|
|
27
|
+
"@types/js-yaml": "^4.0.9",
|
|
28
|
+
"@types/node": "^20.0.0",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
17
30
|
},
|
|
18
31
|
"license": "Proprietary"
|
|
19
32
|
}
|
package/src/api.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { Config } from "./config";
|
|
2
|
-
|
|
3
|
-
export interface RuleResponse {
|
|
4
|
-
name: string;
|
|
5
|
-
type: "style" | "wordlist";
|
|
6
|
-
description: string;
|
|
7
|
-
examples: { from: string; to: string }[];
|
|
8
|
-
tags: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface GetRulesMCPResponse {
|
|
12
|
-
workspaceRules: RuleResponse[];
|
|
13
|
-
projectRules: Record<string, RuleResponse[]>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class DittoApi {
|
|
17
|
-
constructor(private readonly config: Config, private readonly apiKey: string) {}
|
|
18
|
-
|
|
19
|
-
async getRules(): Promise<RuleResponse[]> {
|
|
20
|
-
const url = new URL("/v2/rules/mcp", this.config.apiBase);
|
|
21
|
-
const res = await this.fetch(url, { method: "GET" });
|
|
22
|
-
const data = (await res.json()) as GetRulesMCPResponse;
|
|
23
|
-
return data.workspaceRules;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private async fetch(url: URL, init: RequestInit): Promise<Response> {
|
|
27
|
-
const res = await fetch(url.toString(), {
|
|
28
|
-
...init,
|
|
29
|
-
headers: { ...this.headers(), ...(init.headers ?? {}) },
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
const body = await res.text();
|
|
33
|
-
throw new Error(`${init.method} ${url} → ${res.status}: ${body}`);
|
|
34
|
-
}
|
|
35
|
-
return res;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private headers(): Record<string, string> {
|
|
39
|
-
return {
|
|
40
|
-
authorization: this.apiKey,
|
|
41
|
-
workspace_id: this.config.workspaceId,
|
|
42
|
-
"content-type": "application/json",
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
package/src/bin.js
DELETED
package/src/cli.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { check } from "./commands/check";
|
|
2
|
-
import { init } from "./commands/init";
|
|
3
|
-
import { list } from "./commands/list";
|
|
4
|
-
import { pull } from "./commands/pull";
|
|
5
|
-
import { scaffold } from "./commands/scaffold";
|
|
6
|
-
|
|
7
|
-
const COMMANDS: Record<string, (args: string[]) => Promise<void>> = {
|
|
8
|
-
init: async (args) => init({ writeAgent: args.includes("--agent") }),
|
|
9
|
-
pull: async (args) => pull({ dryRun: args.includes("--dry-run") }),
|
|
10
|
-
check: async () => check(),
|
|
11
|
-
list: async () => list(),
|
|
12
|
-
scaffold: async (args) => {
|
|
13
|
-
const name = args.find((a) => !a.startsWith("--"));
|
|
14
|
-
if (!name) {
|
|
15
|
-
process.stderr.write("Usage: ditto-spec scaffold <ComponentName> [--path <dir>]\n");
|
|
16
|
-
process.exit(2);
|
|
17
|
-
}
|
|
18
|
-
const pathIdx = args.indexOf("--path");
|
|
19
|
-
const targetDir = pathIdx !== -1 && args[pathIdx + 1] ? args[pathIdx + 1] : process.cwd();
|
|
20
|
-
return scaffold({ componentName: name, targetDir });
|
|
21
|
-
},
|
|
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
|
-
|
|
47
|
-
async function main() {
|
|
48
|
-
const args = process.argv.slice(2);
|
|
49
|
-
const cmd = args[0];
|
|
50
|
-
|
|
51
|
-
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
52
|
-
process.stdout.write(HELP);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const handler = COMMANDS[cmd];
|
|
57
|
-
if (!handler) {
|
|
58
|
-
process.stderr.write(`Unknown command: ${cmd}\n\n${HELP}`);
|
|
59
|
-
process.exit(2);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
await handler(args.slice(1));
|
|
64
|
-
} catch (err) {
|
|
65
|
-
console.error(err instanceof Error ? err.message : err);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
main();
|
package/src/commands/check.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { loadConfig } from "../config";
|
|
3
|
-
import { discover } from "../discover";
|
|
4
|
-
import { parseSpecFile } from "../parse";
|
|
5
|
-
|
|
6
|
-
export async function check(): Promise<void> {
|
|
7
|
-
const { config, root } = loadConfig();
|
|
8
|
-
const files = discover(root, config.roots);
|
|
9
|
-
|
|
10
|
-
if (files.length === 0) {
|
|
11
|
-
console.log("No .ditto.md files found.");
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let failed = 0;
|
|
16
|
-
for (const f of files) {
|
|
17
|
-
try {
|
|
18
|
-
const parsed = parseSpecFile(f.abs);
|
|
19
|
-
const specLike = parsed.spec as Record<string, unknown>;
|
|
20
|
-
|
|
21
|
-
if (parsed.kind === "component") {
|
|
22
|
-
const surfaces = specLike.surfaces;
|
|
23
|
-
if (!surfaces || typeof surfaces !== "object") {
|
|
24
|
-
throw new Error("component spec missing 'surfaces' object");
|
|
25
|
-
}
|
|
26
|
-
for (const [key, val] of Object.entries(surfaces as Record<string, unknown>)) {
|
|
27
|
-
if (!val || typeof val !== "object") {
|
|
28
|
-
throw new Error(`surface '${key}' must be an object`);
|
|
29
|
-
}
|
|
30
|
-
const surface = val as Record<string, unknown>;
|
|
31
|
-
if (!Array.isArray(surface.tags)) {
|
|
32
|
-
throw new Error(`surface '${key}' missing 'tags' array`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
console.log(`✓ ${path.relative(root, f.abs)}`);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
failed++;
|
|
40
|
-
console.error(`× ${path.relative(root, f.abs)}: ${err instanceof Error ? err.message : err}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (failed > 0) {
|
|
45
|
-
console.error(`\n${failed} file(s) failed validation.`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
}
|