@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.
- package/README.md +12 -41
- package/bin/utopia-ds-mcp.mjs +55 -0
- package/bin/utopia-ds.mjs +233 -318
- package/data/docs/ai-platform-plan.md +53 -0
- package/data/docs/arabic-friendly.md +72 -0
- package/data/docs/foundations.md +154 -0
- package/data/docs/guide.md +37 -0
- package/data/docs/quick-start-ai.md +122 -0
- package/data/docs/shadcn-conversion.md +35 -0
- package/data/docs/theme-authoring.md +46 -0
- package/data/manifests/catalog.json +547 -0
- package/data/manifests/components.json +4780 -0
- package/data/manifests/patterns.json +12 -0
- package/data/manifests/templates.json +96 -0
- package/data/manifests/theme-barrier-intelligence.json +123 -0
- package/data/manifests/theme-dextrum.json +255 -0
- package/data/manifests/theme-utopia-default.json +163 -0
- package/data/manifests/themes.json +417 -0
- package/data/templates/saas-solution-homepage/README.md +52 -0
- package/data/templates/saas-solution-homepage/agents/index.html +1 -0
- package/data/templates/saas-solution-homepage/changelog/index.html +1 -0
- package/data/templates/saas-solution-homepage/contact-sales/index.html +1 -0
- package/data/templates/saas-solution-homepage/customers/aster-labs/index.html +1 -0
- package/data/templates/saas-solution-homepage/customers/index.html +1 -0
- package/data/templates/saas-solution-homepage/index.html +17 -0
- package/data/templates/saas-solution-homepage/integrations/index.html +1 -0
- package/data/templates/saas-solution-homepage/integrations/slack/index.html +1 -0
- package/data/templates/saas-solution-homepage/main.tsx +642 -0
- package/data/templates/saas-solution-homepage/pricing/index.html +1 -0
- package/data/templates/saas-solution-homepage/product/index.html +1 -0
- package/data/templates/saas-solution-homepage/styles.css +863 -0
- package/data/templates/saas-solution-homepage/template.manifest.json +36 -0
- package/lib/api.mjs +152 -0
- package/package.json +17 -15
- package/LICENSE +0 -21
- package/templates/agent-docs/examples.md +0 -33
- package/templates/agent-docs/index.md +0 -78
- package/templates/agent-docs/llms.txt +0 -33
- package/templates/components/badge.tsx +0 -12
- package/templates/components/brand-action.tsx +0 -32
- package/templates/components/button-group.tsx +0 -30
- package/templates/components/button.tsx +0 -44
- package/templates/components/card.tsx +0 -23
- package/templates/components/cta-section.tsx +0 -34
- package/templates/components/icon-button.tsx +0 -44
- package/templates/components/page-hero.tsx +0 -47
- package/templates/components/tooltip.tsx +0 -48
- package/templates/components/topbar.tsx +0 -59
- package/templates/lib/utils.ts +0 -6
- package/templates/styles/styles.css +0 -271
- package/templates/styles/tokens.css +0 -21
- package/templates/tokens.json +0 -41
package/bin/utopia-ds.mjs
CHANGED
|
@@ -1,353 +1,268 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { dirname, join, resolve } from 'node:path'
|
|
4
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
Usage:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
110
|
-
|
|
68
|
+
function write(path, content) {
|
|
69
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
70
|
+
writeFileSync(path, content, 'utf8')
|
|
111
71
|
}
|
|
112
72
|
|
|
113
|
-
function
|
|
114
|
-
|
|
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
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
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))
|