@webdesignhot/design-md 0.2.0 → 0.4.0

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
@@ -3,7 +3,7 @@
3
3
  > Drop any DESIGN.md into your repo in one command. Browse, install, lint, diff, and export design systems extracted from real production sites.
4
4
 
5
5
  ```bash
6
- $ npx @webdesignhot/design-md add linear
6
+ $ npx @webdesignhot/design-md add stripe
7
7
  ✓ Wrote DESIGN.md (8.2 KB · linear)
8
8
 
9
9
  $ npx @webdesignhot/design-md list
@@ -31,7 +31,7 @@ npx @webdesignhot/design-md <command>
31
31
  # global
32
32
  npm i -g @webdesignhot/design-md
33
33
  # the binary is `design-md` regardless of scope:
34
- design-md add linear
34
+ design-md add stripe
35
35
  ```
36
36
 
37
37
  Requires Node 18+.
@@ -58,14 +58,21 @@ lint <file> [--format=text|json]
58
58
  diff <a> <b> [--format=text|json]
59
59
  Token-level diff between two DESIGN.md files.
60
60
 
61
- export <file> --to <tailwind|css|dtcg|figma> [--tailwind-version <v3|v4>]
61
+ export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma> [--tailwind-version <v3|v4>]
62
62
  Convert tokens to one of:
63
- tailwind — @theme {} CSS block (Tailwind v4 default).
64
- Pass --tailwind-version v3 for the legacy
65
- module.exports = { theme: { extend: { ... } } } shape.
66
- css :root { --color-bg, --radius-card, }
67
- dtcg — W3C Design Tokens Community Group JSON
68
- figma — Figma Variables import format
63
+ tailwind — @theme {} CSS block (Tailwind v4 default).
64
+ Pass --tailwind-version v3 for the legacy
65
+ module.exports = { theme: { extend: { ... } } } shape.
66
+ css-tailwind v4 @theme {} CSS block (alias of `--to tailwind`,
67
+ matches @google/design.md's `--format css-tailwind`
68
+ naming from PR #64 so commands transfer between
69
+ the two CLIs without re-learning flag conventions).
70
+ json-tailwind — v3 module.exports JSON (alias of
71
+ `--to tailwind --tailwind-version v3`, same
72
+ Google CLI compat reason as above).
73
+ css — :root { --color-bg, --radius-card, … }
74
+ dtcg — W3C Design Tokens Community Group JSON
75
+ figma — Figma Variables import format
69
76
 
70
77
  extract <url> [-o <path>] [--token-only]
71
78
  Extract a draft DESIGN.md from any production URL.
@@ -78,6 +85,13 @@ import <url>
78
85
  theme <slug> [--dark|--light]
79
86
  Compute a dark/light variant of any design.
80
87
 
