@utopia-studio-design/design-system-cli 0.1.0 → 0.3.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.
Files changed (52) hide show
  1. package/README.md +12 -41
  2. package/bin/utopia-ds-mcp.mjs +55 -0
  3. package/bin/utopia-ds.mjs +233 -318
  4. package/data/docs/ai-platform-plan.md +53 -0
  5. package/data/docs/arabic-friendly.md +72 -0
  6. package/data/docs/foundations.md +154 -0
  7. package/data/docs/guide.md +37 -0
  8. package/data/docs/quick-start-ai.md +122 -0
  9. package/data/docs/shadcn-conversion.md +35 -0
  10. package/data/docs/theme-authoring.md +46 -0
  11. package/data/manifests/catalog.json +547 -0
  12. package/data/manifests/components.json +4780 -0
  13. package/data/manifests/patterns.json +12 -0
  14. package/data/manifests/templates.json +96 -0
  15. package/data/manifests/theme-barrier-intelligence.json +123 -0
  16. package/data/manifests/theme-dextrum.json +255 -0
  17. package/data/manifests/theme-utopia-default.json +163 -0
  18. package/data/manifests/themes.json +417 -0
  19. package/data/templates/saas-solution-homepage/README.md +52 -0
  20. package/data/templates/saas-solution-homepage/agents/index.html +1 -0
  21. package/data/templates/saas-solution-homepage/changelog/index.html +1 -0
  22. package/data/templates/saas-solution-homepage/contact-sales/index.html +1 -0
  23. package/data/templates/saas-solution-homepage/customers/aster-labs/index.html +1 -0
  24. package/data/templates/saas-solution-homepage/customers/index.html +1 -0
  25. package/data/templates/saas-solution-homepage/index.html +17 -0
  26. package/data/templates/saas-solution-homepage/integrations/index.html +1 -0
  27. package/data/templates/saas-solution-homepage/integrations/slack/index.html +1 -0
  28. package/data/templates/saas-solution-homepage/main.tsx +642 -0
  29. package/data/templates/saas-solution-homepage/pricing/index.html +1 -0
  30. package/data/templates/saas-solution-homepage/product/index.html +1 -0
  31. package/data/templates/saas-solution-homepage/styles.css +863 -0
  32. package/data/templates/saas-solution-homepage/template.manifest.json +36 -0
  33. package/lib/api.mjs +152 -0
  34. package/package.json +17 -15
  35. package/LICENSE +0 -21
  36. package/templates/agent-docs/examples.md +0 -33
  37. package/templates/agent-docs/index.md +0 -78
  38. package/templates/agent-docs/llms.txt +0 -33
  39. package/templates/components/badge.tsx +0 -12
  40. package/templates/components/brand-action.tsx +0 -32
  41. package/templates/components/button-group.tsx +0 -30
  42. package/templates/components/button.tsx +0 -44
  43. package/templates/components/card.tsx +0 -23
  44. package/templates/components/cta-section.tsx +0 -34
  45. package/templates/components/icon-button.tsx +0 -44
  46. package/templates/components/page-hero.tsx +0 -47
  47. package/templates/components/tooltip.tsx +0 -48
  48. package/templates/components/topbar.tsx +0 -59
  49. package/templates/lib/utils.ts +0 -6
  50. package/templates/styles/styles.css +0 -271
  51. package/templates/styles/tokens.css +0 -21
  52. package/templates/tokens.json +0 -41
package/bin/utopia-ds.mjs CHANGED
@@ -1,353 +1,268 @@
1
1
  #!/usr/bin/env node
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
3
3
  import { dirname, join, resolve } from 'node:path'
4
- import { fileURLToPath } from 'node:url'
4
+ import { spawn } from 'node:child_process'
5
+ import {
6
+ capabilityManifest, envelope, getComponent, getDoc, getTemplate, getTheme,
7
+ listComponents, listDocs, listTemplates, listThemes, mcpLaunch, repositoryDoctor, search,
8
+ } from '../lib/api.mjs'
5
9
 
6
- const cliRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
7
- const templateRoot = join(cliRoot, 'templates')
8
10
  const args = process.argv.slice(2)
