@nebutra/design-sync 0.1.0 → 0.1.2

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.
Files changed (61) hide show
  1. package/LICENSE +21 -676
  2. package/dist/cli/index.d.ts +3 -0
  3. package/dist/cli/index.d.ts.map +1 -0
  4. package/dist/cli/index.js +189 -0
  5. package/dist/detect.d.ts +21 -0
  6. package/dist/detect.d.ts.map +1 -0
  7. package/dist/detect.js +81 -0
  8. package/dist/factory.d.ts +42 -0
  9. package/dist/factory.d.ts.map +1 -0
  10. package/dist/factory.js +94 -0
  11. package/dist/figma-config/index.d.ts +37 -0
  12. package/dist/figma-config/index.d.ts.map +1 -0
  13. package/dist/figma-config/index.js +12 -0
  14. package/dist/figma-config/tokens-studio.config.json +34 -0
  15. package/dist/index.d.ts +16 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +38 -0
  18. package/dist/io.d.ts +26 -0
  19. package/dist/io.d.ts.map +1 -0
  20. package/dist/io.js +119 -0
  21. package/dist/providers/design-md.d.ts +49 -0
  22. package/dist/providers/design-md.d.ts.map +1 -0
  23. package/dist/providers/design-md.js +264 -0
  24. package/dist/providers/figma.d.ts +21 -0
  25. package/dist/providers/figma.d.ts.map +1 -0
  26. package/dist/providers/figma.js +179 -0
  27. package/dist/providers/git-only.d.ts +11 -0
  28. package/dist/providers/git-only.d.ts.map +1 -0
  29. package/dist/providers/git-only.js +104 -0
  30. package/dist/providers/memory.d.ts +17 -0
  31. package/dist/providers/memory.d.ts.map +1 -0
  32. package/dist/providers/memory.js +65 -0
  33. package/dist/providers/penpot.d.ts +21 -0
  34. package/dist/providers/penpot.d.ts.map +1 -0
  35. package/dist/providers/penpot.js +137 -0
  36. package/dist/serialize/from-design-md.d.ts +80 -0
  37. package/dist/serialize/from-design-md.d.ts.map +1 -0
  38. package/dist/serialize/from-design-md.js +1329 -0
  39. package/dist/serialize/to-brand-package.d.ts +37 -0
  40. package/dist/serialize/to-brand-package.d.ts.map +1 -0
  41. package/dist/serialize/to-brand-package.js +87 -0
  42. package/dist/serialize/to-design-md.d.ts +42 -0
  43. package/dist/serialize/to-design-md.d.ts.map +1 -0
  44. package/dist/serialize/to-design-md.js +114 -0
  45. package/dist/serialize/to-design-md.prose.d.ts +55 -0
  46. package/dist/serialize/to-design-md.prose.d.ts.map +1 -0
  47. package/dist/serialize/to-design-md.prose.js +127 -0
  48. package/dist/serialize/to-design-md.resolve.d.ts +36 -0
  49. package/dist/serialize/to-design-md.resolve.d.ts.map +1 -0
  50. package/dist/serialize/to-design-md.resolve.js +248 -0
  51. package/dist/serialize/to-preview-html.d.ts +42 -0
  52. package/dist/serialize/to-preview-html.d.ts.map +1 -0
  53. package/dist/serialize/to-preview-html.js +250 -0
  54. package/dist/serialize/to-preview-html.template.d.ts +75 -0
  55. package/dist/serialize/to-preview-html.template.d.ts.map +1 -0
  56. package/dist/serialize/to-preview-html.template.js +267 -0
  57. package/dist/types.d.ts +191 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +16 -0
  60. package/package.json +22 -7
  61. package/src/cli/index.ts +36 -3
