@mizumi25/cli 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 (4) hide show
  1. package/docs-generator.js +1302 -536
  2. package/index.js +866 -325
  3. package/package.json +1 -1
  4. package/watcher.js +153 -40
package/index.js CHANGED
@@ -1,416 +1,957 @@
1
1
  #!/usr/bin/env node
2
2
  // packages/cli/index.js
3
3
 
4
- import { watch } from './watcher.js';
5
- import { DocsGenerator } from './docs-generator.js';
6
- import path from 'path';
7
- import fs from 'fs';
8
- import Mizumi from '../core/index.js';
9
- import { fileURLToPath, pathToFileURL } from 'url';
10
-
11
- // Get command from args
12
- const args = process.argv.slice(2);
13
- const command = args[0];
14
-
15
- // CLI commands
4
+ import { watch } from './watcher.js'
5
+ import { DocsGenerator } from './docs-generator.js'
6
+ import { MizuParser, loadMizuFiles, mergeMizuConfigs } from '../core/mizu-parser.js'
7
+ import { Validator } from '../core/validator.js'
8
+ import Mizumi from '../core/index.js'
9
+ import { generateDevToolsScript } from '../vite-plugin/devtools.js'
10
+
11
+ import path from 'node:path'
12
+ import fs from 'node:fs'
13
+ import { fileURLToPath, pathToFileURL } from 'node:url'
14
+ import { resolveClass } from '../core/class-resolver.js'
15
+
16
+
17
+ const args = process.argv.slice(2)
18
+ const command = args[0]
19
+
20
+ // ── Load config — supports mizumi.config.js AND .mizu files ──
21
+ async function loadConfig(configFile = 'mizumi.config.js') {
22
+ const cwd = process.cwd()
23
+ const configPath = path.resolve(cwd, configFile)
24
+ let config = {}
25
+
26
+ // Load JS config if exists
27
+ if (fs.existsSync(configPath)) {
28
+ const mod = await import(pathToFileURL(configPath).href + `?t=${Date.now()}`)
29
+ config = mod.default || mod
30
+ }
31
+
32
+ // Find and merge all .mizu files in src/
33
+ const srcDir = path.resolve(cwd, 'src')
34
+ const rootDir = cwd
35
+ const mizuDirs = [rootDir, srcDir].filter(fs.existsSync)
36
+
37
+ for (const dir of mizuDirs) {
38
+ const mizuConfig = loadMizuFiles(dir)
39
+ if (Object.keys(mizuConfig.tokens).length > 0 ||
40
+ Object.keys(mizuConfig.patterns).length > 0) {
41
+ config = mergeMizuConfigs(config, mizuConfig)
42
+ }
43
+ }
44
+
45
+ return config
46
+ }
47
+
48
+
49
+ function escapeCSSIdent(str) {
50
+ return str.replace(/([^a-zA-Z0-9_\-])/g, '\\$1')
51
+ }
52
+
53
+ const CLASS_RE = /(?:className|class)\s*=\s*(?:"([^"]*?)"|'([^']*?)'|`([^`]*?)`)/g
54
+
55
+ function extractTokens(source) {
56
+ const tokens = new Set()
57
+ for (const m of source.matchAll(CLASS_RE)) {
58
+ const raw = m[1] || m[2] || m[3] || ''
59
+ raw.split(/\s+/).filter(Boolean).forEach(t => tokens.add(t))
60
+ }
61
+ return tokens
62
+ }
63
+
64
+ function walkDir(dir, exts) {
65
+ const results = []
66
+ const extSet = new Set(exts)
67
+ if (!fs.existsSync(dir)) return results
68
+
69
+ function walk(current) {
70
+ let entries
71
+ try { entries = fs.readdirSync(current, { withFileTypes: true }) }
72
+ catch { return }
73
+ for (const entry of entries) {
74
+ const full = path.join(current, entry.name)
75
+ if (entry.isDirectory()) {
76
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') walk(full)
77
+ } else if (entry.isFile()) {
78
+ const ext = entry.name.split('.').pop()
79
+ if (extSet.has(ext)) results.push(full)
80
+ }
81
+ }
82
+ }
83
+ walk(dir)
84
+ return results
85
+ }
86
+
87
+ function isNonCSSClass(token) {
88
+ const skip = ['animate-','hover-','active-','scroll-','stagger-','focus-','duration-','delay-','ease-']
89
+ const variants = ['sm:','md:','lg:','xl:','2xl:','dark:','hover:','focus:','active:','disabled:','motion-safe:','motion-reduce:','print:']
90
+ if (skip.some(p => token.startsWith(p))) return true
91
+ if (variants.some(p => token.startsWith(p))) return true
92
+ if (!token.includes(':') && !token.includes('{')) return true
93
+ return false
94
+ }
95
+
96
+ function scanAndGenerateCSS(root) {
97
+ const exts = ['html', 'htm', 'jsx', 'tsx', 'js', 'ts', 'vue', 'svelte']
98
+ const files = walkDir(root, exts)
99
+ const allToks = new Set()
100
+
101
+ for (const file of files) {
102
+ if (file.includes('node_modules') || file.includes('.mizumi')) continue
103
+ try {
104
+ extractTokens(fs.readFileSync(file, 'utf8')).forEach(t => allToks.add(t))
105
+ } catch { /* skip */ }
106
+ }
107
+
108
+ const lines = []
109
+ const seen = new Set()
110
+
111
+ for (const token of allToks) {
112
+ if (seen.has(token) || isNonCSSClass(token)) continue
113
+ seen.add(token)
114
+ const css = resolveClass(token)
115
+ if (css) lines.push(`.${escapeCSSIdent(token)} { ${css} }`)
116
+ }
117
+
118
+ console.log(` Scanned ${files.length} files, ${lines.length} arbitrary classes found`)
119
+ return lines.join('\n')
120
+ }
121
+
122
+
123
+
16
124
  const commands = {
17
125
 
18
- /**
19
- * Initialize Mizumi in current project
20
- * npx mizumi init
21
- */
126
+ // ── mizumi init ──
22
127
  init() {
23
- console.log('🌊 Initializing Mizumi...\n');
128
+ console.log('💮 Initializing Mizumi...\n')
24
129
 
25
- // Default config template
26
- const configTemplate = `// mizumi.config.js
27
- export {
130
+ const jsConfig = `// mizumi.config.js
131
+ export default {
28
132
  tokens: {
29
133
  colors: {
30
- primary: {
31
- DEFAULT: '#3B82F6',
32
- 50: '#EFF6FF',
33
- 600: '#2563EB',
34
- 900: '#1E3A8A'
35
- },
134
+ primary: { DEFAULT: '#3B82F6', 50: '#EFF6FF', 600: '#2563EB', 900: '#1E3A8A' },
36
135
  secondary: '#8B5CF6',
37
- neutral: {
38
- 50: '#F9FAFB',
39
- 100: '#F3F4F6',
40
- 200: '#E5E7EB',
41
- 500: '#6B7280',
42
- 900: '#111827'
43
- },
44
- surface: '#FFFFFF',
45
- text: '#111827',
46
- success: '#10B981',
47
- error: '#EF4444'
136
+ neutral: { 50: '#F9FAFB', 100: '#F3F4F6', 500: '#6B7280', 900: '#111827' },
137
+ surface: '#FFFFFF',
138
+ ink: '#111827',
139
+ success: '#10B981',
140
+ error: '#EF4444',
48
141
  },
49
-
50
142
  spacing: {
51
- xs: '4px',
52
- sm: '8px',
53
- md: '16px',
54
- lg: '24px',
55
- xl: '32px',
56
- '2xl': '48px',
57
- '3xl': '64px'
143
+ xs: '4px', sm: '8px', md: '16px',
144
+ lg: '24px', xl: '32px', '2xl': '48px', '3xl': '64px',
58
145
  },
59
-
60
146
  typography: {
61
- h1: { size: '48px', weight: 700, line: 1.2 },
62
- h2: { size: '36px', weight: 600, line: 1.3 },
63
- h3: { size: '24px', weight: 600, line: 1.4 },
64
- body: { size: '16px', weight: 400, line: 1.6 },
65
- small: { size: '14px', weight: 400, line: 1.5 }
147
+ h1: { size: '3rem', weight: '700', line: '1.1' },
148
+ h2: { size: '2.25rem', weight: '700', line: '1.2' },
149
+ h3: { size: '1.875rem',weight: '600', line: '1.3' },
150
+ h4: { size: '1.5rem', weight: '600', line: '1.4' },
151
+ body: { size: '1rem', weight: '400', line: '1.6' },
152
+ sm: { size: '0.875rem', weight: '400', line: '1.5' },
153
+ xs: { size: '0.75rem', weight: '400', line: '1.4' },
154
+ },
155
+ fonts: {
156
+ sans: 'system-ui, sans-serif',
157
+ serif: 'Georgia, serif',
158
+ mono: 'monospace',
66
159
  },
67
-
68
160
  radius: {
69
- none: '0',
70
- sm: '4px',
71
- md: '8px',
72
- lg: '16px',
73
- full: '9999px'
161
+ sm: '4px', md: '8px', lg: '12px',
162
+ xl: '16px', full: '9999px',
74
163
  },
75
-
76
164
  shadows: {
77
- none: 'none',
78
- sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
79
- md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
80
- lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
81
- xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)'
165
+ sm: '0 1px 2px rgba(0,0,0,0.05)',
166
+ md: '0 4px 12px rgba(0,0,0,0.1)',
167
+ lg: '0 8px 24px rgba(0,0,0,0.15)',
168
+ xl: '0 16px 48px rgba(0,0,0,0.2)',
169
+ },
170
+ easing: {
171
+ smooth: 'cubic-bezier(0.4,0,0.2,1)',
172
+ bouncy: 'cubic-bezier(0.34,1.56,0.64,1)',
173
+ sharp: 'cubic-bezier(0.4,0,1,1)',
174
+ back: 'cubic-bezier(0.34,1.4,0.64,1)',
175
+ },
176
+ duration: {
177
+ fast: '150ms',
178
+ normal: '300ms',
179
+ slow: '500ms',
180
+ slower: '800ms',
181
+ },
182
+ blur: {
183
+ sm: '4px',
184
+ md: '8px',
185
+ lg: '16px',
186
+ glass: '20px',
187
+ },
188
+ opacity: {
189
+ ghost: '0.1',
190
+ muted: '0.5',
191
+ soft: '0.75',
192
+ full: '1',
193
+ },
194
+ zIndex: {
195
+ base: 0,
196
+ float: 10,
197
+ sticky: 20,
198
+ modal: 100,
199
+ toast: 200,
200
+ top: 999,
201
+ },
202
+ leading: {
203
+ tight: '1.25',
204
+ snug: '1.375',
205
+ normal: '1.5',
206
+ relaxed:'1.625',
207
+ loose: '2',
208
+ },
209
+ tracking: {
210
+ tight: '-0.05em',
211
+ normal: '0em',
212
+ wide: '0.05em',
213
+ wider: '0.1em',
214
+ widest: '0.2em',
82
215
  },
83
-
84
- animations: {
85
- duration: {
86
- fast: 150,
87
- normal: 300,
88
- slow: 500
89
- },
90
- easing: {
91
- smooth: 'power2.out',
92
- bouncy: 'elastic.out(1, 0.5)',
93
- sharp: 'power4.inOut'
94
- }
95
- }
96
216
  },
97
217
 
98
218
  patterns: {
99
- // Layout
100
- 'flex-center': 'flex items-center justify-center',
101
- 'flex-between': 'flex items-center justify-between',
102
- 'flex-col-center': 'flex flex-col items-center justify-center',
219
+ // Surfaces
220
+ card: 'pad:md paint:surface curve:lg cast:md',
221
+ card-flat: 'pad:md paint:surface curve:lg',
222
+ panel: 'pad:lg paint:surface curve:xl cast:lg',
103
223
 
104
- // Containers
105
- container: 'max-w-6xl mx-auto pad-x-md',
106
- section: 'pad-y-2xl',
224
+ // Buttons
225
+ btn: 'pad-y:sm pad-x:lg curve:md ease:default cursor:pointer',
226
+ btn-primary:'btn paint:primary ink:surface type-weight:semi',
227
+ btn-ghost: 'btn stroke-color:primary ink:primary',
228
+ btn-danger: 'btn paint:error ink:surface',
107
229
 
108
- // Cards
109
- card: 'bg-surface pad-md rounded-lg shadow-md',
110
- 'card-elevated': 'card shadow-xl',
111
- 'card-flat': 'card shadow-sm',
112
- 'card-hover': 'card transition cursor-pointer',
230
+ // Typography
231
+ heading: 'type-face:sans type-weight:bold leading:tight',
232
+ body-text: 'type-face:sans leading:relaxed',
233
+ caption: 'text:xs ink:neutral-500',
234
+ label: 'text:sm type-weight:medium tracking:wide text-case:upper',
113
235
 
114
- // Buttons
115
- button: 'pad-sm pad-x-lg rounded-md cursor-pointer transition inline-flex items-center justify-center',
116
- 'btn-primary': 'button bg-primary color-white',
117
- 'btn-secondary': 'button bg-secondary color-white',
118
- 'btn-ghost': 'button bg-transparent color-primary',
119
- 'btn-outline': 'button border border-primary color-primary',
120
-
121
- // Inputs
122
- input: 'pad-sm rounded-md border border-neutral-200 transition',
123
-
124
- // Text
125
- heading: 'text-h1 color-text',
126
- subheading: 'text-h3 color-neutral-500',
127
- 'text-muted': 'color-neutral-500'
236
+ // Layout
237
+ container: 'canvas-w:full mar-x:auto pad-x:md',
238
+ center: 'display:flex align-x:center align-yi:center',
239
+ stack: 'display:flex flex-dir:col',
240
+ row: 'display:flex flex-dir:row align-yi:center',
241
+ grid-auto: 'display:grid grid-cols:repeat(auto-fit,minmax(250px,1fr)) gap:md',
128
242
  },
129
243
 
130
244
  animations: {
131
- // Entrance
132
- 'animate-fade-in': {
245
+ 'fade-in': {
133
246
  from: { opacity: 0 },
134
- to: { opacity: 1 },
135
- duration: 300,
136
- ease: 'power2.out'
247
+ to: { opacity: 1 },
248
+ duration: 'normal',
249
+ ease: 'smooth',
137
250
  },
138
- 'animate-slide-up': {
139
- from: { y: 100, opacity: 0 },
140
- to: { y: 0, opacity: 1 },
141
- duration: 300,
142
- ease: 'power2.out'
251
+ 'slide-up': {
252
+ from: { opacity: 0, y: 40 },
253
+ to: { opacity: 1, y: 0 },
254
+ duration: 'normal',
255
+ ease: 'smooth',
143
256
  },
144
- 'animate-scale-in': {
145
- from: { scale: 0, opacity: 0 },
146
- to: { scale: 1, opacity: 1 },
147
- duration: 300,
148
- ease: 'back.out'
149
- },
150
-
151
- // Scroll
152
- 'scroll-trigger': {
153
- scrollTrigger: {
154
- trigger: 'self',
155
- start: 'top 80%',
156
- toggleActions: 'play none none reverse'
157
- }
158
- },
159
-
160
- // Hover
161
257
  'hover-lift': {
162
- hover: { y: -8, duration: 150 }
163
- },
164
- 'hover-scale': {
165
- hover: { scale: 1.05, duration: 150 }
258
+ hover: { y: -6, duration: 0.2, ease: 'smooth' },
166
259
  },
167
260
  'hover-glow': {
168
- hover: {
169
- boxShadow: '0 0 20px rgba(59, 130, 246, 0.5)',
170
- duration: 150
171
- }
261
+ hover: { boxShadow: '0 0 24px rgba(59,130,246,0.4)', duration: 0.3 },
172
262
  },
173
-
174
- // Click
175
263
  'active-press': {
176
- active: { scale: 0.95, duration: 100 }
264
+ active: { scale: 0.97, duration: 0.1 },
177
265
  },
178
-
179
- // Stagger
180
- 'stagger-children-100': {
181
- targets: 'children',
182
- stagger: 0.1,
183
- from: { opacity: 0, y: 20 },
184
- to: { opacity: 1, y: 0 }
185
- }
186
266
  },
187
267
 
188
268
  rules: {
189
- darkMode: false,
190
- responsive: true,
269
+ responsive: true,
270
+ darkMode: 'class',
271
+ print: false,
272
+ motion: true,
273
+ orientation: false,
274
+
191
275
  breakpoints: {
192
- sm: '640px',
193
- md: '768px',
194
- lg: '1024px',
195
- xl: '1280px'
196
- }
276
+ sm: '640px',
277
+ md: '768px',
278
+ lg: '1024px',
279
+ xl: '1280px',
280
+ '2xl': '1536px',
281
+ },
282
+
283
+ containers: {
284
+ card: { sm: '300px', md: '500px' },
285
+ sidebar: { collapsed: '200px', expanded: '320px' },
286
+ },
287
+ },
288
+ }
289
+ `
290
+
291
+ const mimuExample = `/* styles.mizu — CSS-style alternative to mizumi.config.js */
292
+ /* Both files are supported — they get merged automatically */
293
+
294
+ @token {
295
+ colors {
296
+ brand: #6366f1;
197
297
  }
198
- };
199
- `;
298
+ spacing {
299
+ section: 80px;
300
+ hero: 120px;
301
+ }
302
+ }
200
303
 
201
- // Check if config already exists
202
- const configPath = path.join(process.cwd(), 'mizumi.config.js');
304
+ @pattern hero {
305
+ pad-y: hero;
306
+ display: flex;
307
+ flex-dir: col;
308
+ align-yi: center;
309
+ align-x: center;
310
+ text-align: center;
311
+ }
203
312
 
204
- if (fs.existsSync(configPath)) {
205
- console.log('⚠️ mizumi.config.js already exists, skipping...');
313
+ @pattern section {
314
+ pad-y: section;
315
+ canvas-w: full;
316
+ }
317
+ `
318
+
319
+ if (!fs.existsSync('mizumi.config.js')) {
320
+ fs.writeFileSync('mizumi.config.js', jsConfig)
321
+ console.log('✅ Created mizumi.config.js')
206
322
  } else {
207
- fs.writeFileSync(configPath, configTemplate);
208
- console.log('✅ Created: mizumi.config.js');
323
+ console.log('⏭️ mizumi.config.js already exists — skipping')
209
324
  }
210
325
 
211
- // Create .mizumi directory
212
- const mizumiDir = path.join(process.cwd(), '.mizumi');
213
- if (!fs.existsSync(mizumiDir)) {
214
- fs.mkdirSync(mizumiDir);
326
+ if (!fs.existsSync('styles.mizu')) {
327
+ fs.writeFileSync('styles.mizu', mimuExample)
328
+ console.log('✅ Created styles.mizu')
215
329
  }
216
330
 
217
- // Create .gitignore entry
218
- const gitignorePath = path.join(process.cwd(), '.gitignore');
219
- if (fs.existsSync(gitignorePath)) {
220
- const gitignore = fs.readFileSync(gitignorePath, 'utf8');
221
- if (!gitignore.includes('.mizumi')) {
222
- fs.appendFileSync(gitignorePath, '\n# Mizumi generated files\n.mizumi/\n');
223
- console.log('✅ Updated: .gitignore');
331
+ console.log('\n💮 Run: npx mizumi build\n')
332
+ },
333
+
334
+ // ── mizumi build ──
335
+ async build() {
336
+ console.log('💮 Mizumi: Building...\n')
337
+ try {
338
+ const cwd = process.cwd()
339
+ const config = await loadConfig()
340
+ const mizumi = new Mizumi(config)
341
+ const outDir = '.mizumi'
342
+
343
+ // Generate base CSS from tokens/patterns
344
+ let css = mizumi.generateCSS()
345
+
346
+ // Scan source files and append arbitrary classes
347
+ const scanned = scanAndGenerateCSS(cwd)
348
+ if (scanned) {
349
+ css += '\n\n/* ===== SCANNED ARBITRARY CLASSES ===== */\n' + scanned
224
350
  }
225
- } else {
226
- fs.writeFileSync(gitignorePath, '# Mizumi generated files\n.mizumi/\n');
227
- console.log('✅ Created: .gitignore');
351
+
352
+ // Write all outputs
353
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true })
354
+
355
+ fs.writeFileSync(path.join(outDir, 'mizumi.css'), css)
356
+ console.log(`✅ CSS: ${path.join(outDir, 'mizumi.css')} (${(Buffer.byteLength(css)/1024).toFixed(2)} KB)`)
357
+
358
+ const js = mizumi.generateRuntimeScript()
359
+ fs.writeFileSync(path.join(outDir, 'mizumi-runtime.js'), js)
360
+ console.log(`✅ Runtime: ${path.join(outDir, 'mizumi-runtime.js')} (${(Buffer.byteLength(js)/1024).toFixed(2)} KB)`)
361
+
362
+ const depthJs = mizumi.depthEngine.generateRuntimeScript()
363
+ fs.writeFileSync(path.join(outDir, 'mizumi-depth-runtime.js'), depthJs)
364
+ console.log(`✅ Depth: ${path.join(outDir, 'mizumi-depth-runtime.js')} (${(Buffer.byteLength(depthJs)/1024).toFixed(2)} KB)`)
365
+
366
+ const dts = mizumi.typesGenerator.generateDTS()
367
+ fs.writeFileSync(path.join(outDir, 'mizumi.d.ts'), dts)
368
+ console.log(`✅ Types: ${path.join(outDir, 'mizumi.d.ts')} (${(Buffer.byteLength(dts)/1024).toFixed(2)} KB)`)
369
+
370
+ const helpers = mizumi.typesGenerator.generateHelpers()
371
+ fs.writeFileSync(path.join(outDir, 'mizumi-helpers.js'), helpers)
372
+ console.log(`✅ Helpers: ${path.join(outDir, 'mizumi-helpers.js')} (${(Buffer.byteLength(helpers)/1024).toFixed(2)} KB)`)
373
+
374
+ const metaObj = {
375
+ tokens: config.tokens,
376
+ patterns: config.patterns,
377
+ animations:config.animations,
378
+ rules: config.rules,
379
+ generated: new Date().toISOString()
380
+ }
381
+ const meta = JSON.stringify(metaObj, null, 2)
382
+ fs.writeFileSync(path.join(outDir, 'mizumi.meta.json'), meta)
383
+ console.log(`✅ Meta: ${path.join(outDir, 'mizumi.meta.json')} (${(Buffer.byteLength(meta)/1024).toFixed(2)} KB)`)
384
+
385
+ // Always write devtools — inactive until mizumi-devtools.js is loaded
386
+ const devtools = generateDevToolsScript({ tokens: metaObj.tokens, patterns: metaObj.patterns, animations: metaObj.animations })
387
+ fs.writeFileSync(path.join(outDir, 'mizumi-devtools.js'), devtools)
388
+ console.log(`✅ DevTools:${path.join(outDir, 'mizumi-devtools.js')} (${(Buffer.byteLength(devtools)/1024).toFixed(2)} KB)`)
389
+
390
+ console.log('\n✅ Build complete')
391
+ console.log(' 💡 Add to HTML for DevTools: <script src=".mizumi/mizumi-devtools.js"></script>')
392
+ } catch (err) {
393
+ console.error('❌ Build failed:', err.message)
394
+ process.exit(1)
228
395
  }
396
+ },
229
397
 
230
- console.log('\n🎉 Mizumi initialized successfully!');
231
- console.log('\nNext steps:');
232
- console.log(' 1. Edit mizumi.config.js to customize your tokens');
233
- console.log(' 2. Run: node mizumi build');
234
- console.log(' 3. Import .mizumi/mizumi.css in your project');
398
+ // ── mizumi watch ──
399
+ async watch() {
400
+ console.log('💮 Mizumi: Watching...\n')
401
+ await commands.build()
402
+ watch(loadConfig, Mizumi, generateDevToolsScript)
235
403
  },
236
404
 
237
- /**
238
- * Build CSS from config
239
- * npx mizumi build
240
- */
241
- build: async function() {
242
- console.log('🌊 Building Mizumi CSS...\n');
243
-
244
- // Find config file
245
- const configPath = path.join(process.cwd(), 'mizumi.config.js');
246
- if (!fs.existsSync(configPath)) {
247
- console.error('❌ mizumi.config.js not found!');
248
- console.error(' Run: node mizumi init');
249
- process.exit(1);
250
- }
405
+ // ── mizumi validate ──
406
+ async validate() {
407
+ console.log('💮 Mizumi: Validating config...\n')
408
+ try {
409
+ const config = await loadConfig()
410
+ const validator = new Validator(config)
411
+ const result = validator.validate()
412
+
413
+ if (result.valid && result.warnings.length === 0) {
414
+ console.log('✅ Config is valid — no issues found\n')
415
+ return
416
+ }
251
417
 
252
- // Load config
253
- const configModule = await import(pathToFileURL(configPath).href);
254
- const config = configModule.default || configModule; // ← handles both ESM and CommonJS
418
+ if (result.errors.length > 0) {
419
+ console.log(`❌ ${result.errors.length} error(s):\n`)
420
+ result.errors.forEach(e => console.log(e + '\n'))
421
+ }
255
422
 
256
- // Generate CSS + Runtime JS
257
- const mizumi = new Mizumi(config);
258
-
423
+ if (result.warnings.length > 0) {
424
+ console.log(`⚠️ ${result.warnings.length} warning(s):\n`)
425
+ result.warnings.forEach(w => console.log(w + '\n'))
426
+ }
427
+
428
+ if (!result.valid) process.exit(1)
429
+
430
+ } catch (err) {
431
+ console.error('❌ Validation failed:', err.message)
432
+ process.exit(1)
433
+ }
434
+ },
259
435
 
260
- const outDir = path.join(process.cwd(), '.mizumi');
261
- mizumi.build(outDir); // ← now passes directory, not file path
436
+ // ── mizumi analyze ──
437
+ async analyze() {
438
+ console.log('💮 Mizumi: Analyzing design system...\n')
439
+ try {
440
+ const config = await loadConfig()
441
+ const mizumi = new Mizumi(config)
442
+
443
+ // Token stats
444
+ const tokens = config.tokens || {}
445
+ const patterns = config.patterns || {}
446
+ const anims = config.animations || {}
447
+
448
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
449
+ console.log(' MIZUMI DESIGN SYSTEM ANALYSIS')
450
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
451
+
452
+ // Token summary
453
+ console.log('📦 TOKENS')
454
+ for (const [category, values] of Object.entries(tokens)) {
455
+ const count = typeof values === 'object'
456
+ ? Object.keys(values).length
457
+ : 1
458
+ console.log(` ${category.padEnd(16)} ${count} values`)
459
+ }
262
460
 
263
- console.log('\n🎉 Build complete!');
264
- console.log(' Add to your HTML:');
265
- console.log(' <link rel="stylesheet" href=".mizumi/mizumi.css">');
266
- console.log(' <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>');
267
- console.log(' <script src=".mizumi/mizumi-runtime.js"></script>');
268
- },
461
+ // Pattern summary
462
+ console.log(`\n🎨 PATTERNS (${Object.keys(patterns).length} total)`)
463
+ for (const [name, value] of Object.entries(patterns)) {
464
+ const classes = value.split(/\s+/).filter(Boolean)
465
+ console.log(` .${name.padEnd(20)} ${classes.length} utilities`)
466
+ }
269
467
 
468
+ // Animation summary
469
+ console.log(`\n✨ ANIMATIONS (${Object.keys(anims).length} total)`)
470
+ for (const [name] of Object.entries(anims)) {
471
+ console.log(` ${name}`)
472
+ }
270
473
 
474
+ // CSS output size estimate
475
+ const css = mizumi.generateCSS()
476
+ const kb = (Buffer.byteLength(css) / 1024).toFixed(2)
477
+ console.log(`\n📄 ESTIMATED OUTPUT`)
478
+ console.log(` CSS size: ${kb} KB`)
479
+ console.log(` Utilities generated: ${css.match(/\./g)?.length || 0}`)
480
+
481
+ // Unused token check — only warn for tokens expected in patterns
482
+ // easing/duration/blur/zIndex/leading/tracking are utility tokens
483
+ // used inline in HTML, not required in patterns
484
+ const PATTERN_TOKEN_CATEGORIES = ['colors', 'spacing', 'typography', 'radius', 'shadows', 'fonts']
485
+ console.log('\n🔍 TOKEN USAGE IN PATTERNS')
486
+ const allPatternText = Object.values(patterns).join(' ')
487
+ let unusedCount = 0
488
+ for (const [category, values] of Object.entries(tokens)) {
489
+ if (!PATTERN_TOKEN_CATEGORIES.includes(category)) continue
490
+ if (typeof values !== 'object') continue
491
+ for (const key of Object.keys(values)) {
492
+ const used = allPatternText.includes(`:${key}`)
493
+ if (!used) {
494
+ console.log(` ⚠️ ${category}.${key} — defined but not used in any pattern`)
495
+ unusedCount++
496
+ }
497
+ }
498
+ }
499
+ if (unusedCount === 0) {
500
+ console.log(' ✅ All pattern tokens are in use')
501
+ }
502
+ console.log('\n ℹ️ easing/duration/blur/opacity/zIndex/leading/tracking')
503
+ console.log(' are utility tokens — use inline in HTML/JSX as needed')
271
504
 
505
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
272
506
 
273
- /**
274
- * List all patterns
275
- * npx mizumi list
276
- */
277
- list: async function() {
278
- const configPath = path.join(process.cwd(), 'mizumi.config.js');
507
+ } catch (err) {
508
+ console.error('❌ Analysis failed:', err.message)
509
+ }
510
+ },
279
511
 
280
- if (!fs.existsSync(configPath)) {
281
- console.error('❌ mizumi.config.js not found! Run: node mizumi init');
282
- process.exit(1);
512
+ // ── mizumi convert ──
513
+ // Converts Tailwind class strings to Mizumi equivalents
514
+ convert() {
515
+ const input = args[1]
516
+ if (!input) {
517
+ console.log('Usage: mizumi convert "p-4 bg-blue-500 rounded-lg text-white"')
518
+ return
283
519
  }
284
520
 
285
-
286
- const configModule = await import(pathToFileURL(configPath).href);
287
- const config = configModule.default || configModule; // ← handles both ESM and CommonJS
521
+ const TAILWIND_MAP = {
522
+ 'flex': 'display:flex',
523
+ 'grid': 'display:grid',
524
+ 'block': 'display:block',
525
+ 'inline': 'display:inline',
526
+ 'hidden': 'display:none',
527
+ 'relative': 'pos:relative',
528
+ 'absolute': 'pos:absolute',
529
+ 'fixed': 'pos:fixed',
530
+ 'sticky': 'pos:sticky',
531
+ 'w-full': 'canvas-w:full',
532
+ 'h-full': 'canvas-h:full',
533
+ 'w-screen': 'canvas-w:screen',
534
+ 'h-screen': 'canvas-h:screen',
535
+ 'mx-auto': 'mar-x:auto',
536
+ 'items-center': 'align-yi:center',
537
+ 'items-start': 'align-yi:start',
538
+ 'items-end': 'align-yi:end',
539
+ 'justify-center': 'align-x:center',
540
+ 'justify-between': 'align-x:between',
541
+ 'justify-start': 'align-x:start',
542
+ 'justify-end': 'align-x:end',
543
+ 'flex-col': 'flex-dir:col',
544
+ 'flex-row': 'flex-dir:row',
545
+ 'flex-wrap': 'flex-wrap:yes',
546
+ 'flex-1': 'flex:1',
547
+ 'font-bold': 'type-weight:bold',
548
+ 'font-semibold': 'type-weight:semi',
549
+ 'font-medium': 'type-weight:medium',
550
+ 'font-normal': 'type-weight:normal',
551
+ 'font-light': 'type-weight:light',
552
+ 'italic': 'type-style:italic',
553
+ 'uppercase': 'text-case:upper',
554
+ 'lowercase': 'text-case:lower',
555
+ 'capitalize': 'text-case:capital',
556
+ 'text-center': 'text-align:center',
557
+ 'text-left': 'text-align:left',
558
+ 'text-right': 'text-align:right',
559
+ 'overflow-hidden': 'overflow:hidden',
560
+ 'overflow-auto': 'overflow:auto',
561
+ 'overflow-scroll': 'overflow:scroll',
562
+ 'cursor-pointer': 'cursor:pointer',
563
+ 'cursor-default': 'cursor:default',
564
+ 'select-none': 'select:none',
565
+ 'pointer-events-none': 'events:none',
566
+ 'truncate': 'overflow:hidden text-overflow:dots wrap:no',
567
+ 'border': 'stroke-width:1px stroke-style:solid',
568
+ 'border-none': 'stroke-style:none',
569
+ 'list-none': 'list:none',
570
+ }
288
571
 
289
- // Generate CSS + Runtime JS
290
- const mizumi = new Mizumi(config);
291
-
292
- console.log('\n🌊 Mizumi Patterns:\n');
572
+ const classes = input.split(/\s+/).filter(Boolean)
573
+ const converted = []
574
+ const unknown = []
293
575
 
294
- for (const [name, value] of Object.entries(config.patterns || {})) {
295
- const expanded = mizumi.expandClassName(name);
296
- console.log(` .${name}`);
297
- console.log(` ${expanded}`);
298
- console.log('');
576
+ for (const cls of classes) {
577
+ if (TAILWIND_MAP[cls]) {
578
+ converted.push(TAILWIND_MAP[cls])
579
+ } else if (/^p-(\d+)$/.exec(cls)) { converted.push(`pad:your-spacing-token /* was ${cls} */`) }
580
+ else if (/^px-(\d+)$/.exec(cls)) { converted.push(`pad-x:your-spacing-token /* was ${cls} */`) }
581
+ else if (/^py-(\d+)$/.exec(cls)) { converted.push(`pad-y:your-spacing-token /* was ${cls} */`) }
582
+ else if (/^m-(\d+)$/.exec(cls)) { converted.push(`mar:your-spacing-token /* was ${cls} */`) }
583
+ else if (/^mx-(\d+)$/.exec(cls)) { converted.push(`mar-x:your-spacing-token /* was ${cls} */`) }
584
+ else if (/^my-(\d+)$/.exec(cls)) { converted.push(`mar-y:your-spacing-token /* was ${cls} */`) }
585
+ else if (/^gap-(\d+)$/.exec(cls)) { converted.push(`gap:your-spacing-token /* was ${cls} */`) }
586
+ else if (/^rounded/.exec(cls)) { converted.push(`curve:your-radius-token /* was ${cls} */`) }
587
+ else if (/^shadow/.exec(cls)) { converted.push(`cast:your-shadow-token /* was ${cls} */`) }
588
+ else if (/^bg-/.exec(cls)) { converted.push(`paint:your-color-token /* was ${cls} */`) }
589
+ else if (/^text-/.exec(cls)) { converted.push(`text:your-type-token /* was ${cls} */`) }
590
+ else if (/^text-\[/.exec(cls)) { converted.push(`type-size:your-size-token /* was ${cls} */`) }
591
+ else if (/^border-/.exec(cls)) { converted.push(`stroke-color:your-color /* was ${cls} */`) }
592
+ else if (/^text-(white|black)/.exec(cls)) { converted.push(`ink:your-color-token /* was ${cls} */`) }
593
+ else { unknown.push(cls) }
299
594
  }
300
595
 
301
- console.log('\n🌊 Mizumi Animations:\n');
596
+ console.log('\n💮 Mizumi equivalent:\n')
597
+ console.log(converted.join('\n'))
302
598
 
303
- for (const [name, value] of Object.entries(config.animations || {})) {
304
- console.log(` .${name}`);
305
- console.log(` → GSAP: ${JSON.stringify(value).slice(0, 60)}...`);
306
- console.log('');
599
+ if (unknown.length > 0) {
600
+ console.log(`\n⚠️ Could not convert (no direct equivalent):\n ${unknown.join(', ')}`)
601
+ console.log(' Check: https://github.com/Mizumi25/MizumiPackage')
307
602
  }
603
+
604
+ console.log('')
308
605
  },
309
606
 
310
- /**
311
- * Explain what a class does
312
- * npx mizumi explain "card"
313
- */
314
- explain: async function() {
315
- const className = args[1];
316
-
317
- if (!className) {
318
- console.error('❌ Please provide a class name');
319
- console.error(' Usage: node mizumi explain "card"');
320
- process.exit(1);
607
+ // ── mizumi docs ──
608
+ async docs() {
609
+ console.log('💮 Mizumi: Generating docs...\n')
610
+ try {
611
+ const config = await loadConfig()
612
+ const docs = new DocsGenerator(config)
613
+ docs.generate('.mizumi/docs')
614
+ console.log('✅ Docs generated in .mizumi/docs/')
615
+ } catch (err) {
616
+ console.error(' Docs failed:', err.message)
321
617
  }
618
+ },
322
619
 
323
- const configPath = path.join(process.cwd(), 'mizumi.config.js');
324
-
325
- const configModule = await import(pathToFileURL(configPath).href);
326
- const config = configModule.default || configModule;
620
+ // ── mizumi help ──
621
+ help() {
622
+ console.log(`
623
+ 💮 Mizumi CLI
624
+
625
+ COMMANDS:
626
+ init Create mizumi.config.js and styles.mizu starter files
627
+ build Build CSS, runtime, types and meta from your config
628
+ watch Build then watch for config changes
629
+ validate Check your config for errors and Tailwind/raw CSS usage
630
+ analyze Design system health report — tokens, patterns, usage
631
+ convert Convert Tailwind class string to Mizumi vocabulary
632
+ docs Generate HTML documentation from your config
633
+ help Show this help
634
+
635
+ EXAMPLES:
636
+ npx mizumi init
637
+ npx mizumi build
638
+ npx mizumi validate
639
+ npx mizumi analyze
640
+ npx mizumi convert "p-4 bg-blue-500 flex items-center rounded-lg"
641
+
642
+ FILES:
643
+ mizumi.config.js JS config (for JS developers)
644
+ *.mizu CSS-style config (for CSS developers)
645
+ Both are auto-detected and merged.
646
+
647
+ OUTPUT (.mizumi/):
648
+ mizumi.css All generated CSS
649
+ mizumi-runtime.js GSAP animation runtime
650
+ mizumi.d.ts TypeScript types
651
+ mizumi-helpers.js JS class name helpers
652
+ mizumi.meta.json Config metadata
653
+ `)
654
+ },
655
+
656
+
657
+
658
+
659
+ // ── ADD THESE TO THE commands OBJECT IN packages/cli/index.js ──
327
660
 
328
- const mizumi = new Mizumi(config);
329
661
 
330
- console.log(`\n🌊 Explaining: "${className}"\n`);
662
+ // ── mizumi sync:defaults ──
663
+ // Reads packages/core/defaults.js and syncs into pattern-expander.js,
664
+ // parser.js, animation-engine parent (index.js), variant-generator.js
665
+ async ['sync:defaults']() {
666
+ console.log('💮 Mizumi: Syncing defaults...\n')
331
667
 
332
- const classes = className.split(' ');
668
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
669
+ const coreDir = path.resolve(__dirname, '../core')
670
+ const defaultsPath = path.resolve(coreDir, 'defaults.js')
333
671
 
334
- for (const cls of classes) {
335
- // Check if it's a pattern
336
- if (config.patterns?.[cls]) {
337
- const expanded = mizumi.expandClassName(cls);
338
- console.log(` Pattern: .${cls}`);
339
- console.log(` Expands to: ${expanded}`);
340
- }
341
- // Check if it's an animation
342
- else if (config.animations?.[cls]) {
343
- console.log(` Animation: .${cls}`);
344
- console.log(` Config: ${JSON.stringify(config.animations[cls], null, 4)}`);
672
+ if (!fs.existsSync(defaultsPath)) {
673
+ console.error('❌ packages/core/defaults.js not found')
674
+ process.exit(1)
675
+ }
676
+
677
+ const { DEFAULT_TOKENS, DEFAULT_PATTERNS, DEFAULT_ANIMATIONS, DEFAULT_RULES } =
678
+ await import(pathToFileURL(defaultsPath).href + `?t=${Date.now()}`)
679
+
680
+ // ── 1. Sync patterns into pattern-expander.js ──
681
+ const peFile = path.resolve(coreDir, 'pattern-expander.js')
682
+ let peSrc = fs.readFileSync(peFile, 'utf8')
683
+
684
+ const patternsJSON = JSON.stringify(DEFAULT_PATTERNS, null, 4)
685
+ .replace(/"([^"]+)":/g, "'$1':") // use single quotes for keys
686
+ .replace(/"/g, "'") // use single quotes for values
687
+
688
+ const peMarkerStart = '// <<<DEFAULTS:PATTERNS:START>>>'
689
+ const peMarkerEnd = '// <<<DEFAULTS:PATTERNS:END>>>'
690
+
691
+ if (peSrc.includes(peMarkerStart)) {
692
+ // Replace existing block
693
+ const re = new RegExp(`${peMarkerStart}[\\s\\S]*?${peMarkerEnd}`)
694
+ peSrc = peSrc.replace(re,
695
+ `${peMarkerStart}\n ...${patternsJSON},\n ${peMarkerEnd}`)
696
+ } else {
697
+ // Inject into constructor — find: this.patterns = {
698
+ peSrc = peSrc.replace(
699
+ 'this.patterns = {',
700
+ `this.patterns = {\n ${peMarkerStart}\n ...${patternsJSON},\n ${peMarkerEnd}`
701
+ )
702
+ }
703
+ fs.writeFileSync(peFile, peSrc)
704
+ console.log(`✅ Patterns synced → pattern-expander.js (${Object.keys(DEFAULT_PATTERNS).length} patterns)`)
705
+
706
+ // ── 2. Sync tokens into parser.js ──
707
+ const parserFile = path.resolve(coreDir, 'parser.js')
708
+ let parserSrc = fs.readFileSync(parserFile, 'utf8')
709
+
710
+ const tokensJSON = JSON.stringify(DEFAULT_TOKENS, null, 4)
711
+
712
+ const tokMarkerStart = '// <<<DEFAULTS:TOKENS:START>>>'
713
+ const tokMarkerEnd = '// <<<DEFAULTS:TOKENS:END>>>'
714
+
715
+ if (parserSrc.includes(tokMarkerStart)) {
716
+ const re = new RegExp(`${tokMarkerStart}[\\s\\S]*?${tokMarkerEnd}`)
717
+ parserSrc = parserSrc.replace(re,
718
+ `${tokMarkerStart}\n const DEFAULT_TOKENS = ${tokensJSON}\n c = this._mergeDeep(DEFAULT_TOKENS, c)\n ${tokMarkerEnd}`)
719
+ } else {
720
+ // Inject at start of parse() method, after: const c = this.config
721
+ parserSrc = parserSrc.replace(
722
+ 'parse() {\n const vars = {}\n const c = this.config',
723
+ `parse() {\n const vars = {}\n let c = this.config\n ${tokMarkerStart}\n const DEFAULT_TOKENS = ${tokensJSON}\n c = this._mergeDeep(DEFAULT_TOKENS, c)\n ${tokMarkerEnd}`
724
+ )
725
+
726
+ // Add _mergeDeep helper if not present
727
+ if (!parserSrc.includes('_mergeDeep')) {
728
+ parserSrc = parserSrc.replace(
729
+ 'export class TokenParser {',
730
+ `export class TokenParser {
731
+ _mergeDeep(target, source) {
732
+ const out = { ...target }
733
+ for (const key of Object.keys(source || {})) {
734
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
735
+ out[key] = this._mergeDeep(target[key] || {}, source[key])
736
+ } else {
737
+ out[key] = source[key]
345
738
  }
346
- // Check if it's a utility
347
- else {
348
- console.log(` Utility: .${cls}`);
349
- console.log(` (Built-in utility class)`);
739
+ }
740
+ return out
741
+ }
742
+ `
743
+ )
350
744
  }
351
- console.log('');
352
745
  }
746
+ fs.writeFileSync(parserFile, parserSrc)
747
+ console.log(`✅ Tokens synced → parser.js`)
748
+
749
+ // ── 3. Sync animations into core/index.js ──
750
+ const indexFile = path.resolve(coreDir, 'index.js')
751
+ let indexSrc = fs.readFileSync(indexFile, 'utf8')
752
+
753
+ const animJSON = JSON.stringify(DEFAULT_ANIMATIONS, null, 4)
754
+ const animMarkerStart = '// <<<DEFAULTS:ANIMATIONS:START>>>'
755
+ const animMarkerEnd = '// <<<DEFAULTS:ANIMATIONS:END>>>'
756
+
757
+ if (indexSrc.includes(animMarkerStart)) {
758
+ const re = new RegExp(`${animMarkerStart}[\\s\\S]*?${animMarkerEnd}`)
759
+ indexSrc = indexSrc.replace(re,
760
+ `${animMarkerStart}\n const DEFAULT_ANIMATIONS = ${animJSON}\n ${animMarkerEnd}`)
761
+ } else {
762
+ // Find AnimationEngine constructor call and inject defaults merge
763
+ indexSrc = indexSrc.replace(
764
+ 'this.animationEngine = new AnimationEngine(',
765
+ `${animMarkerStart}\n const DEFAULT_ANIMATIONS = ${animJSON}\n ${animMarkerEnd}\n this.animationEngine = new AnimationEngine(`
766
+ )
767
+ // Now fix the actual merge — find the animations arg
768
+ indexSrc = indexSrc.replace(
769
+ 'new AnimationEngine(\n config.animations',
770
+ 'new AnimationEngine(\n { ...DEFAULT_ANIMATIONS, ...config.animations }'
771
+ )
772
+ // Handle single-line version too
773
+ indexSrc = indexSrc.replace(
774
+ /new AnimationEngine\(\s*config\.animations\s*,/g,
775
+ 'new AnimationEngine({ ...DEFAULT_ANIMATIONS, ...config.animations },'
776
+ )
777
+ }
778
+ fs.writeFileSync(indexFile, indexSrc)
779
+ console.log(`✅ Animations synced → index.js (${Object.keys(DEFAULT_ANIMATIONS).length} animations)`)
780
+
781
+ console.log('\n✅ Sync complete — rebuild to apply: npx mizumi build')
353
782
  },
354
-
355
- watch() {
356
- watch();
783
+
784
+
785
+ // ── mizumi add:pattern <name> "<utilities>" ──
786
+ async ['add:pattern']() {
787
+ const name = args[1]
788
+ const value = args[2]
789
+
790
+ if (!name || !value) {
791
+ console.log('Usage: node cli/index.js add:pattern <name> "<utilities>"')
792
+ console.log('Example: node cli/index.js add:pattern flex-center "display:flex align-x:center align-yi:center"')
793
+ return
794
+ }
795
+
796
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
797
+ const defaultsPath = path.resolve(__dirname, '../core/defaults.js')
798
+
799
+ if (!fs.existsSync(defaultsPath)) {
800
+ console.error('❌ packages/core/defaults.js not found')
801
+ process.exit(1)
802
+ }
803
+
804
+ let src = fs.readFileSync(defaultsPath, 'utf8')
805
+
806
+ // Find the closing of DEFAULT_PATTERNS and inject before it
807
+ const insertBefore = '\n}\n\nexport const DEFAULT_ANIMATIONS'
808
+ if (!src.includes(insertBefore)) {
809
+ console.error('❌ Could not find insertion point in defaults.js')
810
+ process.exit(1)
811
+ }
812
+
813
+ const line = ` '${name}': '${value}',`
814
+
815
+ // Check if already exists
816
+ if (src.includes(`'${name}':`)) {
817
+ // Update existing
818
+ src = src.replace(
819
+ new RegExp(`'${name}':\\s*'[^']*'`),
820
+ `'${name}': '${value}'`
821
+ )
822
+ console.log(`✏️ Updated pattern: ${name}`)
823
+ } else {
824
+ // Insert before closing brace of DEFAULT_PATTERNS
825
+ src = src.replace(insertBefore, `\n${line}${insertBefore}`)
826
+ console.log(`✅ Added pattern: ${name}`)
827
+ }
828
+
829
+ fs.writeFileSync(defaultsPath, src)
830
+ console.log(` Run sync:defaults to apply → node packages/cli/index.js sync:defaults`)
357
831
  },
358
832
 
359
- /**
360
- * Show help
361
- */
362
- help() {
363
- console.log(`
364
- 🌊 Mizumi CSS Framework v0.1.0
365
-
366
- Commands:
367
- init Initialize Mizumi in project
368
- build Generate CSS + runtime JS
369
- watch Watch config and auto-rebuild
370
- docs Generate documentation page
371
- list List all patterns & animations
372
- explain <class> Explain what a class does
373
- help Show help
374
-
375
- Examples:
376
- node mizumi init
377
- node mizumi build
378
- node mizumi watch
379
- node mizumi list
380
- node mizumi explain "card animate-fade-in"
381
- `);
382
- },
383
-
384
-
385
-
386
- docs: async function() {
387
- console.log('🌊 Generating Mizumi Docs...\n');
388
- const cfgPath = path.join(process.cwd(), 'mizumi.config.js');
389
- if (!fs.existsSync(cfgPath)) {
390
- console.error('❌ mizumi.config.js not found!');
391
- process.exit(1);
833
+
834
+ // ── mizumi add:token <category> <name> <value> ──
835
+ async ['add:token']() {
836
+ const category = args[1]
837
+ const name = args[2]
838
+ const value = args[3]
839
+
840
+ if (!category || !name || !value) {
841
+ console.log('Usage: node cli/index.js add:token <category> <name> <value>')
842
+ console.log('Example: node cli/index.js add:token colors brand "#ff6b6b"')
843
+ console.log('Example: node cli/index.js add:token spacing hero "120px"')
844
+ return
845
+ }
846
+
847
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
848
+ const defaultsPath = path.resolve(__dirname, '../core/defaults.js')
849
+
850
+ if (!fs.existsSync(defaultsPath)) {
851
+ console.error('❌ packages/core/defaults.js not found')
852
+ process.exit(1)
853
+ }
854
+
855
+ let src = fs.readFileSync(defaultsPath, 'utf8')
856
+
857
+ // Check if already exists
858
+ if (src.includes(`${name}:`) ) {
859
+ src = src.replace(
860
+ new RegExp(`(${name}:\\s*)(['"\`][^'"\`]*['"\`]|\\d+)`),
861
+ `$1'${value}'`
862
+ )
863
+ console.log(`✏️ Updated token: ${category}.${name} = ${value}`)
864
+ } else {
865
+ // Find the category block and inject
866
+ const catMarker = ` ${category}: {`
867
+ if (!src.includes(catMarker)) {
868
+ console.error(`❌ Token category "${category}" not found in defaults.js`)
869
+ console.log(` Valid categories: colors, spacing, typography, fonts, radius, shadows, easing, duration, blur, opacity, zIndex, leading, tracking`)
870
+ process.exit(1)
871
+ }
872
+ src = src.replace(catMarker, `${catMarker}\n ${name}: '${value}',`)
873
+ console.log(`✅ Added token: ${category}.${name} = ${value}`)
392
874
  }
393
-
394
- const configModule = await import(pathToFileURL(cfgPath).href);
395
- const config = configModule.default || configModule;
396
-
397
- const generator = new DocsGenerator(config);
398
- const html = generator.generate();
399
- const outDir = path.join(process.cwd(), '.mizumi');
400
- const outPath = path.join(outDir, 'docs.html');
401
- if (!fs.existsSync(outDir)) fs.mkdirSync(outDir);
402
- fs.writeFileSync(outPath, html);
403
- console.log(`✅ Docs generated: ${outPath}`);
404
- console.log('\n🎉 Open in browser:');
405
- console.log(` .mizumi/docs.html`);
875
+
876
+ fs.writeFileSync(defaultsPath, src)
877
+ console.log(` Run sync:defaults to apply → node packages/cli/index.js sync:defaults`)
406
878
  },
407
879
 
408
- };
880
+
881
+ // ── mizumi add:animation <name> ──
882
+ // Opens a prompt-style flow (just prints the template for now)
883
+ async ['add:animation']() {
884
+ const name = args[1]
885
+
886
+ if (!name) {
887
+ console.log('Usage: node cli/index.js add:animation <name>')
888
+ console.log('Example: node cli/index.js add:animation hover-glow')
889
+ return
890
+ }
891
+
892
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
893
+ const defaultsPath = path.resolve(__dirname, '../core/defaults.js')
894
+
895
+ if (!fs.existsSync(defaultsPath)) {
896
+ console.error('❌ packages/core/defaults.js not found')
897
+ process.exit(1)
898
+ }
899
+
900
+ let src = fs.readFileSync(defaultsPath, 'utf8')
901
+
902
+ if (src.includes(`'${name}':`)) {
903
+ console.log(`⚠️ Animation "${name}" already exists in defaults.js`)
904
+ console.log(' Edit it directly in packages/core/defaults.js')
905
+ return
906
+ }
907
+
908
+ // Detect type from name
909
+ let template
910
+ if (name.startsWith('hover-')) {
911
+ template = ` '${name}': {\n hover: { y: -6, duration: 0.15, ease: 'power2.out' },\n },`
912
+ } else if (name.startsWith('active-')) {
913
+ template = ` '${name}': {\n active: { scale: 0.95, duration: 0.1 },\n },`
914
+ } else if (name.startsWith('scroll-')) {
915
+ template = ` '${name}': {\n from: { opacity: 0, y: 30 },\n to: { opacity: 1, y: 0 },\n duration: 0.6,\n ease: 'power2.out',\n scrollTrigger: { trigger: 'self', start: 'top 85%' },\n },`
916
+ } else if (name.startsWith('stagger-')) {
917
+ template = ` '${name}': {\n targets: 'children',\n stagger: 0.1,\n from: { opacity: 0, y: 20 },\n to: { opacity: 1, y: 0 },\n },`
918
+ } else {
919
+ template = ` '${name}': {\n from: { opacity: 0, y: 40 },\n to: { opacity: 1, y: 0 },\n duration: 0.3,\n ease: 'power2.out',\n },`
920
+ }
921
+
922
+ // Inject before closing of DEFAULT_ANIMATIONS
923
+ const insertBefore = '\n}\n\nexport const DEFAULT_RULES'
924
+ src = src.replace(insertBefore, `\n${template}${insertBefore}`)
925
+
926
+ fs.writeFileSync(defaultsPath, src)
927
+ console.log(`✅ Added animation: ${name}`)
928
+ console.log(` Edit the config in packages/core/defaults.js if needed`)
929
+ console.log(` Run sync:defaults to apply → node packages/cli/index.js sync:defaults`)
930
+ },
931
+
932
+
933
+
934
+ }
409
935
 
410
936
  // Run command
411
- if (commands[command]) {
412
- await commands[command](); // top-level await is allowed in ESM
413
- } else {
414
- console.error(`❌ Unknown command: ${command}`);
415
- commands.help();
937
+ if (!command || command === 'help') {
938
+ commands.help()
939
+ } else if (commands[command]) {
940
+ Promise.resolve(commands[command]()).catch(err => {
941
+ console.error('❌', err.message)
942
+ process.exit(1)
943
+ })
944
+ } else if (command === 'sync:defaults') {
945
+ Promise.resolve(commands['sync:defaults']()).catch(err => { console.error('❌', err.message); process.exit(1) })
946
+ } else if (command === 'add:pattern') {
947
+ Promise.resolve(commands['add:pattern']()).catch(err => { console.error('❌', err.message); process.exit(1) })
948
+ } else if (command === 'add:token') {
949
+ Promise.resolve(commands['add:token']()).catch(err => { console.error('❌', err.message); process.exit(1) })
950
+ } else if (command === 'add:animation') {
951
+ Promise.resolve(commands['add:animation']()).catch(err => { console.error('❌', err.message); process.exit(1) })
416
952
  }
953
+ else {
954
+ console.error(`❌ Unknown command: ${command}`)
955
+ console.log('Run: npx mizumi help')
956
+ process.exit(1)
957
+ }