@webdesignhot/design-md 0.1.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 +29 -8
- package/package.json +1 -1
- package/src/commands/export.mjs +71 -8
- package/src/commands/extract.mjs +1 -1
- package/src/commands/help.mjs +26 -15
- package/src/commands/submit.mjs +240 -0
- package/src/index.mjs +3 -0
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
|
|
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
|
|
@@ -12,7 +12,8 @@ $ npx @webdesignhot/design-md list
|
|
|
12
12
|
arc Arc light warm sans
|
|
13
13
|
…
|
|
14
14
|
|
|
15
|
-
$ npx @webdesignhot/design-md export DESIGN.md --to tailwind >
|
|
15
|
+
$ npx @webdesignhot/design-md export DESIGN.md --to tailwind > theme.css # Tailwind v4 default
|
|
16
|
+
$ npx @webdesignhot/design-md export DESIGN.md --to tailwind --tailwind-version v3 > tailwind.config.js
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
## Why
|
|
@@ -30,7 +31,7 @@ npx @webdesignhot/design-md <command>
|
|
|
30
31
|
# global
|
|
31
32
|
npm i -g @webdesignhot/design-md
|
|
32
33
|
# the binary is `design-md` regardless of scope:
|
|
33
|
-
design-md add
|
|
34
|
+
design-md add stripe
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
Requires Node 18+.
|
|
@@ -57,20 +58,40 @@ lint <file> [--format=text|json]
|
|
|
57
58
|
diff <a> <b> [--format=text|json]
|
|
58
59
|
Token-level diff between two DESIGN.md files.
|
|
59
60
|
|
|
60
|
-
export <file> --to <tailwind|css|dtcg|figma>
|
|
61
|
+
export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma> [--tailwind-version <v3|v4>]
|
|
61
62
|
Convert tokens to one of:
|
|
62
|
-
tailwind
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
66
76
|
|
|
67
77
|
extract <url> [-o <path>] [--token-only]
|
|
68
78
|
Extract a draft DESIGN.md from any production URL.
|
|
69
79
|
(Requires a webdesignhot session — opens the browser flow.)
|
|
70
80
|
|
|
81
|
+
import <url>
|
|
82
|
+
Alias of `extract`. Naming aligned with Google Labs DESIGN.md tooling
|
|
83
|
+
so the same verb works whichever CLI you've internalised.
|
|
84
|
+
|
|
71
85
|
theme <slug> [--dark|--light]
|
|
72
86
|
Compute a dark/light variant of any design.
|
|
73
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
|
+
|
|
74
95
|
preview <slug>
|
|
75
96
|
Open the directory detail page in your browser.
|
|
76
97
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webdesignhot/design-md",
|
|
3
|
-
"version": "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",
|
package/src/commands/export.mjs
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import { readDesignMd } from '../lib/parse.mjs'
|
|
3
3
|
|
|
4
|
-
|
|
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'])
|
|
15
|
+
const TAILWIND_VERSIONS = new Set(['v3', 'v4'])
|
|
5
16
|
|
|
6
17
|
function parseFlags(args) {
|
|
7
|
-
const flags = { to: null }
|
|
18
|
+
const flags = { to: null, tailwindVersion: 'v4' }
|
|
8
19
|
const positional = []
|
|
9
20
|
for (let i = 0; i < args.length; i++) {
|
|
10
21
|
const a = args[i]
|
|
11
22
|
if (a.startsWith('--to=')) flags.to = a.slice(5)
|
|
12
23
|
else if (a === '--to') flags.to = args[++i]
|
|
24
|
+
else if (a.startsWith('--tailwind-version=')) flags.tailwindVersion = a.slice(19)
|
|
25
|
+
else if (a === '--tailwind-version') flags.tailwindVersion = args[++i]
|
|
13
26
|
else positional.push(a)
|
|
14
27
|
}
|
|
15
28
|
return { flags, positional }
|
|
@@ -25,13 +38,44 @@ function toCss(data) {
|
|
|
25
38
|
return lines.join('\n')
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
function
|
|
29
|
-
|
|
41
|
+
function flattenColors(c) {
|
|
42
|
+
// Multi-theme: pick the default theme (or first one) for export.
|
|
43
|
+
const values = Object.values(c)
|
|
44
|
+
if (values.length > 0 && values.every((v) => v && typeof v === 'object')) {
|
|
45
|
+
return values[0]
|
|
46
|
+
}
|
|
47
|
+
return c
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toTailwindV4(data) {
|
|
51
|
+
const c = flattenColors(data.colors ?? {})
|
|
52
|
+
const r = data.radius ?? {}
|
|
53
|
+
const s = data.spacing?.scale
|
|
54
|
+
const lines = ['@theme {']
|
|
55
|
+
for (const [k, v] of Object.entries(c)) {
|
|
56
|
+
if (typeof v === 'string') lines.push(` --color-${k}: ${v};`)
|
|
57
|
+
}
|
|
58
|
+
for (const [k, v] of Object.entries(r)) {
|
|
59
|
+
lines.push(` --radius-${k}: ${typeof v === 'number' ? `${v}px` : v};`)
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(s)) {
|
|
62
|
+
s.forEach((unit, i) => lines.push(` --spacing-${i}: ${typeof unit === 'number' ? `${unit}px` : unit};`))
|
|
63
|
+
}
|
|
64
|
+
lines.push('}')
|
|
65
|
+
return lines.join('\n')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toTailwindV3(data) {
|
|
69
|
+
const c = flattenColors(data.colors ?? {})
|
|
30
70
|
const r = data.radius ?? {}
|
|
31
71
|
const out = {
|
|
32
72
|
theme: {
|
|
33
73
|
extend: {
|
|
34
|
-
colors: Object.fromEntries(
|
|
74
|
+
colors: Object.fromEntries(
|
|
75
|
+
Object.entries(c)
|
|
76
|
+
.filter(([, v]) => typeof v === 'string')
|
|
77
|
+
.map(([k, v]) => [k, String(v)]),
|
|
78
|
+
),
|
|
35
79
|
borderRadius: Object.fromEntries(
|
|
36
80
|
Object.entries(r).map(([k, v]) => [k, typeof v === 'number' ? `${v}px` : v]),
|
|
37
81
|
),
|
|
@@ -41,6 +85,10 @@ function toTailwind(data) {
|
|
|
41
85
|
return `module.exports = ${JSON.stringify(out, null, 2)}`
|
|
42
86
|
}
|
|
43
87
|
|
|
88
|
+
function toTailwind(data, version = 'v4') {
|
|
89
|
+
return version === 'v3' ? toTailwindV3(data) : toTailwindV4(data)
|
|
90
|
+
}
|
|
91
|
+
|
|
44
92
|
function toDtcg(data) {
|
|
45
93
|
const c = data.colors ?? {}
|
|
46
94
|
const r = data.radius ?? {}
|
|
@@ -73,19 +121,34 @@ function toFigma(data) {
|
|
|
73
121
|
const RENDERERS = { tailwind: toTailwind, css: toCss, dtcg: toDtcg, figma: toFigma }
|
|
74
122
|
|
|
75
123
|
export const exportTokens = {
|
|
76
|
-
usage: 'export <file> --to <tailwind|css|dtcg|figma>',
|
|
124
|
+
usage: 'export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma> [--tailwind-version v3|v4]',
|
|
77
125
|
async run(args) {
|
|
78
126
|
const { flags, positional } = parseFlags(args)
|
|
79
127
|
const file = positional[0]
|
|
80
128
|
if (!file || !flags.to) {
|
|
81
|
-
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>')
|
|
82
130
|
process.exit(2)
|
|
83
131
|
}
|
|
84
132
|
if (!FORMATS.has(flags.to)) {
|
|
85
133
|
console.error(`Unknown format "${flags.to}". Choose: ${[...FORMATS].join(', ')}`)
|
|
86
134
|
process.exit(2)
|
|
87
135
|
}
|
|
136
|
+
if (!TAILWIND_VERSIONS.has(flags.tailwindVersion)) {
|
|
137
|
+
console.error(`Unknown --tailwind-version "${flags.tailwindVersion}". Choose: v3, v4`)
|
|
138
|
+
process.exit(2)
|
|
139
|
+
}
|
|
88
140
|
const { data } = await readDesignMd(resolve(process.cwd(), file))
|
|
89
|
-
|
|
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
|
+
}
|
|
152
|
+
process.stdout.write(out + '\n')
|
|
90
153
|
},
|
|
91
154
|
}
|
package/src/commands/extract.mjs
CHANGED
|
@@ -29,6 +29,6 @@ export const extract = {
|
|
|
29
29
|
console.log(` ${kleur.cyan(target)}`)
|
|
30
30
|
console.log()
|
|
31
31
|
console.log(kleur.yellow('Tip: extract requires a webdesignhot session — visit the URL above to authorize.'))
|
|
32
|
-
console.log(kleur.dim('
|
|
32
|
+
console.log(kleur.dim(' A direct extraction endpoint is on the roadmap — for now this opens the web flow.'))
|
|
33
33
|
},
|
|
34
34
|
}
|
package/src/commands/help.mjs
CHANGED
|
@@ -7,26 +7,37 @@ ${kleur.dim('USAGE')}
|
|
|
7
7
|
design-md <command> [args]
|
|
8
8
|
|
|
9
9
|
${kleur.dim('COMMANDS')}
|
|
10
|
-
add <slug>
|
|
11
|
-
list
|
|
12
|
-
category [name]
|
|
13
|
-
init
|
|
14
|
-
lint <file>
|
|
15
|
-
diff <a> <b>
|
|
16
|
-
export <file> --to <f>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
add <slug> Write the design's DESIGN.md to your CWD
|
|
11
|
+
list Print the full catalog (slug · name · tags)
|
|
12
|
+
category [name] Browse categories — without a name lists all
|
|
13
|
+
init Interactive picker (default if no command)
|
|
14
|
+
lint <file> Validate a DESIGN.md for spec compliance
|
|
15
|
+
diff <a> <b> Token-level diff between two DESIGN.md files
|
|
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
|
|
23
|
+
extract <url> Extract a draft DESIGN.md from a production URL
|
|
24
|
+
import <url> Alias of extract — name aligned with Google Labs
|
|
25
|
+
theme <slug> --dark Compute a dark-mode counterpart of a light design
|
|
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
|
|
28
|
+
help This screen
|
|
21
29
|
|
|
22
30
|
${kleur.dim('EXAMPLES')}
|
|
23
|
-
$ design-md add
|
|
31
|
+
$ design-md add stripe ${kleur.dim('# writes DESIGN.md to CWD')}
|
|
24
32
|
$ design-md list | grep editorial
|
|
25
|
-
$ design-md export DESIGN.md --to tailwind >
|
|
33
|
+
$ design-md export DESIGN.md --to tailwind > theme.css
|
|
34
|
+
$ design-md export DESIGN.md --to tailwind --tailwind-version v3 > tailwind.config.js
|
|
35
|
+
$ design-md import https://stripe.com
|
|
26
36
|
$ design-md diff DESIGN.md old.md
|
|
37
|
+
$ design-md submit ./DESIGN.md ${kleur.dim('# auto-fork + open PR via `gh` CLI')}
|
|
27
38
|
|
|
28
|
-
${kleur.dim('Spec:')} https://
|
|
29
|
-
${kleur.dim('Catalog:')} https://www.webdesignhot.com/design
|
|
39
|
+
${kleur.dim('Spec:')} https://www.webdesignhot.com/design.md/spec ${kleur.dim('(webdesignhot/0.1)')}
|
|
40
|
+
${kleur.dim('Catalog:')} https://www.webdesignhot.com/design.md/
|
|
30
41
|
`
|
|
31
42
|
|
|
32
43
|
export const help = {
|
|
@@ -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 = {
|
|
@@ -25,8 +26,10 @@ const COMMANDS = {
|
|
|
25
26
|
diff,
|
|
26
27
|
export: exportTokens,
|
|
27
28
|
extract,
|
|
29
|
+
import: extract, // alias — name aligned with Google Labs PR #40
|
|
28
30
|
theme,
|
|
29
31
|
preview,
|
|
32
|
+
submit,
|
|
30
33
|
help,
|
|
31
34
|
'--help': help,
|
|
32
35
|
'-h': help,
|