9
-
10
- function argValue(name, fallback) {
11
- const index = args.indexOf(name)
12
- return index >= 0 && args[index + 1] ? args[index + 1] : fallback
11
+ const command = args.find((arg) => !arg.startsWith('--')) ?? 'help'
12
+ const commandIndex = args.indexOf(command)
13
+ const values = commandIndex >= 0 ? args.slice(commandIndex + 1).filter((arg) => !arg.startsWith('--')) : []
14
+ const json = args.includes('--json')
15
+ const dense = args.includes('--dense')
16
+
17
+ function output(type, data, format = null) {
18
+ if (json) return console.log(JSON.stringify(envelope(type, data), null, 2))
19
+ if (format) return console.log(format(data, dense))
20
+ console.log(typeof data === 'string' ? data : JSON.stringify(data, null, 2))
13
21
  }
14
22
 
15
- const cwd = resolve(argValue('--cwd', process.cwd()))
16
- const command = args.find((arg) => !arg.startsWith('--')) ?? '--help'
17
-
18
- function ensureDir(path) {
19
- mkdirSync(path, { recursive: true })
20
- }
21
-
22
- function copyTemplate(from, to) {
23
- ensureDir(dirname(to))
24
- copyFileSync(join(templateRoot, from), to)
25
- console.log(`created ${to}`)
23
+ function fail(message, code = 'ERR_INPUT', suggestions = []) {
24
+ if (json) console.error(JSON.stringify({ apiVersion: 1, error: message, code, suggestions }, null, 2))
25
+ else console.error(message)
26
+ process.exitCode = 1
26
27
  }
27
28
 
28
- function readTemplate(path) {
29
- return readFileSync(join(templateRoot, path), 'utf8')
29
+ function argAfter(flag, fallback) {
30
+ const index = args.indexOf(flag)
31
+ return index >= 0 ? args[index + 1] : fallback
30
32
  }
31
33
 
32
34
  function help() {
33
- console.log(`Utopia Design System CLI
34
-
35
- Usage:
36
- utopia-ds init [--cwd path]
37
- utopia-ds add <component> [--cwd path]
38
- utopia-ds tokens
39
- utopia-ds agent-docs [--cwd path]
40
- utopia-ds doctor [--cwd path]
41
-
42
- Components:
43
- button, icon-button, button-group, card, badge, tooltip, page-hero,
44
- cta-section, brand-action, topbar
45
-
46
- Agent contract:
47
- Components should define useWhen, avoidWhen, fallbackToShadcn,
48
- requiredTokens, neverInvent, and validationChecklist.
49
-
50
- Themes:
51
- Utopia is the default/reference theme. Themeable apps/templates
52
- should map all visual decisions through semantic roles.
35
+ output('help', capabilityManifest(), () => `Ceramic Design System CLI
36
+
37
+ Usage: utopia-ds <command> [options]
38
+
39
+ Commands:
40
+ init [directory] [--theme <id>] [--yes] Add Ceramic and agent instructions
41
+ search <query> Search every design-system domain
42
+ component <Name>|--list Inspect component contracts
43
+ template <id>|--list [--skeleton] Inspect starter structures
44
+ template <id> --copy <directory> [--theme <id>]
45
+ Generate a themed runnable project
46
+ theme <id>|--list Inspect theme contracts
47
+ theme create <id> [directory] Scaffold and register a theme
48
+ docs <topic>|--list Read design-system guidance
49
+ manifest Print the self-describing CLI contract
50
+ doctor Validate the design-system source
51
+ mcp Start the stdio MCP server
52
+
53
+ Global options: --json, --dense
53
54
  `)
54
55
  }
55
56
 
56
- function init() {
57
- copyTemplate('styles/tokens.css', join(cwd, 'src/styles/utopia-tokens.css'))
58
- copyTemplate('styles/styles.css', join(cwd, 'src/styles/utopia-styles.css'))
59
- copyTemplate('lib/utils.ts', join(cwd, 'src/lib/utils.ts'))
60
- console.log('Add `import "./styles/utopia-styles.css"` to your app entrypoint.')
61
- }
62
-
63
- function add() {
64
- const component = args[1]
65
- const componentMap = {
66
- button: 'button.tsx',
67
- 'icon-button': 'icon-button.tsx',
68
- 'button-group': 'button-group.tsx',
69
- card: 'card.tsx',
70
- badge: 'badge.tsx',
71
- tooltip: 'tooltip.tsx',
72
- 'page-hero': 'page-hero.tsx',
73
- 'cta-section': 'cta-section.tsx',
74
- 'brand-action': 'brand-action.tsx',
75
- topbar: 'topbar.tsx',
76
- }
77
- const componentDependencies = {
78
- 'icon-button': ['button', 'tooltip'],
79
- 'button-group': ['button'],
80
- 'page-hero': ['brand-action'],
81
- 'cta-section': ['brand-action'],
82
- topbar: ['button'],
83
- }
84
-
85
- if (!component || !componentMap[component]) {
86
- throw new Error(`Unknown component "${component ?? ''}". Run utopia-ds --help for available components.`)
87
- }
88
-
89
- const componentsToCopy = new Set()
90
- const queue = [component]
91
-
92
- while (queue.length) {
93
- const next = queue.shift()
94
-
95
- for (const dependency of componentDependencies[next] ?? []) {
96
- if (!componentsToCopy.has(dependency)) queue.push(dependency)
97
- }
98
-
99
- componentsToCopy.add(next)
100
- }
101
-
102
- for (const componentName of componentsToCopy) {
103
- copyTemplate(`components/${componentMap[componentName]}`, join(cwd, `src/components/utopia/${componentMap[componentName]}`))
104
- }
105
-
106
- copyTemplate('lib/utils.ts', join(cwd, 'src/lib/utils.ts'))
57
+ function formatComponent(entry) {
58
+ return [
59
+ `name=${entry.name}`, `status=${entry.status}`, `category=${entry.category}`,
60
+ `import=${entry.packageImport}`, `sourcePath=${entry.sourcePath}`,
61
+ `shadcn=${entry.shadcnFoundation.join(',')}`, `fallbackToShadcn=${entry.fallbackToShadcn}`,
62
+ `tokens=${entry.requiredTokens.join(',')}`, `useWhen=${entry.useWhen.join(' | ')}`,
63
+ `avoidWhen=${entry.avoidWhen.join(' | ')}`, `neverInvent=${entry.neverInvent.join(',')}`,
64
+ `ai=${JSON.stringify(entry.ai ?? {})}`,
65
+ ].join('\n')
107
66
  }
108
67
 
109
- function tokens() {
110
- process.stdout.write(readTemplate('tokens.json'))
68
+ function write(path, content) {
69
+ mkdirSync(dirname(path), { recursive: true })
70
+ writeFileSync(path, content, 'utf8')
111
71
  }
112
72
 
113
- function agentDocs() {
114
- copyTemplate('agent-docs/index.md', join(cwd, 'docs/design-system/agent-docs/index.md'))
115
- copyTemplate('agent-docs/examples.md', join(cwd, 'docs/design-system/examples.md'))
116
- copyTemplate('agent-docs/llms.txt', join(cwd, 'public/llms.txt'))
73
+ function titleFromId(id) {
74
+ return id.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ')
117
75
  }
118
76
 
119
- function doctor() {
120
- const packageJsonPath = join(cwd, 'package.json')
121
- const packageJson = existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : {}
122
- const deps = { ...(packageJson.dependencies ?? {}), ...(packageJson.devDependencies ?? {}) }
123
- const warnings = []
124
-
125
- for (const dep of ['react', 'class-variance-authority', 'radix-ui']) {
126
- if (!deps[dep]) warnings.push(`Missing dependency: ${dep}`)
77
+ function scaffoldTheme() {
78
+ const id = values[1]
79
+ const root = resolve(values[2] ?? process.cwd())
80
+ if (!id || !/^[a-z][a-z0-9-]*$/.test(id)) return fail('Theme create requires a lowercase kebab-case id.', 'ERR_THEME_ID')
81
+
82
+ const themesPath = join(root, 'packages/design-system/src/manifests/themes.json')
83
+ const themeDirectory = join(root, 'packages/design-system/src/themes')
84
+ const manifestDirectory = join(root, 'packages/design-system/src/manifests')
85
+ if (!existsSync(themesPath)) return fail(`No Ceramic theme workspace found in ${root}.`, 'ERR_THEME_WORKSPACE')
86
+
87
+ const themes = JSON.parse(readFileSync(themesPath, 'utf8'))
88
+ const existingThemeIndex = themes.themes.findIndex((theme) => theme.id === id)
89
+ if (existingThemeIndex >= 0 && !args.includes('--force')) return fail(`Theme "${id}" already exists. Pass --force to replace its scaffold.`, 'ERR_THEME_EXISTS')
90
+
91
+ const name = titleFromId(id)
92
+ const cssPath = join(themeDirectory, `${id}.css`)
93
+ const policyPath = join(manifestDirectory, `theme-${id}.json`)
94
+ if ((existsSync(cssPath) || existsSync(policyPath)) && !args.includes('--force')) {
95
+ return fail(`Theme files for "${id}" already exist. Pass --force to overwrite them.`, 'ERR_TARGET_EXISTS')
127
96
  }
128
97
 
129
- const candidateCssFiles = [
130
- join(cwd, 'src/index.css'),
131
- join(cwd, 'src/app/globals.css'),
132
- join(cwd, 'src/styles/utopia-tokens.css'),
133
- ]
134
- const css = candidateCssFiles.filter(existsSync).map((file) => readFileSync(file, 'utf8')).join('\n')
135
- for (const token of ['--background', '--primary', '--font-sans', '--radius']) {
136
- if (!css.includes(token)) warnings.push(`Missing CSS token: ${token}`)
98
+ const themeValues = {
99
+ '--background': '#15171C', '--foreground': '#F7F7F5', '--card': '#1D2027', '--card-foreground': '#F7F7F5',
100
+ '--primary': '#6F5CFF', '--primary-foreground': '#FFFFFF', '--secondary': '#292D37', '--secondary-foreground': '#F7F7F5',
101
+ '--muted': '#222630', '--muted-foreground': '#A8ADB8', '--border': 'rgba(247,247,245,0.16)', '--input': 'rgba(247,247,245,0.24)',
102
+ '--ring': '#6F5CFF', '--radius': '8px', '--radius-control': '8px', '--radius-surface': '12px', '--radius-chat-bubble': '18px',
103
+ '--radius-chat-composer': '24px', '--radius-chat-token': '14px', '--shadow-control': 'none', '--motion-duration-press': '160ms',
104
+ '--motion-duration-page': '200ms', '--motion-duration-expand': '320ms', '--motion-duration-reveal': '520ms', '--motion-duration-icon': '640ms',
105
+ '--motion-ease-standard': 'cubic-bezier(0.2, 0, 0, 1)', '--motion-ease-emphasized': 'cubic-bezier(0.16, 1, 0.3, 1)',
106
+ '--motion-ease-icon': 'cubic-bezier(0.16, 1, 0.3, 1)', '--motion-press-scale': '0.97', '--motion-distance-page': '8px',
107
+ '--motion-distance-reveal': '8px', '--font-sans': 'Inter', '--font-display': 'Inter', '--font-ui-support': 'Inter',
108
+ '--font-marketing-display': 'Inter', '--font-arabic': 'IBM Plex Sans Arabic', '--font-arabic-body': 'IBM Plex Sans Arabic',
109
+ '--font-arabic-display': 'IBM Plex Sans Arabic', '--font-weight-arabic-body': '400', '--font-weight-arabic-display': '700',
110
+ '--font-size-display': 'clamp(2.75rem, 7vw, 5.5rem)', '--font-size-arabic-body': '16px', '--font-size-arabic-body-lg': '18px',
111
+ '--font-size-arabic-display': 'clamp(2.625rem, 6.6vw, 5.25rem)', '--line-height-arabic': '1.75', '--line-height-arabic-body': '1.75',
112
+ '--line-height-arabic-display': '1.14', '--tracking-arabic': '0', '--tracking-arabic-display': '0', '--sidebar-width': '17rem',
113
+ '--control-height-sm': '2rem', '--sidebar-width-collapsed': '4rem', '--sidebar-min-block-size': '30rem', '--sidebar-rail-size': '8px',
114
+ '--button-height': '40px', '--card-padding': '24px', '--overlay': 'rgba(21,23,28,0.86)', '--modal-surface': '#1D2027', '--popover-surface': '#1D2027',
137
115
  }
138
-
139
- if (css.includes('border-radius: 16px') || css.includes('--radius: 16px')) {
140
- warnings.push('Found 16px radius. Utopia DS expects square geometry.')
116
+ const declarations = Object.entries(themeValues).map(([token, value]) => ` ${token}: ${value};`).join('\n')
117
+ const css = `[data-theme="${id}"] {\n color-scheme: dark;\n${declarations}\n}\n\n[data-theme="${id}"][data-color-mode="light"] {\n color-scheme: light;\n --background: #F7F7F5;\n --foreground: #15171C;\n --card: #FFFFFF;\n --card-foreground: #15171C;\n --secondary: #ECECF0;\n --secondary-foreground: #15171C;\n --muted: #E5E6EA;\n --muted-foreground: #5F6470;\n --border: rgb(21 23 28 / 16%);\n --input: rgb(21 23 28 / 24%);\n --surface: rgb(21 23 28 / 4.5%);\n --surface-strong: rgb(21 23 28 / 8%);\n --surface-elevated: #FFFFFF;\n --overlay: rgb(247 247 245 / 86%);\n --modal-surface: #FFFFFF;\n --popover-surface: #FFFFFF;\n}\n\n[data-theme="${id}"] :where(:lang(ar), [lang|="ar"]) {\n font-family: var(--font-arabic-body);\n line-height: var(--line-height-arabic-body);\n letter-spacing: var(--tracking-arabic);\n text-transform: none;\n}\n`
118
+ const policy = {
119
+ '$schema': 'https://utopia-studio.co/design-system/theme.schema.json', id, name, type: 'theme-policy', locked: false,
120
+ summary: `${name} brand theme mapped to the Ceramic semantic contract.`,
121
+ sourceFiles: [`packages/design-system/src/themes/${id}.css`, `packages/design-system/src/manifests/theme-${id}.json`],
122
+ brandPrimitives: { colors: { background: themeValues['--background'], foreground: themeValues['--foreground'], accent: themeValues['--primary'] }, typography: { latin: 'Inter', arabic: 'IBM Plex Sans Arabic' }, geometry: { controlRadius: themeValues['--radius-control'], surfaceRadius: themeValues['--radius-surface'] } },
123
+ semanticMappings: Object.fromEntries(Object.entries(themeValues).map(([token, value]) => [token, value])),
124
+ visualPolicy: { tone: ['clear', 'intentional', 'product-focused'], allow: ['semantic color roles', 'logical layout properties', 'theme-owned brand expression'], avoid: ['Utopia brand primitives', 'component-specific color literals', 'left/right-only layout APIs'] },
125
+ arabicFriendly: { direction: 'Support dir="rtl" with logical CSS properties.', typography: 'Use the declared Arabic family without Latin tracking or casing.' },
126
+ translations: { ar: { summary: `ثيم ${name} مبني على عقد Ceramic الدلالي.`, iconPolicy: { description: 'استخدم أيقونات واضحة ومحايدة الاتجاه، واعكس الأيقونات الاتجاهية في RTL.', allow: ['أيقونات Lucide', 'أيقونات محايدة الاتجاه', 'انعكاس الأيقونات الاتجاهية في RTL'] } } },
141
127
  }
142
-
143
- const manifestCandidates = [
144
- join(cwd, 'packages/design-system/src/manifests/components.json'),
145
- join(cwd, 'src/manifests/components.json'),
146
- join(cwd, 'node_modules/@utopia-studio-design/design-system/components.json'),
147
- ]
148
- const manifestPath = manifestCandidates.find(existsSync)
149
- const requiredAgentContractFields = [
150
- 'useWhen',
151
- 'avoidWhen',
152
- 'fallbackToShadcn',
153
- 'requiredTokens',
154
- 'neverInvent',
155
- 'validationChecklist',
156
- ]
157
-
158
- if (!manifestPath) {
159
- warnings.push('Missing components manifest with agent contracts.')
160
- } else {
161
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
162
- const components = Array.isArray(manifest.components) ? manifest.components : []
163
-
164
- if (!components.length) {
165
- warnings.push('Components manifest has no components.')
166
- }
167
-
168
- for (const component of components) {
169
- for (const field of ['props', 'anatomy', 'states', 'slots', 'examples', 'aiRules']) {
170
- if (!Array.isArray(component[field]) || component[field].length === 0) {
171
- warnings.push(`Incomplete component ${field}: ${component.name ?? 'unknown component'}`)
172
- }
173
- }
174
-
175
- const contract = component.agentContract
176
- if (!contract) {
177
- warnings.push(`Missing agentContract: ${component.name ?? 'unknown component'}`)
178
- continue
179
- }
180
-
181
- for (const field of requiredAgentContractFields) {
182
- if (!Array.isArray(contract[field]) || contract[field].length === 0) {
183
- warnings.push(`Incomplete agentContract.${field}: ${component.name ?? 'unknown component'}`)
184
- }
185
- }
186
- }
128
+ const entry = {
129
+ id, name, shortName: name, locked: false, role: 'brand theme', description: policy.summary,
130
+ bestFor: [`${name} product interfaces`], principles: policy.visualPolicy.tone,
131
+ translations: { ar: { name, role: 'ثيم علامة', description: `ثيم ${name} مبني على عقد Ceramic الدلالي.`, bestFor: [`واجهات ${name}`], principles: ['وضوح', 'اتساق', 'دعم العربية'] } },
132
+ policyManifest: `packages/design-system/src/manifests/theme-${id}.json`, css: `packages/design-system/src/themes/${id}.css`, iconSystem: 'lucide', values: themeValues,
187
133
  }
188
134
 
189
- const themeManifestCandidates = [
190
- join(cwd, 'packages/design-system/src/manifests/themes.json'),
191
- join(cwd, 'src/manifests/themes.json'),
192
- join(cwd, 'node_modules/@utopia-studio-design/design-system/themes.json'),
193
- ]
194
- const themeManifestPath = themeManifestCandidates.find(existsSync)
195
- const requiredThemeRoles = [
196
- 'background',
197
- 'foreground',
198
- 'card',
199
- 'cardForeground',
200
- 'primary',
201
- 'primaryForeground',
202
- 'secondary',
203
- 'secondaryForeground',
204
- 'muted',
205
- 'mutedForeground',
206
- 'border',
207
- 'input',
208
- 'ring',
209
- 'radius',
210
- ]
211
-
212
- if (!themeManifestPath) {
213
- warnings.push('Missing themes manifest.')
214
- } else {
215
- const themeManifest = JSON.parse(readFileSync(themeManifestPath, 'utf8'))
216
- const defaultTheme = themeManifest.defaultTheme
217
- const themes = Array.isArray(themeManifest.themes) ? themeManifest.themes : []
218
- const selectedTheme = themes.find((theme) => theme.id === defaultTheme)
219
- const semanticRoles = Array.isArray(themeManifest.semanticRoles) ? themeManifest.semanticRoles : []
220
- const roleNames = new Set(semanticRoles.map((role) => role.name))
221
-
222
- if (!defaultTheme) warnings.push('themes.json missing defaultTheme.')
223
- if (!selectedTheme) warnings.push(`themes.json missing default theme entry: ${defaultTheme ?? 'unknown'}`)
224
-
225
- for (const role of requiredThemeRoles) {
226
- if (!roleNames.has(role)) warnings.push(`themes.json missing semantic role: ${role}`)
227
- if (selectedTheme && !(role in (selectedTheme.values ?? {}))) {
228
- warnings.push(`Default theme missing value for role: ${role}`)
229
- }
230
- }
231
-
232
- const plannedThemeSlots = Array.isArray(themeManifest.plannedThemeSlots) ? themeManifest.plannedThemeSlots : []
233
- if (!plannedThemeSlots.length) warnings.push('themes.json missing plannedThemeSlots.')
234
-
235
- for (const slot of plannedThemeSlots) {
236
- for (const field of ['sourceTemplates', 'recommendedComponents', 'roleGuidance', 'validationChecklist']) {
237
- if (!Array.isArray(slot[field]) || slot[field].length === 0) {
238
- warnings.push(`Theme slot ${slot.id ?? 'unknown'} missing ${field}.`)
239
- }
240
- }
241
- }
242
-
243
- const themeAuthoring = themeManifest.themeAuthoring ?? {}
244
- for (const field of ['readOrder', 'newThemeChecklist', 'doNot']) {
245
- if (!Array.isArray(themeAuthoring[field]) || themeAuthoring[field].length === 0) {
246
- warnings.push(`themes.json missing themeAuthoring.${field}.`)
247
- }
248
- }
249
-
250
- if (!themeAuthoring.requiredDecision) warnings.push('themes.json missing themeAuthoring.requiredDecision.')
251
- }
252
-
253
- const siteExamplesCandidates = [
254
- join(cwd, 'packages/design-system/src/manifests/site-examples.json'),
255
- join(cwd, 'src/manifests/site-examples.json'),
256
- join(cwd, 'node_modules/@utopia-studio-design/design-system/site-examples.json'),
257
- ]
258
- const siteExamplesPath = siteExamplesCandidates.find(existsSync)
259
-
260
- if (!siteExamplesPath) {
261
- warnings.push('Missing site examples manifest.')
262
- } else {
263
- const siteExamples = JSON.parse(readFileSync(siteExamplesPath, 'utf8'))
264
- for (const collection of ['sourceRoutes', 'badges', 'cards', 'heroes', 'ctas']) {
265
- if (!Array.isArray(siteExamples[collection]) || siteExamples[collection].length === 0) {
266
- warnings.push(`site-examples.json missing collection: ${collection}`)
267
- }
268
- }
269
- }
270
-
271
- const examplesDocPath = join(cwd, 'docs/design-system/examples.md')
272
- const llmsPath = join(cwd, 'public/llms.txt')
273
-
274
- if (!existsSync(examplesDocPath)) {
275
- warnings.push('Missing docs/design-system/examples.md source-example summary.')
276
- }
277
-
278
- if (!existsSync(llmsPath)) {
279
- warnings.push('Missing public/llms.txt AI entrypoint.')
280
- } else {
281
- const llms = readFileSync(llmsPath, 'utf8')
282
- if (!llms.includes('docs/design-system/examples.md')) {
283
- warnings.push('public/llms.txt does not reference docs/design-system/examples.md.')
284
- }
285
- }
286
-
287
- const templateManifestCandidates = [
288
- join(cwd, 'packages/design-system/src/manifests/templates.json'),
289
- join(cwd, 'src/manifests/templates.json'),
290
- join(cwd, 'node_modules/@utopia-studio-design/design-system/templates.json'),
291
- ]
292
- const templateManifestPath = templateManifestCandidates.find(existsSync)
293
-
294
- if (!templateManifestPath) {
295
- warnings.push('Missing templates manifest.')
296
- } else {
297
- const templateManifest = JSON.parse(readFileSync(templateManifestPath, 'utf8'))
298
- const templates = Array.isArray(templateManifest.templates) ? templateManifest.templates : []
135
+ write(cssPath, css)
136
+ write(policyPath, `${JSON.stringify(policy, null, 2)}\n`)
137
+ if (existingThemeIndex >= 0) themes.themes[existingThemeIndex] = entry
138
+ else themes.themes.push(entry)
139
+ write(themesPath, `${JSON.stringify(themes, null, 2)}\n`)
140
+ output('theme-create-result', { ok: true, id, name, root, files: [cssPath, policyPath, themesPath] },
141
+ (data) => `Created and registered theme ${data.id}.\nNext: npm run sync-data --workspace @utopia-studio-design/design-system-cli && npm run ds -- doctor`)
142
+ }
299
143
 
300
- if (!templates.length) warnings.push('templates.json has no templates.')
144
+ function init() {
145
+ const target = resolve(values[0] ?? process.cwd())
146
+ const theme = argAfter('--theme', 'utopia-default')
147
+ if (!getTheme(theme)) return fail(`Unknown theme "${theme}".`, 'ERR_THEME', listThemes().map((item) => item.id))
148
+ const pkgPath = join(target, 'package.json')
149
+ if (!existsSync(pkgPath)) return fail(`No package.json found in ${target}.`, 'ERR_PROJECT')
150
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
151
+ pkg.scripts = { ...pkg.scripts, 'ceramic': 'utopia-ds', 'ceramic:doctor': 'utopia-ds doctor --json' }
152
+ write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
153
+
154
+ const rules = `# Ceramic Design System agent rules
155
+
156
+ 1. Run \`npm run ceramic -- manifest --json\` before generating UI.
157
+ 2. Run \`npm run ceramic -- search <intent> --json\`, then inspect the selected component or template.
158
+ 3. Prefer \`@utopia-studio-design/design-system\` exports over raw shadcn/ui source.
159
+ 4. Components consume semantic tokens only. The active theme is \`${theme}\`.
160
+ 5. Read \`npm run ceramic -- docs arabic-friendly --dense\` before Arabic or RTL work.
161
+ 6. Never invent component props, import paths, tokens, Arabic product copy, or left/right-only APIs.
162
+ 7. Validate with \`npm run ceramic:doctor\` before handoff.
163
+ `
164
+ write(join(target, 'AGENTS.md'), rules)
165
+ write(join(target, 'CLAUDE.md'), rules)
166
+ write(join(target, '.cursor/rules/ceramic-design-system.mdc'), `---\ndescription: Ceramic Design System rules\nalwaysApply: true\n---\n\n${rules}`)
167
+ write(join(target, '.github/copilot-instructions.md'), rules)
168
+ write(join(target, '.ceramic/config.json'), `${JSON.stringify({ apiVersion: 1, theme, arabicFriendly: true, source: '@utopia-studio-design/design-system' }, null, 2)}\n`)
169
+ write(join(target, '.mcp.json'), `${JSON.stringify({ mcpServers: { ceramic: mcpLaunch } }, null, 2)}\n`)
170
+ output('init-result', { ok: true, target, theme, files: ['AGENTS.md', 'CLAUDE.md', '.cursor/rules/ceramic-design-system.mdc', '.github/copilot-instructions.md', '.ceramic/config.json', '.mcp.json'] },
171
+ (data) => `Ceramic initialized in ${data.target}.\nCreated ${data.files.join(', ')}.`)
172
+ }
301
173
 
302
- for (const template of templates) {
303
- for (const field of ['id', 'title', 'purpose', 'aiPrompt']) {
304
- if (!template[field]) warnings.push(`Template missing ${field}: ${template.id ?? 'unknown template'}`)
305
- }
174
+ function runMcp() {
175
+ const child = spawn(process.execPath, [join(dirname(new URL(import.meta.url).pathname), 'utopia-ds-mcp.mjs')], { stdio: 'inherit' })
176
+ child.on('exit', (code) => { process.exitCode = code ?? 0 })
177
+ }
306
178
 
307
- for (const field of ['requiredSections', 'componentStack', 'bestPractices', 'validationChecklist']) {
308
- if (!Array.isArray(template[field]) || template[field].length === 0) {
309
- warnings.push(`Template ${template.id ?? 'unknown template'} missing ${field}.`)
310
- }
179
+ function copyTemplateProject(entry) {
180
+ if (!entry.bundlePath) return fail(`Template "${entry.id}" is a blueprint contract and has no runnable bundle.`, 'ERR_TEMPLATE_BUNDLE')
181
+ const requestedTarget = argAfter('--copy', entry.id.replace(/^template-/, ''))
182
+ const target = resolve(requestedTarget)
183
+ const theme = argAfter('--theme', 'utopia-default')
184
+ if (!getTheme(theme)) return fail(`Unknown theme "${theme}".`, 'ERR_THEME', listThemes().map((item) => item.id))
185
+ if (existsSync(target) && !args.includes('--force')) return fail(`Target already exists: ${target}. Pass --force to overwrite it.`, 'ERR_TARGET_EXISTS')
186
+ const source = resolve(dirname(new URL(import.meta.url).pathname), '..', 'data', entry.bundlePath)
187
+ if (!existsSync(source)) return fail(`Runnable bundle is missing for "${entry.id}".`, 'ERR_TEMPLATE_BUNDLE')
188
+ cpSync(source, target, { recursive: true, force: args.includes('--force') })
189
+
190
+ const replaceTheme = (directory) => {
191
+ for (const name of readdirSync(directory)) {
192
+ const path = join(directory, name)
193
+ if (statSync(path).isDirectory()) replaceTheme(path)
194
+ else if (/\.(html|tsx|md)$/.test(name)) {
195
+ const content = readFileSync(path, 'utf8')
196
+ .replaceAll('/themes/utopia-default.css', `/themes/${theme}.css`)
197
+ .replaceAll('data-theme="utopia-default"', `data-theme="${theme}"`)
198
+ .replaceAll("const activeTheme = 'utopia-default'", `const activeTheme = '${theme}'`)
199
+ write(path, content)
311
200
  }
312
201
  }
313
202
  }
314
-
315
- if (warnings.length) {
316
- console.log('Utopia DS doctor warnings:')
317
- for (const warning of warnings) console.log(`- ${warning}`)
318
- process.exitCode = 1
319
- return
320
- }
321
-
322
- console.log('Utopia DS doctor passed.')
203
+ replaceTheme(target)
204
+
205
+ const packageName = target.split('/').filter(Boolean).at(-1)?.replace(/[^a-z0-9-]+/gi, '-').toLowerCase() || 'ceramic-saas-website'
206
+ const workspacePackage = resolve(process.cwd(), 'packages/design-system')
207
+ const workspaceCli = resolve(process.cwd(), 'packages/design-system-cli')
208
+ const designSystemDependency = existsSync(join(workspacePackage, 'package.json'))
209
+ ? `file:${workspacePackage}`
210
+ : '^0.3.0'
211
+ const cliDependency = existsSync(join(workspaceCli, 'package.json'))
212
+ ? `file:${workspaceCli}`
213
+ : '^0.3.0'
214
+ write(join(target, 'package.json'), `${JSON.stringify({
215
+ name: packageName,
216
+ private: true,
217
+ version: '0.1.0',
218
+ type: 'module',
219
+ scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview', ceramic: 'utopia-ds', 'ceramic:doctor': 'utopia-ds doctor --json' },
220
+ dependencies: {
221
+ '@utopia-studio-design/design-system': designSystemDependency,
222
+ 'framer-motion': '^12.42.2',
223
+ 'lucide-react': '^1.23.0',
224
+ react: '^19.0.0',
225
+ 'react-dom': '^19.0.0',
226
+ },
227
+ devDependencies: { '@utopia-studio-design/design-system-cli': cliDependency, '@vitejs/plugin-react': '^4.3.4', typescript: '^5.7.2', vite: '^6.0.7' },
228
+ }, null, 2)}\n`)
229
+ write(join(target, '.ceramic/config.json'), `${JSON.stringify({ apiVersion: 1, theme, arabicFriendly: true, source: '@utopia-studio-design/design-system' }, null, 2)}\n`)
230
+ write(join(target, 'vite.config.ts'), `import { resolve } from 'node:path'\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n plugins: [react()],\n build: {\n rollupOptions: {\n input: {\n home: resolve('index.html'),\n product: resolve('product/index.html'),\n agents: resolve('agents/index.html'),\n integrations: resolve('integrations/index.html'),\n integrationDetail: resolve('integrations/slack/index.html'),\n customers: resolve('customers/index.html'),\n customerStory: resolve('customers/aster-labs/index.html'),\n pricing: resolve('pricing/index.html'),\n changelog: resolve('changelog/index.html'),\n contactSales: resolve('contact-sales/index.html'),\n },\n },\n },\n})\n`)
231
+ const existingReadme = readFileSync(join(target, 'README.md'), 'utf8')
232
+ write(join(target, 'README.md'), `# Generated Ceramic SaaS website\n\nActive theme: \`${theme}\`\n\n\`\`\`sh\nnpm install\nnpm run dev\n\`\`\`\n\nOpen \`http://localhost:5173/?seed=1974341818\`. All ten page entries share the same seed, theme, and locale state.\n\n${existingReadme}`)
233
+ output('template-copy-result', { ok: true, id: entry.id, target, theme, pages: entry.pages ?? [] }, (data) => `Generated ${data.id} with theme ${data.theme} in ${data.target}.\nNext: cd ${data.target} && npm install && npm run dev`)
323
234
  }
324
235
 
325
- try {
326
- switch (command) {
327
- case '--help':
328
- case 'help':
329
- help()
330
- break
331
- case 'init':
332
- init()
333
- break
334
- case 'add':
335
- add()
336
- break
337
- case 'tokens':
338
- tokens()
339
- break
340
- case 'agent-docs':
341
- agentDocs()
342
- break
343
- case 'doctor':
344
- doctor()
345
- break
346
- default:
347
- help()
348
- process.exitCode = 1
236
+ if (command === 'help' || args.includes('--help')) help()
237
+ else if (command === 'init') init()
238
+ else if (command === 'manifest') output('manifest', capabilityManifest())
239
+ else if (command === 'search') {
240
+ const query = values.join(' ')
241
+ if (!query) fail('Search requires a query.', 'ERR_QUERY')
242
+ else output('search-results', search(query), (rows) => rows.map((row) => `${row.kind}|${row.id}|score=${row.score}`).join('\n'))
243
+ } else if (command === 'component') {
244
+ const name = values[0]
245
+ if (!name || args.includes('--list')) output('component-list', listComponents(), (rows) => rows.map((item) => `${item.name}|${item.status}|${item.category}|${item.packageImport}`).join(dense ? '\n' : '\n- '))
246
+ else { const item = getComponent(name); item ? output('component', item, formatComponent) : fail(`Unknown component "${name}".`, 'ERR_COMPONENT', search(name).slice(0, 5).map((result) => result.id)) }
247
+ } else if (command === 'template') {
248
+ const name = values[0]
249
+ if (!name || args.includes('--list')) output('template-list', listTemplates(), (rows) => rows.map((item) => `${item.id}|${item.category}|${item.title}|${item.purpose}`).join('\n'))
250
+ else {
251
+ const item = getTemplate(name)
252
+ if (!item) fail(`Unknown template "${name}".`, 'ERR_TEMPLATE')
253
+ else if (args.includes('--copy')) copyTemplateProject(item)
254
+ else output('template', item, (entry) => args.includes('--skeleton') ? entry.sections.map((section) => `<section data-ceramic-part="${section}">{/* ${section} */}</section>`).join('\n') : JSON.stringify(entry, null, 2))
349
255
  }
350
- } catch (error) {
351
- console.error(error instanceof Error ? error.message : error)
352
- process.exitCode = 1
353
- }
256
+ } else if (command === 'theme') {
257
+ const name = values[0]
258
+ if (name === 'create') scaffoldTheme()
259
+ else if (!name || args.includes('--list')) output('theme-list', listThemes(), (rows) => rows.map((item) => `${item.id}|${item.name}|${item.role}|${item.policyManifest}`).join('\n'))
260
+ else { const item = getTheme(name); item ? output('theme', item) : fail(`Unknown theme "${name}".`, 'ERR_THEME') }
261
+ } else if (command === 'docs') {
262
+ const topic = values[0]
263
+ if (!topic || args.includes('--list')) output('docs-list', listDocs(), (rows) => rows.join('\n'))
264
+ else { const item = getDoc(topic); item ? output('docs', item, (doc) => dense ? doc.content.replace(/\n{2,}/g, '\n').trim() : doc.content) : fail(`Unknown docs topic "${topic}".`, 'ERR_DOCS', listDocs()) }
265
+ } else if (command === 'doctor') {
266
+ const result = repositoryDoctor(); output('doctor-result', result, (data) => data.ok ? `Ceramic doctor passed. ${data.checks.components} components, ${data.checks.templates} templates, ${data.checks.themes} themes, MCP ready.` : `Ceramic doctor failed: ${data.missing.join(', ')}`); if (!result.ok) process.exitCode = 1
267
+ } else if (command === 'mcp') runMcp()
268
+ else fail(`Unknown command "${command}".`, 'ERR_COMMAND', capabilityManifest().commands.map((item) => item.name))