@fragments-sdk/cli 0.15.0 → 0.15.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.
- package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +565 -548
- package/dist/bin.js.map +1 -1
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
- package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
- package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
- package/dist/core/index.js +0 -1
- package/dist/create-JVAU3YKN.js +852 -0
- package/dist/create-JVAU3YKN.js.map +1 -0
- package/dist/doctor-BDPMYYE6.js +385 -0
- package/dist/doctor-BDPMYYE6.js.map +1 -0
- package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
- package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +2 -22
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
- package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
- package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
- package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +1 -2
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
- package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
- package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
- package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
- package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
- package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
- package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
- package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
- package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +18 -16
- package/src/bin.ts +94 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
- package/src/commands/__tests__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +9 -1
- package/src/commands/__tests__/scan-generate.test.ts +1 -1
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +590 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/govern-scan.ts +187 -8
- package/src/commands/govern.ts +65 -2
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +1 -1
- package/src/commands/scan.ts +13 -0
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-push.ts +199 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +1 -1
- package/src/core/drift-verifier.ts +1 -1
- package/src/core/extractor-adapter.ts +1 -1
- package/src/index.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +2 -2
- package/src/service/index.ts +8 -0
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +30 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/dist/chunk-65WSVDV5.js.map +0 -1
- package/dist/chunk-7WHVW72L.js +0 -2664
- package/dist/chunk-7WHVW72L.js.map +0 -1
- package/dist/chunk-CZD3AD4Q.js.map +0 -1
- package/dist/chunk-MN3TJ3D5.js +0 -695
- package/dist/chunk-MN3TJ3D5.js.map +0 -1
- package/dist/chunk-XJQ5BIWI.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/govern-scan-UCBZR6D6.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-generate-TWRHNU5M.js.map +0 -1
- package/src/build.ts +0 -736
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1121
- package/src/core/token-resolver.ts +0 -155
- package/src/viewer/preview-adapter.ts +0 -116
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
- /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
- /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments create - Scaffold a new project with Fragments UI and a custom theme
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the shadcn model: configure on the web (usefragments.com/create),
|
|
5
|
+
* then `npx @fragments-sdk/cli create` bootstraps a project with that config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'node:fs';
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
import pc from 'picocolors';
|
|
12
|
+
import { BRAND } from '../core/index.js';
|
|
13
|
+
import { decompressTheme } from '../theme/serializer.js';
|
|
14
|
+
import { generateCssTokens, generateScssTokens } from '../theme/generator.js';
|
|
15
|
+
import type { ThemeConfig } from '../theme/types.js';
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// Font Helpers
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract the primary font family name from a CSS font-stack string.
|
|
23
|
+
* e.g. '"Space Grotesk", sans-serif' → 'Space Grotesk'
|
|
24
|
+
*/
|
|
25
|
+
function extractFontFamily(cssFontStack: string): string | null {
|
|
26
|
+
const match = cssFontStack.match(/^["']?([^"',]+)["']?/);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const name = match[1].trim();
|
|
29
|
+
const generics = ["sans-serif", "serif", "monospace", "cursive", "fantasy", "system-ui"];
|
|
30
|
+
if (generics.includes(name.toLowerCase())) return null;
|
|
31
|
+
if (name.toLowerCase() === "inter") return null; // Inter is the default, no need to load
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function googleFontsUrl(familyName: string): string {
|
|
36
|
+
return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(familyName)}:wght@400;500;600;700&display=swap`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// Types
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
export interface CreateOptions {
|
|
44
|
+
name?: string;
|
|
45
|
+
template?: 'nextjs' | 'vite';
|
|
46
|
+
packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
47
|
+
theme?: string;
|
|
48
|
+
preset?: string;
|
|
49
|
+
brand?: string;
|
|
50
|
+
scss?: boolean;
|
|
51
|
+
mcp?: boolean;
|
|
52
|
+
yes?: boolean;
|
|
53
|
+
noGit?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CreateResult {
|
|
57
|
+
success: boolean;
|
|
58
|
+
projectDir?: string;
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// Package Manager Detection
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
function detectPackageManager(): 'npm' | 'pnpm' | 'yarn' | 'bun' {
|
|
67
|
+
const agent = process.env.npm_config_user_agent || '';
|
|
68
|
+
if (agent.startsWith('pnpm')) return 'pnpm';
|
|
69
|
+
if (agent.startsWith('yarn')) return 'yarn';
|
|
70
|
+
if (agent.startsWith('bun')) return 'bun';
|
|
71
|
+
return 'npm';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getInstallCommand(pm: string): string {
|
|
75
|
+
return pm === 'yarn' ? 'yarn add' : `${pm} add`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getDevInstallCommand(pm: string): string {
|
|
79
|
+
return pm === 'yarn' ? 'yarn add -D' : `${pm} add -D`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getRunCommand(pm: string): string {
|
|
83
|
+
return pm === 'npm' ? 'npm run' : pm;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================
|
|
87
|
+
// Validation
|
|
88
|
+
// ============================================
|
|
89
|
+
|
|
90
|
+
function isValidProjectName(name: string): boolean {
|
|
91
|
+
return /^[a-z0-9][a-z0-9._-]*$/.test(name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================
|
|
95
|
+
// Theme Resolution
|
|
96
|
+
// ============================================
|
|
97
|
+
|
|
98
|
+
const PRESET_API_URL = 'https://canny-otter-874.convex.site/api/theme-presets';
|
|
99
|
+
|
|
100
|
+
async function fetchPreset(presetId: string): Promise<ThemeConfig | null> {
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(`${PRESET_API_URL}?id=${encodeURIComponent(presetId)}`);
|
|
103
|
+
if (!res.ok) return null;
|
|
104
|
+
const theme = await res.json();
|
|
105
|
+
if (!theme || !theme.name) return null;
|
|
106
|
+
return theme as ThemeConfig;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function resolveTheme(options: CreateOptions): Promise<ThemeConfig | null> {
|
|
113
|
+
if (options.preset) {
|
|
114
|
+
console.log(pc.dim(` Fetching theme preset ${options.preset}...`));
|
|
115
|
+
const theme = await fetchPreset(options.preset);
|
|
116
|
+
if (!theme) {
|
|
117
|
+
console.error(pc.red(`Error: Could not fetch preset "${options.preset}". It may have expired or the ID is invalid.`));
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return theme;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.theme) {
|
|
124
|
+
const decoded = decompressTheme(options.theme);
|
|
125
|
+
if (!decoded) {
|
|
126
|
+
console.error(pc.red('Error: Could not decode theme string. Make sure you copied it from usefragments.com/create'));
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return decoded;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.brand) {
|
|
133
|
+
return {
|
|
134
|
+
name: 'custom',
|
|
135
|
+
colors: {
|
|
136
|
+
accent: options.brand,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
name: 'default',
|
|
143
|
+
colors: {
|
|
144
|
+
accent: '#6366f1',
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================
|
|
150
|
+
// Templates
|
|
151
|
+
// ============================================
|
|
152
|
+
|
|
153
|
+
export function generateNextjsLayout(themePath: string, theme?: ThemeConfig): string {
|
|
154
|
+
const fontName = theme?.typography?.fontSans ? extractFontFamily(theme.typography.fontSans) : null;
|
|
155
|
+
const fontUrl = fontName ? googleFontsUrl(fontName) : null;
|
|
156
|
+
|
|
157
|
+
const htmlOpen = fontUrl
|
|
158
|
+
? ` <html lang="en" suppressHydrationWarning>
|
|
159
|
+
<head>
|
|
160
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
161
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
|
162
|
+
<link href="${fontUrl}" rel="stylesheet" />
|
|
163
|
+
</head>
|
|
164
|
+
<body>`
|
|
165
|
+
: ` <html lang="en" suppressHydrationWarning>
|
|
166
|
+
<body>`;
|
|
167
|
+
|
|
168
|
+
return `import type { Metadata } from 'next';
|
|
169
|
+
import '@fragments-sdk/ui/styles';
|
|
170
|
+
import '${themePath}';
|
|
171
|
+
import { Providers } from './providers';
|
|
172
|
+
|
|
173
|
+
export const metadata: Metadata = {
|
|
174
|
+
title: 'My App',
|
|
175
|
+
description: 'Built with Fragments UI',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default function RootLayout({
|
|
179
|
+
children,
|
|
180
|
+
}: {
|
|
181
|
+
children: React.ReactNode;
|
|
182
|
+
}) {
|
|
183
|
+
return (
|
|
184
|
+
${htmlOpen}
|
|
185
|
+
<Providers>{children}</Providers>
|
|
186
|
+
</body>
|
|
187
|
+
</html>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function generateNextjsProviders(): string {
|
|
194
|
+
return `'use client';
|
|
195
|
+
|
|
196
|
+
import type { ReactNode } from 'react';
|
|
197
|
+
import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';
|
|
198
|
+
|
|
199
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
200
|
+
return (
|
|
201
|
+
<ThemeProvider defaultMode="system">
|
|
202
|
+
<TooltipProvider>
|
|
203
|
+
<ToastProvider>{children}</ToastProvider>
|
|
204
|
+
</TooltipProvider>
|
|
205
|
+
</ThemeProvider>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function generateNextjsPage(): string {
|
|
212
|
+
return `'use client';
|
|
213
|
+
|
|
214
|
+
import { Button, Card, Stack, Text, Input } from '@fragments-sdk/ui';
|
|
215
|
+
|
|
216
|
+
export default function Home() {
|
|
217
|
+
return (
|
|
218
|
+
<main style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
219
|
+
<Card style={{ maxWidth: 480, width: '100%' }}>
|
|
220
|
+
<Card.Header>
|
|
221
|
+
<Card.Title>Welcome to Fragments</Card.Title>
|
|
222
|
+
<Card.Description>Your app is ready. Start building something great.</Card.Description>
|
|
223
|
+
</Card.Header>
|
|
224
|
+
<Card.Body>
|
|
225
|
+
<Stack gap={3}>
|
|
226
|
+
<Input placeholder="Enter something..." />
|
|
227
|
+
<Stack direction="row" gap={2}>
|
|
228
|
+
<Button>Get Started</Button>
|
|
229
|
+
<Button variant="secondary">Learn More</Button>
|
|
230
|
+
</Stack>
|
|
231
|
+
</Stack>
|
|
232
|
+
</Card.Body>
|
|
233
|
+
</Card>
|
|
234
|
+
</main>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function generateViteMain(themePath: string): string {
|
|
241
|
+
return `import { StrictMode } from 'react';
|
|
242
|
+
import { createRoot } from 'react-dom/client';
|
|
243
|
+
import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';
|
|
244
|
+
import '@fragments-sdk/ui/styles';
|
|
245
|
+
import '${themePath}';
|
|
246
|
+
import App from './App';
|
|
247
|
+
|
|
248
|
+
createRoot(document.getElementById('root')!).render(
|
|
249
|
+
<StrictMode>
|
|
250
|
+
<ThemeProvider defaultMode="system">
|
|
251
|
+
<TooltipProvider>
|
|
252
|
+
<ToastProvider>
|
|
253
|
+
<App />
|
|
254
|
+
</ToastProvider>
|
|
255
|
+
</TooltipProvider>
|
|
256
|
+
</ThemeProvider>
|
|
257
|
+
</StrictMode>,
|
|
258
|
+
);
|
|
259
|
+
`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function generateViteApp(): string {
|
|
263
|
+
return `import { Button, Card, Stack, Text, Input } from '@fragments-sdk/ui';
|
|
264
|
+
|
|
265
|
+
function App() {
|
|
266
|
+
return (
|
|
267
|
+
<main style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
268
|
+
<Card style={{ maxWidth: 480, width: '100%' }}>
|
|
269
|
+
<Card.Header>
|
|
270
|
+
<Card.Title>Welcome to Fragments</Card.Title>
|
|
271
|
+
<Card.Description>Your app is ready. Start building something great.</Card.Description>
|
|
272
|
+
</Card.Header>
|
|
273
|
+
<Card.Body>
|
|
274
|
+
<Stack gap={3}>
|
|
275
|
+
<Input placeholder="Enter something..." />
|
|
276
|
+
<Stack direction="row" gap={2}>
|
|
277
|
+
<Button>Get Started</Button>
|
|
278
|
+
<Button variant="secondary">Learn More</Button>
|
|
279
|
+
</Stack>
|
|
280
|
+
</Stack>
|
|
281
|
+
</Card.Body>
|
|
282
|
+
</Card>
|
|
283
|
+
</main>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export default App;
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function injectFontIntoViteHtml(projectDir: string, theme: ThemeConfig): void {
|
|
292
|
+
const fontName = theme.typography?.fontSans ? extractFontFamily(theme.typography.fontSans) : null;
|
|
293
|
+
if (!fontName) return;
|
|
294
|
+
|
|
295
|
+
const indexPath = join(projectDir, "index.html");
|
|
296
|
+
if (!existsSync(indexPath)) return;
|
|
297
|
+
|
|
298
|
+
let html = readFileSync(indexPath, "utf-8");
|
|
299
|
+
const fontLinks = [
|
|
300
|
+
' <link rel="preconnect" href="https://fonts.googleapis.com" />',
|
|
301
|
+
' <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />',
|
|
302
|
+
` <link href="${googleFontsUrl(fontName)}" rel="stylesheet" />`,
|
|
303
|
+
].join("\n");
|
|
304
|
+
html = html.replace("</head>", `${fontLinks}\n </head>`);
|
|
305
|
+
writeFileSync(indexPath, html, "utf-8");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ============================================
|
|
309
|
+
// Scaffolding
|
|
310
|
+
// ============================================
|
|
311
|
+
|
|
312
|
+
async function promptIfMissing(options: CreateOptions): Promise<CreateOptions> {
|
|
313
|
+
const resolved = { ...options };
|
|
314
|
+
|
|
315
|
+
if (!resolved.name) {
|
|
316
|
+
if (resolved.yes) {
|
|
317
|
+
resolved.name = 'my-app';
|
|
318
|
+
} else {
|
|
319
|
+
try {
|
|
320
|
+
const { input } = await import('@inquirer/prompts');
|
|
321
|
+
resolved.name = await input({
|
|
322
|
+
message: 'Project name:',
|
|
323
|
+
default: 'my-app',
|
|
324
|
+
validate: (val) => isValidProjectName(val) || 'Use lowercase letters, numbers, hyphens, dots, underscores',
|
|
325
|
+
});
|
|
326
|
+
} catch {
|
|
327
|
+
resolved.name = 'my-app';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!resolved.template && !resolved.yes) {
|
|
333
|
+
try {
|
|
334
|
+
const { select } = await import('@inquirer/prompts');
|
|
335
|
+
resolved.template = await select({
|
|
336
|
+
message: 'Framework:',
|
|
337
|
+
choices: [
|
|
338
|
+
{ name: 'Next.js (App Router)', value: 'nextjs' as const },
|
|
339
|
+
{ name: 'Vite + React', value: 'vite' as const },
|
|
340
|
+
],
|
|
341
|
+
default: 'nextjs',
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
resolved.template = 'nextjs';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
resolved.template = resolved.template || 'nextjs';
|
|
349
|
+
resolved.packageManager = resolved.packageManager || detectPackageManager();
|
|
350
|
+
|
|
351
|
+
return resolved;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function scaffoldFramework(name: string, template: string, pm: string): void {
|
|
355
|
+
console.log(pc.cyan(`\nScaffolding ${template === 'nextjs' ? 'Next.js' : 'Vite + React'} project...\n`));
|
|
356
|
+
|
|
357
|
+
if (template === 'nextjs') {
|
|
358
|
+
const cmd = `npx create-next-app@latest ${name} --ts --app --src-dir --no-tailwind --no-eslint --import-alias "@/*" --yes`;
|
|
359
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
360
|
+
} else {
|
|
361
|
+
// Vite
|
|
362
|
+
if (pm === 'bun') {
|
|
363
|
+
execSync(`bun create vite ${name} --template react-ts`, { stdio: 'inherit' });
|
|
364
|
+
} else {
|
|
365
|
+
execSync(`npm create vite@latest ${name} -- --template react-ts`, { stdio: 'inherit' });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function installDeps(projectDir: string, pm: string, scss: boolean): void {
|
|
371
|
+
console.log(pc.cyan('\nInstalling Fragments UI...\n'));
|
|
372
|
+
|
|
373
|
+
const install = getInstallCommand(pm);
|
|
374
|
+
execSync(`${install} @fragments-sdk/ui`, { cwd: projectDir, stdio: 'inherit' });
|
|
375
|
+
|
|
376
|
+
if (scss) {
|
|
377
|
+
const devInstall = getDevInstallCommand(pm);
|
|
378
|
+
execSync(`${devInstall} sass`, { cwd: projectDir, stdio: 'inherit' });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function writeThemeFile(projectDir: string, theme: ThemeConfig, scss: boolean, template: string): string {
|
|
383
|
+
const stylesDir = join(projectDir, 'src', 'styles');
|
|
384
|
+
mkdirSync(stylesDir, { recursive: true });
|
|
385
|
+
|
|
386
|
+
const ext = scss ? 'scss' : 'css';
|
|
387
|
+
const content = scss ? generateScssTokens(theme) : generateCssTokens(theme);
|
|
388
|
+
const filePath = join(stylesDir, `theme.${ext}`);
|
|
389
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
390
|
+
|
|
391
|
+
// Next.js layout is at src/app/layout.tsx → relative path is ../styles/theme.ext
|
|
392
|
+
// Vite main is at src/main.tsx → relative path is ./styles/theme.ext
|
|
393
|
+
if (template === 'nextjs') {
|
|
394
|
+
return `../styles/theme.${ext}`;
|
|
395
|
+
}
|
|
396
|
+
return `./styles/theme.${ext}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function addNextTranspilePackages(projectDir: string): void {
|
|
400
|
+
const configCandidates = ['next.config.ts', 'next.config.mjs', 'next.config.js'];
|
|
401
|
+
|
|
402
|
+
for (const configFile of configCandidates) {
|
|
403
|
+
const fullPath = join(projectDir, configFile);
|
|
404
|
+
if (!existsSync(fullPath)) continue;
|
|
405
|
+
|
|
406
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
407
|
+
|
|
408
|
+
if (content.includes('transpilePackages') && content.includes('@fragments-sdk/ui')) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (content.includes('transpilePackages')) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const patterns = [
|
|
417
|
+
{
|
|
418
|
+
search: /const\s+\w+\s*(?::\s*\w+)?\s*=\s*\{/,
|
|
419
|
+
replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],`,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
search: /module\.exports\s*=\s*\{/,
|
|
423
|
+
replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],`,
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
search: /export\s+default\s*\{/,
|
|
427
|
+
replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],`,
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
for (const pattern of patterns) {
|
|
432
|
+
if (!pattern.search.test(content)) continue;
|
|
433
|
+
|
|
434
|
+
writeFileSync(
|
|
435
|
+
fullPath,
|
|
436
|
+
content.replace(pattern.search, pattern.replacement),
|
|
437
|
+
'utf-8',
|
|
438
|
+
);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function rewriteAppFiles(projectDir: string, template: string, themePath: string, theme: ThemeConfig): void {
|
|
447
|
+
if (template === 'nextjs') {
|
|
448
|
+
// Rewrite layout.tsx
|
|
449
|
+
const layoutPath = join(projectDir, 'src', 'app', 'layout.tsx');
|
|
450
|
+
writeFileSync(layoutPath, generateNextjsLayout(themePath, theme), 'utf-8');
|
|
451
|
+
|
|
452
|
+
const providersPath = join(projectDir, 'src', 'app', 'providers.tsx');
|
|
453
|
+
writeFileSync(providersPath, generateNextjsProviders(), 'utf-8');
|
|
454
|
+
|
|
455
|
+
// Rewrite page.tsx
|
|
456
|
+
const pagePath = join(projectDir, 'src', 'app', 'page.tsx');
|
|
457
|
+
writeFileSync(pagePath, generateNextjsPage(), 'utf-8');
|
|
458
|
+
|
|
459
|
+
// Remove default page.module.css if it exists
|
|
460
|
+
const moduleCssPath = join(projectDir, 'src', 'app', 'page.module.css');
|
|
461
|
+
try {
|
|
462
|
+
unlinkSync(moduleCssPath);
|
|
463
|
+
} catch { /* doesn't exist, that's fine */ }
|
|
464
|
+
|
|
465
|
+
// Remove globals.css if it exists (we use our own theme)
|
|
466
|
+
const globalsCssPath = join(projectDir, 'src', 'app', 'globals.css');
|
|
467
|
+
try {
|
|
468
|
+
unlinkSync(globalsCssPath);
|
|
469
|
+
} catch { /* doesn't exist */ }
|
|
470
|
+
|
|
471
|
+
addNextTranspilePackages(projectDir);
|
|
472
|
+
} else {
|
|
473
|
+
// Vite: rewrite main.tsx and App.tsx
|
|
474
|
+
const mainPath = join(projectDir, 'src', 'main.tsx');
|
|
475
|
+
writeFileSync(mainPath, generateViteMain(themePath), 'utf-8');
|
|
476
|
+
|
|
477
|
+
const appPath = join(projectDir, 'src', 'App.tsx');
|
|
478
|
+
writeFileSync(appPath, generateViteApp(), 'utf-8');
|
|
479
|
+
|
|
480
|
+
// Clean up default Vite files
|
|
481
|
+
for (const file of ['src/App.css', 'src/index.css']) {
|
|
482
|
+
try {
|
|
483
|
+
unlinkSync(join(projectDir, file));
|
|
484
|
+
} catch { /* doesn't exist */ }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
injectFontIntoViteHtml(projectDir, theme);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function initGit(projectDir: string): void {
|
|
492
|
+
try {
|
|
493
|
+
// create-next-app already inits git, so check first
|
|
494
|
+
if (!existsSync(join(projectDir, '.git'))) {
|
|
495
|
+
execSync('git init', { cwd: projectDir, stdio: 'ignore' });
|
|
496
|
+
execSync('git add -A', { cwd: projectDir, stdio: 'ignore' });
|
|
497
|
+
execSync('git commit -m "Initial commit with Fragments UI"', { cwd: projectDir, stdio: 'ignore' });
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
// Git not available — skip silently
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function configureMcp(projectDir: string): void {
|
|
505
|
+
const mcpConfig = {
|
|
506
|
+
mcpServers: {
|
|
507
|
+
fragments: {
|
|
508
|
+
command: 'npx',
|
|
509
|
+
args: ['-y', '@fragments-sdk/cli', 'mcp'],
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const mcpDir = join(projectDir, '.mcp');
|
|
515
|
+
mkdirSync(mcpDir, { recursive: true });
|
|
516
|
+
writeFileSync(join(mcpDir, 'config.json'), JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ============================================
|
|
520
|
+
// Main
|
|
521
|
+
// ============================================
|
|
522
|
+
|
|
523
|
+
export async function create(options: CreateOptions): Promise<CreateResult> {
|
|
524
|
+
console.log(pc.cyan(`\n${BRAND.name} Create\n`));
|
|
525
|
+
|
|
526
|
+
// 1. Gather inputs
|
|
527
|
+
const resolved = await promptIfMissing(options);
|
|
528
|
+
const name = resolved.name!;
|
|
529
|
+
const template = resolved.template!;
|
|
530
|
+
const pm = resolved.packageManager!;
|
|
531
|
+
|
|
532
|
+
// 2. Validate
|
|
533
|
+
if (!isValidProjectName(name)) {
|
|
534
|
+
return { success: false, error: `Invalid project name: ${name}. Use lowercase letters, numbers, hyphens, dots, underscores.` };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const projectDir = resolve(process.cwd(), name);
|
|
538
|
+
if (existsSync(projectDir)) {
|
|
539
|
+
return { success: false, error: `Directory "${name}" already exists.` };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 3. Resolve theme
|
|
543
|
+
const theme = await resolveTheme(resolved);
|
|
544
|
+
if (!theme) {
|
|
545
|
+
return { success: false, error: 'Invalid theme configuration.' };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// 4. Scaffold framework
|
|
549
|
+
scaffoldFramework(name, template, pm);
|
|
550
|
+
|
|
551
|
+
if (!existsSync(projectDir)) {
|
|
552
|
+
return { success: false, error: 'Framework scaffolding failed — project directory was not created.' };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// 5. Install deps
|
|
556
|
+
installDeps(projectDir, pm, !!resolved.scss);
|
|
557
|
+
|
|
558
|
+
// 6. Write theme tokens
|
|
559
|
+
const themePath = writeThemeFile(projectDir, theme, !!resolved.scss, template);
|
|
560
|
+
|
|
561
|
+
// 7. Rewrite app files with providers + theme import
|
|
562
|
+
rewriteAppFiles(projectDir, template, themePath, theme);
|
|
563
|
+
|
|
564
|
+
// 8. MCP config
|
|
565
|
+
if (resolved.mcp) {
|
|
566
|
+
configureMcp(projectDir);
|
|
567
|
+
console.log(pc.dim(' Configured MCP server for AI tooling'));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 9. Git init
|
|
571
|
+
if (!resolved.noGit) {
|
|
572
|
+
initGit(projectDir);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 10. Success message
|
|
576
|
+
const run = getRunCommand(pm);
|
|
577
|
+
console.log('');
|
|
578
|
+
console.log(pc.green(' Project created successfully!'));
|
|
579
|
+
console.log('');
|
|
580
|
+
console.log(` ${pc.cyan('cd')} ${name}`);
|
|
581
|
+
console.log(` ${pc.cyan(run)} dev`);
|
|
582
|
+
console.log('');
|
|
583
|
+
|
|
584
|
+
if (theme.name !== 'default') {
|
|
585
|
+
console.log(pc.dim(` Theme "${theme.name}" applied with full token set.`));
|
|
586
|
+
console.log(pc.dim(` Edit src/styles/theme.${resolved.scss ? 'scss' : 'css'} to customize.\n`));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return { success: true, projectDir };
|
|
590
|
+
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { readFile, access } from 'node:fs/promises';
|
|
|
13
13
|
import { join, resolve } from 'node:path';
|
|
14
14
|
import pc from 'picocolors';
|
|
15
15
|
import { BRAND } from '../core/index.js';
|
|
16
|
+
import { NEUTRAL_PALETTES } from '@fragments-sdk/viewer/docs-data';
|
|
16
17
|
|
|
17
18
|
// ============================================
|
|
18
19
|
// Types
|
|
@@ -43,10 +44,10 @@ export interface DoctorResult {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// ============================================
|
|
46
|
-
// Valid seed values
|
|
47
|
+
// Valid seed values — derived from canonical sources
|
|
47
48
|
// ============================================
|
|
48
49
|
|
|
49
|
-
const VALID_NEUTRALS =
|
|
50
|
+
const VALID_NEUTRALS = NEUTRAL_PALETTES.map((p) => p.name);
|
|
50
51
|
const VALID_DENSITIES = ['compact', 'default', 'relaxed'];
|
|
51
52
|
const VALID_RADII = ['sharp', 'subtle', 'default', 'rounded', 'pill'];
|
|
52
53
|
|