@@ -0,0 +1,267 @@
1
+ /**
2
+ * to-preview-html.template.ts
3
+ *
4
+ * HTML / CSS template layer for the DTCG → preview.html serializer.
5
+ *
6
+ * Exports:
7
+ * - escapeHtml — generic HTML-attribute / content escaping
8
+ * - cssAttrValue — escapes a token value for use inside style="…"
9
+ * - cssVarsDecl — turns a CssVarBlock into CSS custom-property lines
10
+ * - buildDocument — assembles the full <!DOCTYPE html> string
11
+ *
12
+ * Pure: no filesystem I/O, no token resolution.
13
+ */
14
+ // ─── HTML / CSS escaping ───────────────────────────────────────────────────────
15
+ /**
16
+ * Escape a string for safe insertion into HTML content or attribute values.
17
+ * Converts the five characters that have special meaning in HTML.
18
+ */
19
+ export function escapeHtml(value) {
20
+ return value
21
+ .replace(/&/g, "&amp;")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;")
24
+ .replace(/"/g, "&quot;")
25
+ .replace(/'/g, "&#x27;");
26
+ }
27
+ /**
28
+ * Escape a token-derived value for safe use inside a `style="…"` attribute.
29
+ *
30
+ * Strategy:
31
+ * 1. Strip CSS structural characters that could inject extra declarations or
32
+ * break the style attribute — specifically `;`, `{`, `}`, `<`, `>`.
33
+ * NOTE: We deliberately keep `"` in the string before step 2 so that
34
+ * font-family values like `"Geist", sans-serif` are preserved; `escapeHtml`
35
+ * will turn those `"` into `&quot;` which the browser decodes correctly.
36
+ * 2. HTML-escape the result so embedded quotes survive as `&quot;` and any
37
+ * remaining angle brackets become `&lt;`/`&gt;`.
38
+ *
39
+ * Example:
40
+ * cssAttrValue('"Geist", "Noto Sans SC", sans-serif')
41
+ * → '&quot;Geist&quot;, &quot;Noto Sans SC&quot;, sans-serif'
42
+ *
43
+ * cssAttrValue('red;display:none')
44
+ * → 'reddisplay:none' (semicolon stripped before HTML-escape)
45
+ */
46
+ export function cssAttrValue(raw) {
47
+ return escapeHtml(raw.replace(/[;{}<>]/g, ""));
48
+ }
49
+ /**
50
+ * Render a CssVarBlock as indented CSS custom-property declarations (no braces).
51
+ * Values come from the validated toHex path so they are hex strings, but we
52
+ * wrap in cssAttrValue for consistency (harmless for safe hex values).
53
+ */
54
+ export function cssVarsDecl(vars) {
55
+ return [
56
+ ` --color-primary: ${vars.primary};`,
57
+ ` --color-accent: ${vars.accent};`,
58
+ ` --color-tertiary: ${vars.tertiary};`,
59
+ ` --color-danger: ${vars.danger};`,
60
+ ` --color-warning: ${vars.warning};`,
61
+ ` --color-success: ${vars.success};`,
62
+ ` --color-background: ${vars.background};`,
63
+ ` --color-foreground: ${vars.foreground};`,
64
+ ` --preview-bg: ${vars.background};`,
65
+ ` --preview-fg: ${vars.foreground};`,
66
+ ].join("\n");
67
+ }
68
+ /**
69
+ * Assemble the full <!DOCTYPE html> preview document.
70
+ *
71
+ * Dark-mode CSS is emitted as TWO separate, valid rules:
72
+ * 1. `:root[data-theme="dark"]` — explicit toggle
73
+ * 2. `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) }`
74
+ * — OS/browser preference (but not when the user has explicitly chosen light)
75
+ *
76
+ * This avoids the invalid CSS form of comma-joining a selector with an at-rule.
77
+ */
78
+ export function buildDocument(args) {
79
+ const { escapedName, lightVars, darkVars } = args;
80
+ const darkVarsDecl = cssVarsDecl(darkVars);
81
+ return `<!DOCTYPE html>
82
+ <html lang="en">
83
+ <head>
84
+ <meta charset="UTF-8" />
85
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
86
+ <title>${escapedName} — Design System Preview</title>
87
+ <style>
88
+ /* ── Light mode tokens (default) ────────────────────────── */
89
+ :root {
90
+ ${cssVarsDecl(lightVars)}
91
+ }
92
+
93
+ /* ── Dark mode tokens — explicit toggle ─────────────────── */
94
+ :root[data-theme="dark"] {
95
+ ${darkVarsDecl}
96
+ }
97
+
98
+ /* ── Dark mode tokens — OS/browser preference ───────────── */
99
+ @media (prefers-color-scheme: dark) {
100
+ :root:not([data-theme="light"]) {
101
+ ${darkVarsDecl}
102
+ }
103
+ }
104
+
105
+ /* ── Base reset ─────────────────────────────────────────── */
106
+ *, *::before, *::after { box-sizing: border-box; }
107
+
108
+ body {
109
+ margin: 0;
110
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
111
+ font-size: 16px;
112
+ line-height: 1.6;
113
+ background: var(--color-background);
114
+ color: var(--color-foreground);
115
+ padding: 2rem;
116
+ }
117
+
118
+ /* ── Layout ─────────────────────────────────────────────── */
119
+ .preview-header {
120
+ border-bottom: 1.5px solid rgba(128,128,128,0.18);
121
+ padding-bottom: 1.5rem;
122
+ margin-bottom: 2.5rem;
123
+ }
124
+ .preview-title {
125
+ font-size: 2rem;
126
+ font-weight: 700;
127
+ margin: 0 0 0.25rem;
128
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
129
+ -webkit-background-clip: text;
130
+ -webkit-text-fill-color: transparent;
131
+ background-clip: text;
132
+ }
133
+ .preview-subtitle {
134
+ font-size: 0.875rem;
135
+ opacity: 0.6;
136
+ margin: 0;
137
+ }
138
+
139
+ .preview-section {
140
+ margin-bottom: 3rem;
141
+ }
142
+ .section-title {
143
+ font-size: 1.125rem;
144
+ font-weight: 600;
145
+ margin: 0 0 1rem;
146
+ padding-bottom: 0.5rem;
147
+ border-bottom: 1px solid rgba(128,128,128,0.12);
148
+ }
149
+
150
+ /* ── Swatches ────────────────────────────────────────────── */
151
+ .swatch-grid {
152
+ display: flex;
153
+ flex-wrap: wrap;
154
+ gap: 1rem;
155
+ }
156
+ .swatch {
157
+ width: 120px;
158
+ }
159
+ .swatch-color {
160
+ width: 120px;
161
+ height: 80px;
162
+ border-radius: 8px;
163
+ display: flex;
164
+ align-items: flex-end;
165
+ padding: 0.4rem;
166
+ border: 1px solid rgba(0,0,0,0.06);
167
+ }
168
+ .swatch-hex {
169
+ font-size: 0.65rem;
170
+ font-family: ui-monospace, monospace;
171
+ font-weight: 600;
172
+ letter-spacing: 0.02em;
173
+ }
174
+ .swatch-label {
175
+ margin-top: 0.4rem;
176
+ font-size: 0.75rem;
177
+ font-weight: 500;
178
+ }
179
+
180
+ /* ── Typography ──────────────────────────────────────────── */
181
+ .type-stack {
182
+ display: flex;
183
+ flex-direction: column;
184
+ gap: 1.5rem;
185
+ }
186
+ .type-sample {
187
+ border-left: 3px solid var(--color-primary);
188
+ padding-left: 1rem;
189
+ }
190
+ .type-meta {
191
+ font-size: 0.7rem;
192
+ font-family: ui-monospace, monospace;
193
+ opacity: 0.55;
194
+ margin-bottom: 0.4rem;
195
+ }
196
+ .type-preview {
197
+ line-height: 1.3;
198
+ }
199
+
200
+ /* ── Components ──────────────────────────────────────────── */
201
+ .component-grid {
202
+ display: flex;
203
+ flex-wrap: wrap;
204
+ gap: 2rem;
205
+ align-items: flex-start;
206
+ }
207
+ .component-item {
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: 0.5rem;
211
+ }
212
+ .component-label {
213
+ font-size: 0.7rem;
214
+ font-family: ui-monospace, monospace;
215
+ opacity: 0.55;
216
+ font-weight: 500;
217
+ text-transform: uppercase;
218
+ letter-spacing: 0.06em;
219
+ }
220
+
221
+ /* ── Radius ──────────────────────────────────────────────── */
222
+ .radius-grid {
223
+ display: flex;
224
+ flex-wrap: wrap;
225
+ gap: 1.5rem;
226
+ align-items: flex-end;
227
+ }
228
+ .radius-item {
229
+ display: flex;
230
+ flex-direction: column;
231
+ align-items: center;
232
+ gap: 0.35rem;
233
+ }
234
+ .radius-demo {
235
+ width: 56px;
236
+ height: 56px;
237
+ background: var(--color-primary);
238
+ opacity: 0.8;
239
+ }
240
+ .radius-name {
241
+ font-size: 0.7rem;
242
+ font-weight: 600;
243
+ }
244
+ .radius-value {
245
+ font-size: 0.65rem;
246
+ font-family: ui-monospace, monospace;
247
+ opacity: 0.55;
248
+ }
249
+ </style>
250
+ </head>
251
+ <body>
252
+
253
+ <header class="preview-header">
254
+ <h1 class="preview-title">${escapedName}</h1>
255
+ <p class="preview-subtitle">Design System Preview &mdash; generated from DTCG token sets</p>
256
+ </header>
257
+
258
+ <main>
259
+ ${args.swatchesHtml}
260
+ ${args.typographyHtml}
261
+ ${args.componentsHtml}
262
+ ${args.radiusHtml}
263
+ </main>
264
+
265
+ </body>
266
+ </html>`;
267
+ }
@@ -0,0 +1,191 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Supported design-sync backend providers.
4
+ *
5
+ * - `figma` — Figma + Tokens Studio plugin (DTCG via GitHub provider sync)
6
+ * - `penpot` — Penpot REST API (self-hostable, China-friendly)
7
+ * - `git-only` — No design tool; reads/writes local DTCG files directly
8
+ * - `memory` — In-memory test fixture (CI / unit tests only)
9
+ * - `design-md` — AI-native DESIGN.md format (@google/design.md) — markdown + YAML front matter
10
+ */
11
+ export type DesignSyncProviderType = "figma" | "penpot" | "git-only" | "memory" | "design-md";
12
+ /**
13
+ * A W3C Design Tokens Community Group (DTCG) leaf token.
14
+ * Every leaf MUST carry both `$value` and `$type`.
15
+ *
16
+ * Spec: https://design-tokens.github.io/community-group/format/
17
+ */
18
+ export declare const DesignTokenLeafSchema: z.ZodObject<{
19
+ $value: z.ZodUnknown;
20
+ $type: z.ZodString;
21
+ $description: z.ZodOptional<z.ZodString>;
22
+ $extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
23
+ }, z.core.$loose>;
24
+ export type DesignTokenLeaf = z.infer<typeof DesignTokenLeafSchema>;
25
+ /**
26
+ * A DTCG token tree is a recursive map of named groups and leaves.
27
+ * Groups are plain objects; leaves carry `$value` + `$type`.
28
+ */
29
+ export type DesignTokenTree = {
30
+ [key: string]: DesignTokenTree | DesignTokenLeaf;
31
+ };
32
+ /**
33
+ * A single DTCG file = one token set (e.g. `core.json`, `themes/light.json`).
34
+ */
35
+ export interface DesignTokenSet {
36
+ /** Token-set name (matches file basename without extension, e.g. "core") */
37
+ name: string;
38
+ /** Path relative to the tokens directory (e.g. "themes/light.json") */
39
+ relativePath: string;
40
+ /** Parsed DTCG tree */
41
+ tokens: DesignTokenTree;
42
+ }
43
+ export interface PullOptions {
44
+ /**
45
+ * Optional theme/token-set filter. When omitted, all sets are pulled.
46
+ * Provider-specific naming applies (e.g. Figma collection IDs).
47
+ */
48
+ themes?: string[];
49
+ /**
50
+ * If true, the provider should validate but NOT write to the local repo.
51
+ * Used by CI to verify a remote state without producing diffs.
52
+ */
53
+ dryRun?: boolean;
54
+ /** Tenant scoping for multi-workspace SaaS deployments */
55
+ tenantId?: string;
56
+ }
57
+ export interface PushOptions {
58
+ /** Restrict the push to specific token sets */
59
+ themes?: string[];
60
+ /**
61
+ * If true, the provider should compute the diff but NOT call the remote API.
62
+ * `figma.push({ dryRun: true })` is the default until the user opts in.
63
+ */
64
+ dryRun?: boolean;
65
+ /** Tenant scoping */
66
+ tenantId?: string;
67
+ }
68
+ export interface PullResult {
69
+ /** Token sets that were retrieved */
70
+ sets: DesignTokenSet[];
71
+ /** Whether the local repo was modified */
72
+ written: boolean;
73
+ /** Provider that produced the result */
74
+ provider: DesignSyncProviderType;
75
+ /** ISO-8601 timestamp */
76
+ pulledAt: string;
77
+ /** Human-readable summary line for CLI output */
78
+ summary: string;
79
+ }
80
+ export interface PushResult {
81
+ /** Whether the remote design tool was modified */
82
+ pushed: boolean;
83
+ /** Token sets that were sent (or would have been sent in a dry-run) */
84
+ sets: string[];
85
+ /** Provider name */
86
+ provider: DesignSyncProviderType;
87
+ /** ISO-8601 timestamp */
88
+ pushedAt: string;
89
+ /** Human-readable summary */
90
+ summary: string;
91
+ /** True when the provider intentionally skipped the call (no creds, dry-run) */
92
+ dryRun: boolean;
93
+ }
94
+ export interface HealthStatus {
95
+ /** Is the provider configured + reachable? */
96
+ ok: boolean;
97
+ /** Provider name */
98
+ provider: DesignSyncProviderType;
99
+ /** Free-form diagnostic */
100
+ message: string;
101
+ /** Detected env var names that are present (not their values) */
102
+ detectedEnv: string[];
103
+ /** Detected env var names that are missing */
104
+ missingEnv: string[];
105
+ }
106
+ /**
107
+ * Every design-sync backend implements this interface.
108
+ * The factory (`createDesignSync`) returns a `DesignSyncProvider`.
109
+ */
110
+ export interface DesignSyncProvider {
111
+ readonly name: DesignSyncProviderType;
112
+ /**
113
+ * Pull: design-tool → repo (DTCG JSON).
114
+ * For `git-only` this is a no-op read of local files.
115
+ */
116
+ pull(options?: PullOptions): Promise<PullResult>;
117
+ /**
118
+ * Push: repo (DTCG JSON) → design-tool.
119
+ * For `git-only` this is a no-op write/format of local files.
120
+ * Real providers default to dry-run unless explicit credentials exist.
121
+ */
122
+ push(options?: PushOptions): Promise<PushResult>;
123
+ /**
124
+ * Confirm the provider has the env vars / file access it needs.
125
+ */
126
+ healthcheck(): Promise<HealthStatus>;
127
+ }
128
+ /**
129
+ * Shared options every provider accepts. The tokens directory is the
130
+ * filesystem source of truth (DTCG JSON), independent of which design tool
131
+ * we sync with.
132
+ */
133
+ export interface BaseProviderConfig {
134
+ /**
135
+ * Absolute path to the DTCG tokens directory.
136
+ * Defaults to `<cwd>/packages/design/design-tokens/tokens`.
137
+ */
138
+ tokensDir?: string;
139
+ /**
140
+ * Absolute path to the `.tokens-studio` metadata directory (if used).
141
+ * Defaults to `<cwd>/.tokens-studio`.
142
+ */
143
+ tokensStudioDir?: string;
144
+ }
145
+ export interface FigmaProviderConfig extends BaseProviderConfig {
146
+ provider: "figma";
147
+ /** Figma personal access token. Defaults to `process.env.FIGMA_PERSONAL_ACCESS_TOKEN`. */
148
+ personalAccessToken?: string;
149
+ /** Figma file ID (the `:file_key` segment in the URL). */
150
+ fileId?: string;
151
+ /** GitHub repo (owner/name) used by the Tokens Studio plugin sync. */
152
+ githubRepo?: string;
153
+ /** GitHub branch the plugin pushes to. Defaults to `main`. */
154
+ githubBranch?: string;
155
+ }
156
+ export interface PenpotProviderConfig extends BaseProviderConfig {
157
+ provider: "penpot";
158
+ /** Penpot API URL — public cloud or self-hosted. */
159
+ apiUrl?: string;
160
+ /** Penpot personal access token. */
161
+ token?: string;
162
+ /** Penpot file ID. */
163
+ fileId?: string;
164
+ /** Penpot team / workspace ID. */
165
+ teamId?: string;
166
+ }
167
+ export interface GitOnlyProviderConfig extends BaseProviderConfig {
168
+ provider: "git-only";
169
+ }
170
+ export interface MemoryProviderConfig extends BaseProviderConfig {
171
+ provider: "memory";
172
+ /** Optional initial in-memory token sets (used in tests). */
173
+ initialSets?: DesignTokenSet[];
174
+ }
175
+ export interface DesignMdProviderConfig extends BaseProviderConfig {
176
+ provider: "design-md";
177
+ /** Absolute path to the DESIGN.md file. Defaults to `process.cwd()/DESIGN.md`. */
178
+ designMdPath?: string;
179
+ /**
180
+ * Design-system name emitted into the generated DESIGN.md front matter +
181
+ * Overview. Defaults to Nebutra branding.
182
+ */
183
+ name?: string;
184
+ /**
185
+ * Design-system description emitted into the generated DESIGN.md front matter +
186
+ * Overview. Defaults to Nebutra branding.
187
+ */
188
+ description?: string;
189
+ }
190
+ export type DesignSyncConfig = FigmaProviderConfig | PenpotProviderConfig | GitOnlyProviderConfig | MemoryProviderConfig | DesignMdProviderConfig;
191
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ // ── DTCG Token Schema ───────────────────────────────────────────────────────
3
+ /**
4
+ * A W3C Design Tokens Community Group (DTCG) leaf token.
5
+ * Every leaf MUST carry both `$value` and `$type`.
6
+ *
7
+ * Spec: https://design-tokens.github.io/community-group/format/
8
+ */
9
+ export const DesignTokenLeafSchema = z
10
+ .object({
11
+ $value: z.unknown(),
12
+ $type: z.string(),
13
+ $description: z.string().optional(),
14
+ $extensions: z.record(z.string(), z.unknown()).optional(),
15
+ })
16
+ .passthrough();
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@nebutra/design-sync",
3
- "version": "0.1.0",
4
- "description": "Provider-agnostic design-tool sync. Figma + Tokens Studio | Penpot | git-only | memory.",
3
+ "version": "0.1.2",
4
+ "description": "Provider-agnostic design-tool sync. Figma + Tokens Studio | Penpot | git-only | design-md | memory.",
5
5
  "private": false,
6
- "license": "AGPL-3.0",
6
+ "license": "MIT",
7
7
  "type": "module",
8
8
  "nebutra": {
9
9
  "status": "foundation",
@@ -16,11 +16,12 @@
16
16
  "gaps": [
17
17
  "Figma push (Variables REST API) is dry-run; manual enable required",
18
18
  "Penpot adapter implements healthcheck + pull; push is dry-run scaffolding",
19
- "No real-time webhook listeners (manual pull/push only)"
19
+ "No real-time webhook listeners (manual pull/push only)",
20
+ "design-md provider depends on @google/design.md (alpha v0.x); pinned exactly — a 0.x bump may change linter rules and require re-validating the serializer output"
20
21
  ],
21
22
  "featureId": "design-sync",
22
23
  "category": "design",
23
- "summary": "Provider-agnostic design-tool sync (Figma | Penpot | git-only)"
24
+ "summary": "Provider-agnostic design-tool sync (Figma | Penpot | git-only | design-md)"
24
25
  },
25
26
  "main": "./dist/index.js",
26
27
  "types": "./dist/index.d.ts",
@@ -29,6 +30,14 @@
29
30
  "types": "./dist/index.d.ts",
30
31
  "import": "./dist/index.js"
31
32
  },
33
+ "./design-md": {
34
+ "types": "./dist/providers/design-md.d.ts",
35
+ "import": "./dist/providers/design-md.js"
36
+ },
37
+ "./serialize/from-design-md": {
38
+ "types": "./dist/serialize/from-design-md.d.ts",
39
+ "import": "./dist/serialize/from-design-md.js"
40
+ },
32
41
  "./figma": {
33
42
  "types": "./dist/providers/figma.d.ts",
34
43
  "import": "./dist/providers/figma.js"
@@ -48,6 +57,10 @@
48
57
  "./cli": {
49
58
  "types": "./dist/cli/index.d.ts",
50
59
  "import": "./dist/cli/index.js"
60
+ },
61
+ "./to-brand-package": {
62
+ "types": "./dist/serialize/to-brand-package.d.ts",
63
+ "import": "./dist/serialize/to-brand-package.js"
51
64
  }
52
65
  },
53
66
  "bin": {
@@ -58,11 +71,13 @@
58
71
  "README.md"
59
72
  ],
60
73
  "dependencies": {
74
+ "@google/design.md": "0.2.0",
61
75
  "zod": "^4.3.6",
62
- "@nebutra/logger": "0.1.0"
76
+ "@nebutra/logger": "0.1.2",
77
+ "@nebutra/tokens": "0.1.2"
63
78
  },
64
79
  "devDependencies": {
65
- "@types/node": "^22.19.15",
80
+ "@types/node": "^25.9.1",
66
81
  "tsx": "^4.21.0",
67
82
  "typescript": "^5.9.3",
68
83
  "vitest": "^4.1.4"
package/src/cli/index.ts CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { describeEnv } from "../detect";
19
19
  import { createDesignSync } from "../factory";
20
+ import { compileBrandFromTokenSets } from "../serialize/to-brand-package";
20
21
  import type { DesignSyncProviderType } from "../types";
21
22
 
22
23
  interface ParsedArgs {
@@ -25,6 +26,8 @@ interface ParsedArgs {
25
26
  json: boolean;
26
27
  themes: string[];
27
28
  provider: DesignSyncProviderType | undefined;
29
+ brandId: string | undefined;
30
+ brandName: string | undefined;
28
31
  }
29
32
 
30
33
  function parseArgs(argv: string[]): ParsedArgs {
@@ -33,6 +36,8 @@ function parseArgs(argv: string[]): ParsedArgs {
33
36
  let json = false;
34
37
  const themes: string[] = [];
35
38
  let provider: DesignSyncProviderType | undefined;
39
+ let brandId: string | undefined;
40
+ let brandName: string | undefined;
36
41
 
37
42
  for (let i = 0; i < rest.length; i++) {
38
43
  const arg = rest[i];
@@ -53,10 +58,14 @@ function parseArgs(argv: string[]): ParsedArgs {
53
58
  provider = next.trim() as DesignSyncProviderType;
54
59
  i++;
55
60
  }
61
+ } else if (arg === "--id") {
62
+ brandId = rest[++i];
63
+ } else if (arg === "--name") {
64
+ brandName = rest[++i];
56
65
  }
57
66
  }
58
67
 
59
- return { command, dryRun, json, themes, provider };
68
+ return { command, dryRun, json, themes, provider, brandId, brandName };
60
69
  }
61
70
 
62
71
  function emit(payload: unknown, json: boolean): void {
@@ -88,12 +97,15 @@ Commands:
88
97
  detect Print the resolved provider + env diagnostics
89
98
  healthcheck Run the provider's healthcheck()
90
99
  pull Pull design-tool → repo (DTCG JSON)
91
- push Push repo → design-tool (defaults to dry-run on figma/penpot)
100
+ push Push repo → design-tool (defaults to dry-run on figma/penpot/design-md)
101
+ brand Pull tokens then compile a Brand Package (Create Center)
92
102
  help Show this message
93
103
 
94
104
  Options:
95
- --provider <X> Override provider (figma|penpot|git-only|memory)
105
+ --provider <X> Override provider (figma|penpot|git-only|memory|design-md)
96
106
  --themes a,b Restrict to specific token sets
107
+ --id <id> Brand package id (for brand command)
108
+ --name <name> Brand package display name
97
109
  --dry-run Force dry-run on push
98
110
  --json Emit machine-readable JSON
99
111
  `;
@@ -149,6 +161,27 @@ export async function run(argv: string[] = process.argv): Promise<number> {
149
161
  return 0;
150
162
  }
151
163
 
164
+ if (args.command === "brand") {
165
+ const themes = args.themes.length > 0 ? { themes: args.themes } : {};
166
+ const pull = await provider.pull(themes);
167
+ const compiled = compileBrandFromTokenSets(pull.sets, {
168
+ ...(args.brandId ? { id: args.brandId } : {}),
169
+ ...(args.brandName ? { name: args.brandName } : {}),
170
+ });
171
+ emit(
172
+ args.json
173
+ ? {
174
+ provider: pull.provider,
175
+ brand: compiled.brand,
176
+ css: compiled.css,
177
+ warnings: compiled.warnings,
178
+ }
179
+ : `brand=${compiled.brand.id} recipe=${compiled.brand.recipe.buttonDefault} fonts=${compiled.brand.typography.faces?.length ?? 0} warnings=${compiled.warnings.length}`,
180
+ args.json,
181
+ );
182
+ return 0;
183
+ }
184
+
152
185
  emitError(`unknown command: ${args.command}\n\n${HELP_TEXT}`, args.json);
153
186
  return 2;
154
187
  } catch (error) {