88
+ submit <file> [--dry-run]
89
+ Submit your DESIGN.md as a PR to the public catalog at
90
+ github.com/WebDesignHot/design-md. Auto-forks via the `gh` CLI,
91
+ syncs the fork with upstream main, branches, commits, pushes,
92
+ and opens a PR with templated body. Requires `gh` installed and
93
+ authed (https://cli.github.com).
94
+
81
95
  preview <slug>
82
96
  Open the directory detail page in your browser.
83
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webdesignhot/design-md",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Drop any DESIGN.md into your repo in one command. Browse, install, lint, diff, and export design systems extracted from real production sites.",
5
5
  "keywords": ["design.md", "design-system", "design-tokens", "ai-agents", "cli", "tailwind", "figma", "dtcg"],
6
6
  "author": "webdesignhot",
@@ -1,7 +1,17 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { readDesignMd } from '../lib/parse.mjs'
3
3
 
4
- const FORMATS = new Set(['tailwind', 'css', 'dtcg', 'figma'])
4
+ // Format aliases align our CLI with @google/design.md's PR #64 naming
5
+ // (`--format css-tailwind` / `--format json-tailwind`) so users can move
6
+ // between the two CLIs without re-learning flag conventions. Both shapes
7
+ // are accepted; the underlying renderer is the same.
8
+ //
9
+ // `--to tailwind` → defaults to v4 CSS (was v3 JSON pre-0.2;
10
+ // same default we set in 0.2 still applies)
11
+ // `--to css-tailwind` → v4 CSS @theme block (Google compat)
12
+ // `--to json-tailwind` → v3 JSON theme.extend (Google compat)
13
+ // `--to tailwind --tailwind-version v3` → v3 JSON (legacy flag, kept)
14
+ const FORMATS = new Set(['tailwind', 'css-tailwind', 'json-tailwind', 'css', 'dtcg', 'figma'])
5
15
  const TAILWIND_VERSIONS = new Set(['v3', 'v4'])
6
16
 
7
17
  function parseFlags(args) {
@@ -111,12 +121,12 @@ function toFigma(data) {
111
121
  const RENDERERS = { tailwind: toTailwind, css: toCss, dtcg: toDtcg, figma: toFigma }
112
122
 
113
123
  export const exportTokens = {
114
- usage: 'export <file> --to <tailwind|css|dtcg|figma> [--tailwind-version v3|v4]',
124
+ usage: 'export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma> [--tailwind-version v3|v4]',
115
125
  async run(args) {
116
126
  const { flags, positional } = parseFlags(args)
117
127
  const file = positional[0]
118
128
  if (!file || !flags.to) {
119
- console.error('Usage: design-md export <file> --to <tailwind|css|dtcg|figma>')
129
+ console.error('Usage: design-md export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma>')
120
130
  process.exit(2)
121
131
  }
122
132
  if (!FORMATS.has(flags.to)) {
@@ -128,9 +138,17 @@ export const exportTokens = {
128
138
  process.exit(2)
129
139
  }
130
140
  const { data } = await readDesignMd(resolve(process.cwd(), file))
131
- const out = flags.to === 'tailwind'
132
- ? toTailwind(data, flags.tailwindVersion)
133
- : RENDERERS[flags.to](data)
141
+
142
+ let out
143
+ if (flags.to === 'tailwind') {
144
+ out = toTailwind(data, flags.tailwindVersion)
145
+ } else if (flags.to === 'css-tailwind') {
146
+ out = toTailwindV4(data)
147
+ } else if (flags.to === 'json-tailwind') {
148
+ out = toTailwindV3(data)
149
+ } else {
150
+ out = RENDERERS[flags.to](data)
151
+ }
134
152
  process.stdout.write(out + '\n')
135
153
  },
136
154
  }
@@ -13,21 +13,28 @@ ${kleur.dim('COMMANDS')}
13
13
  init Interactive picker (default if no command)
14
14
  lint <file> Validate a DESIGN.md for spec compliance
15
15
  diff <a> <b> Token-level diff between two DESIGN.md files
16
- export <file> --to <f> Convert tokens to tailwind|css|dtcg|figma
17
- tailwind defaults to v4 (--tailwind-version v3 for legacy)
16
+ export <file> --to <f> Convert tokens formats:
17
+ tailwind v4 CSS @theme (default; --tailwind-version v3 for legacy)
18
+ css-tailwind v4 CSS @theme (Google CLI alias)
19
+ json-tailwind v3 JSON theme.extend (Google CLI alias)
20
+ css :root { --color-* / --radius-* }
21
+ dtcg W3C Design Tokens JSON
22
+ figma Figma Variables import format
18
23
  extract <url> Extract a draft DESIGN.md from a production URL
19
24
  import <url> Alias of extract — name aligned with Google Labs
20
25
  theme <slug> --dark Compute a dark-mode counterpart of a light design
21
26
  preview <slug> Open the directory detail page in your browser
27
+ submit <file> Open a PR to add your DESIGN.md to the public catalog
22
28
  help This screen
23
29
 
24
30
  ${kleur.dim('EXAMPLES')}
25
- $ design-md add linear ${kleur.dim('# writes DESIGN.md to CWD')}
31
+ $ design-md add stripe ${kleur.dim('# writes DESIGN.md to CWD')}
26
32
  $ design-md list | grep editorial
27
33
  $ design-md export DESIGN.md --to tailwind > theme.css
28
34
  $ design-md export DESIGN.md --to tailwind --tailwind-version v3 > tailwind.config.js
29
35
  $ design-md import https://stripe.com
30
36
  $ design-md diff DESIGN.md old.md
37
+ $ design-md submit ./DESIGN.md ${kleur.dim('# auto-fork + open PR via `gh` CLI')}
31
38
 
32
39
  ${kleur.dim('Spec:')} https://www.webdesignhot.com/design.md/spec ${kleur.dim('(webdesignhot/0.1)')}
33
40
  ${kleur.dim('Catalog:')} https://www.webdesignhot.com/design.md/
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Submit a DESIGN.md to the public catalog at github.com/WebDesignHot/design-md.
3
+ *
4
+ * Flow:
5
+ * 1. Validate file (lint).
6
+ * 2. Resolve slug from frontmatter (or prompt).
7
+ * 3. Verify `gh` CLI is installed and authed.
8
+ * 4. Fork WebDesignHot/design-md to user's account (idempotent).
9
+ * 5. Clone fork to a temp dir.
10
+ * 6. Copy file to design-md/<slug>.md.
11
+ * 7. Branch + commit + push.
12
+ * 8. Open a PR against WebDesignHot/design-md:main with a templated body.
13
+ * 9. Print the PR URL.
14
+ *
15
+ * Aborts gracefully if `gh` is missing or unauthed; falls back to printing
16
+ * the manual-PR instructions (file path + raw upload guide).
17
+ */
18
+ import { resolve, join, basename } from 'node:path'
19
+ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises'
20
+ import { tmpdir } from 'node:os'
21
+ import { execSync, spawnSync } from 'node:child_process'
22
+ import kleur from 'kleur'
23
+ import * as p from '@clack/prompts'
24
+ import { readDesignMd } from '../lib/parse.mjs'
25
+
26
+ const UPSTREAM = 'WebDesignHot/design-md'
27
+ const UPSTREAM_BRANCH = 'main'
28
+
29
+ function parseFlags(args) {
30
+ const flags = { dryRun: false }
31
+ const positional = []
32
+ for (let i = 0; i < args.length; i++) {
33
+ const a = args[i]
34
+ if (a === '--dry-run') flags.dryRun = true
35
+ else positional.push(a)
36
+ }
37
+ return { flags, positional }
38
+ }
39
+
40
+ function which(cmd) {
41
+ const r = spawnSync('which', [cmd], { encoding: 'utf8' })
42
+ return r.status === 0 ? r.stdout.trim() : null
43
+ }
44
+
45
+ function run(cmd, args, opts = {}) {
46
+ const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts })
47
+ if (r.status !== 0) {
48
+ throw new Error(`${cmd} ${args.join(' ')} failed:\n${r.stderr || r.stdout || '(no output)'}`)
49
+ }
50
+ return r.stdout?.trim() ?? ''
51
+ }
52
+
53
+ function deriveSlug(name) {
54
+ return String(name)
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9-]+/g, '-')
57
+ .replace(/^-+|-+$/g, '')
58
+ .replace(/--+/g, '-')
59
+ }
60
+
61
+ async function exists(p) {
62
+ try {
63
+ await stat(p)
64
+ return true
65
+ } catch {
66
+ return false
67
+ }
68
+ }
69
+
70
+ export const submit = {
71
+ usage: 'submit <file> [--dry-run]',
72
+ async run(args) {
73
+ const { flags, positional } = parseFlags(args)
74
+ const file = positional[0]
75
+ if (!file) {
76
+ console.error('Usage: design-md submit <file>')
77
+ console.error(' Submits your DESIGN.md as a PR to github.com/WebDesignHot/design-md.')
78
+ process.exit(2)
79
+ }
80
+
81
+ const filePath = resolve(process.cwd(), file)
82
+ if (!(await exists(filePath))) {
83
+ console.error(kleur.red(`✗ File not found: ${filePath}`))
84
+ process.exit(1)
85
+ }
86
+
87
+ // 1. Lint inline (cheap re-check)
88
+ const { data } = await readDesignMd(filePath)
89
+ const errors = []
90
+ if (!data.name) errors.push('frontmatter.name is required')
91
+ if (!data.spec) errors.push('frontmatter.spec is required (e.g. webdesignhot/0.1)')
92
+ if (!data.source_url) errors.push('frontmatter.source_url is required')
93
+ if (!data.colors?.bg || !data.colors?.text || !data.colors?.brand) {
94
+ errors.push('frontmatter.colors must include bg, text, and brand')
95
+ }
96
+ if (errors.length) {
97
+ console.error(kleur.red('✗ File has issues that must be fixed before submitting:'))
98
+ for (const e of errors) console.error(` · ${e}`)
99
+ console.error(kleur.dim(' Run `design-md lint <file>` for full diagnostics.'))
100
+ process.exit(1)
101
+ }
102
+
103
+ // 2. Resolve slug
104
+ let slug = data.slug
105
+ if (!slug) {
106
+ const fromFile = basename(filePath, '.md')
107
+ slug = fromFile === 'DESIGN' ? deriveSlug(data.name) : fromFile
108
+ }
109
+ if (!slug || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
110
+ console.error(kleur.red(`✗ Invalid slug "${slug}" — must be kebab-case lowercase.`))
111
+ process.exit(1)
112
+ }
113
+
114
+ console.log(kleur.dim(`→ Submitting ${kleur.bold(data.name)} as slug "${kleur.bold(slug)}"`))
115
+
116
+ // 3. Check `gh` CLI
117
+ const ghPath = which('gh')
118
+ if (!ghPath) {
119
+ console.error()
120
+ console.error(kleur.yellow('⚠ The GitHub CLI (`gh`) is not installed.'))
121
+ console.error()
122
+ console.error(' This command auto-forks + opens a PR for you. Install with:')
123
+ console.error(kleur.cyan(' brew install gh # macOS'))
124
+ console.error(kleur.cyan(' winget install GitHub.cli # Windows'))
125
+ console.error(kleur.cyan(' https://cli.github.com — other platforms'))
126
+ console.error()
127
+ console.error(' Or submit manually:')
128
+ console.error(` 1. Fork ${kleur.cyan(`https://github.com/${UPSTREAM}`)}`)
129
+ console.error(` 2. Add your file at ${kleur.cyan(`design-md/${slug}.md`)}`)
130
+ console.error(' 3. Open a PR against `main`.')
131
+ process.exit(1)
132
+ }
133
+
134
+ // Verify gh is authed
135
+ try {
136
+ run('gh', ['auth', 'status'], { stdio: ['ignore', 'pipe', 'pipe'] })
137
+ } catch {
138
+ console.error(kleur.yellow('⚠ `gh` is installed but not authenticated.'))
139
+ console.error(` Run: ${kleur.cyan('gh auth login')}`)
140
+ process.exit(1)
141
+ }
142
+
143
+ if (flags.dryRun) {
144
+ console.log(kleur.green(`✓ Dry-run passed. Would submit ${data.name} as ${slug}.`))
145
+ console.log(kleur.dim(` File: ${filePath}`))
146
+ console.log(kleur.dim(` Target: ${UPSTREAM}/design-md/${slug}.md`))
147
+ return
148
+ }
149
+
150
+ // 4. Fork (idempotent — gh handles already-forked case)
151
+ const ghUser = run('gh', ['api', 'user', '-q', '.login'])
152
+ console.log(kleur.dim(`→ Forking ${UPSTREAM} to ${ghUser}/design-md (no-op if already forked)`))
153
+ try {
154
+ run('gh', ['repo', 'fork', UPSTREAM, '--clone=false'], { stdio: ['ignore', 'pipe', 'pipe'] })
155
+ } catch {
156
+ // gh fork prints "already exists" to stderr but exits 0 sometimes; treat as ok
157
+ }
158
+
159
+ // 5. Clone fork to a temp dir
160
+ const work = join(tmpdir(), `design-md-submit-${slug}-${Date.now()}`)
161
+ await mkdir(work, { recursive: true })
162
+ console.log(kleur.dim(`→ Cloning fork to ${work}`))
163
+ run('gh', ['repo', 'clone', `${ghUser}/design-md`, work, '--', '--depth=1'])
164
+
165
+ // Sync fork with upstream's main (in case fork is stale)
166
+ try {
167
+ run('git', ['-C', work, 'remote', 'add', 'upstream', `https://github.com/${UPSTREAM}.git`])
168
+ run('git', ['-C', work, 'fetch', '--depth=1', 'upstream', UPSTREAM_BRANCH])
169
+ run('git', ['-C', work, 'reset', '--hard', `upstream/${UPSTREAM_BRANCH}`])
170
+ } catch (err) {
171
+ console.error(kleur.yellow(`⚠ Could not sync fork with upstream: ${err.message}`))
172
+ console.error(kleur.dim(' Continuing with current fork state.'))
173
+ }
174
+
175
+ // 6. Copy file
176
+ const targetPath = join(work, 'design-md', `${slug}.md`)
177
+ if (await exists(targetPath)) {
178
+ const overwrite = await p.confirm({
179
+ message: `design-md/${slug}.md already exists in the catalog. Open a PR to update it?`,
180
+ initialValue: false,
181
+ })
182
+ if (p.isCancel(overwrite) || !overwrite) {
183
+ console.log(kleur.yellow('Aborted.'))
184
+ process.exit(0)
185
+ }
186
+ }
187
+ const fileContent = await readFile(filePath, 'utf8')
188
+ await writeFile(targetPath, fileContent)
189
+
190
+ // 7. Branch + commit + push
191
+ const branch = `submit/${slug}-${Date.now().toString(36).slice(-4)}`
192
+ console.log(kleur.dim(`→ Branch: ${branch}`))
193
+ run('git', ['-C', work, 'checkout', '-b', branch])
194
+ run('git', ['-C', work, 'add', `design-md/${slug}.md`])
195
+ const commitMsg = `submit: ${data.name} (${slug})\n\nFrom: ${data.source_url ?? 'unspecified'}`
196
+ run('git', ['-C', work, '-c', 'commit.gpgsign=false', 'commit', '-m', commitMsg])
197
+ console.log(kleur.dim(`→ Pushing to ${ghUser}/design-md:${branch}`))
198
+ run('git', ['-C', work, 'push', '-u', 'origin', branch])
199
+
200
+ // 8. Open PR
201
+ const prTitle = `submit: ${data.name}`
202
+ const prBody = `## New entry: ${data.name}
203
+
204
+ **Slug**: \`${slug}\`
205
+ **Source URL**: ${data.source_url ?? '(not provided)'}
206
+ **Categories**: ${(data.categories ?? []).join(', ') || '(none)'}
207
+ **Tags**: ${(data.tags ?? []).join(', ') || '(none)'}
208
+
209
+ ${data.tagline ? `> ${data.tagline}\n` : ''}
210
+ ${data.description ?? ''}
211
+
212
+ ---
213
+
214
+ Submitted via \`design-md submit\`.
215
+
216
+ ### Reviewer checklist
217
+ - [ ] \`design-md lint\` passes
218
+ - [ ] All 15 sections present in body
219
+ - [ ] Tokens reflect actual production site
220
+ - [ ] Lineage block names real influences with URLs
221
+ - [ ] Categories + tags use existing taxonomy
222
+ `
223
+ console.log(kleur.dim('→ Opening PR'))
224
+ const prUrl = run('gh', [
225
+ 'pr', 'create',
226
+ '--repo', UPSTREAM,
227
+ '--base', UPSTREAM_BRANCH,
228
+ '--head', `${ghUser}:${branch}`,
229
+ '--title', prTitle,
230
+ '--body', prBody,
231
+ ])
232
+
233
+ // 9. Done
234
+ console.log()
235
+ console.log(kleur.green('✓ Submitted.'))
236
+ console.log(` ${kleur.cyan(prUrl)}`)
237
+ console.log()
238
+ console.log(kleur.dim('Your design will appear on webdesignhot.com once we review and merge.'))
239
+ },
240
+ }
package/src/index.mjs CHANGED
@@ -14,6 +14,7 @@ import { exportTokens } from './commands/export.mjs'
14
14
  import { extract } from './commands/extract.mjs'
15
15
  import { theme } from './commands/theme.mjs'
16
16
  import { preview } from './commands/preview.mjs'
17
+ import { submit } from './commands/submit.mjs'
17
18
  import { help } from './commands/help.mjs'
18
19
 
19
20
  const COMMANDS = {
@@ -28,6 +29,7 @@ const COMMANDS = {
28
29
  import: extract, // alias — name aligned with Google Labs PR #40
29
30
  theme,
30
31
  preview,
32
+ submit,
31
33
  help,
32
34
  '--help': help,
33
35
  '-h': help,