sapient-ai 0.1.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 (55) hide show
  1. package/README.md +48 -0
  2. package/bin/sapient-ai.js +623 -0
  3. package/local-registry/README.md +59 -0
  4. package/local-registry/r/accordion.json +65 -0
  5. package/local-registry/r/alert.json +64 -0
  6. package/local-registry/r/badge.json +64 -0
  7. package/local-registry/r/button.json +66 -0
  8. package/local-registry/r/checkbox.json +65 -0
  9. package/local-registry/r/customer-satisfaction.json +61 -0
  10. package/local-registry/r/input.json +61 -0
  11. package/local-registry/r/label.json +64 -0
  12. package/local-registry/r/multiple-choice-card.json +66 -0
  13. package/local-registry/r/multiple-choice-grid.json +64 -0
  14. package/local-registry/r/multiple-choice-list.json +64 -0
  15. package/local-registry/r/news-card.json +61 -0
  16. package/local-registry/r/privacy-consent.json +61 -0
  17. package/local-registry/r/product-card.json +64 -0
  18. package/local-registry/r/profile-card.json +64 -0
  19. package/local-registry/r/progress.json +64 -0
  20. package/local-registry/r/promo-card.json +64 -0
  21. package/local-registry/r/radio-group.json +65 -0
  22. package/local-registry/r/separator.json +64 -0
  23. package/local-registry/r/switch.json +64 -0
  24. package/local-registry/r/tabs.json +64 -0
  25. package/local-registry/r/textarea.json +61 -0
  26. package/local-registry/r/video-card.json +69 -0
  27. package/local-registry/scripts/build-registry.mjs +283 -0
  28. package/local-registry/scripts/sync-to-design-system-public.mjs +43 -0
  29. package/local-registry/src/components/ui/sapient-accordion.tsx +89 -0
  30. package/local-registry/src/components/ui/sapient-alert.tsx +68 -0
  31. package/local-registry/src/components/ui/sapient-badge.tsx +28 -0
  32. package/local-registry/src/components/ui/sapient-button.tsx +31 -0
  33. package/local-registry/src/components/ui/sapient-checkbox.tsx +35 -0
  34. package/local-registry/src/components/ui/sapient-customer-satisfaction.tsx +189 -0
  35. package/local-registry/src/components/ui/sapient-icon.tsx +40 -0
  36. package/local-registry/src/components/ui/sapient-input.tsx +23 -0
  37. package/local-registry/src/components/ui/sapient-label.tsx +25 -0
  38. package/local-registry/src/components/ui/sapient-multiple-choice-card.tsx +172 -0
  39. package/local-registry/src/components/ui/sapient-multiple-choice-grid.tsx +94 -0
  40. package/local-registry/src/components/ui/sapient-multiple-choice-list.tsx +74 -0
  41. package/local-registry/src/components/ui/sapient-news-card.tsx +227 -0
  42. package/local-registry/src/components/ui/sapient-privacy-consent.tsx +197 -0
  43. package/local-registry/src/components/ui/sapient-product-card.tsx +468 -0
  44. package/local-registry/src/components/ui/sapient-profile-card.tsx +193 -0
  45. package/local-registry/src/components/ui/sapient-progress.tsx +32 -0
  46. package/local-registry/src/components/ui/sapient-promo-card.tsx +247 -0
  47. package/local-registry/src/components/ui/sapient-radio-button.tsx +82 -0
  48. package/local-registry/src/components/ui/sapient-radio-group.tsx +54 -0
  49. package/local-registry/src/components/ui/sapient-separator.tsx +28 -0
  50. package/local-registry/src/components/ui/sapient-switch.tsx +36 -0
  51. package/local-registry/src/components/ui/sapient-tabs.tsx +82 -0
  52. package/local-registry/src/components/ui/sapient-textarea.tsx +23 -0
  53. package/local-registry/src/components/ui/sapient-video-card.tsx +159 -0
  54. package/local-registry/src/components/ui/sapient-video-controller.tsx +214 -0
  55. package/package.json +25 -0
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # sapient-ai CLI
2
+
3
+ Thin wrapper around `shadcn` for Sapient registry usage.
4
+
5
+ ## Commands
6
+
7
+ - `sapient-ai init`
8
+ - `sapient-ai init --preset <handle-or-id> --template next`
9
+ - `sapient-ai add <component>`
10
+ - `sapient-ai shadcn <args...>`
11
+
12
+ ## Behavior
13
+
14
+ - `init` runs Sapient's setup flow, can offer a Design System picker when no preset is specified, then initializes the repo and adds `@sapient` registry to `components.json`.
15
+ - `init --preset <handle-or-id>` fetches a saved design-system preset and writes Sapient artifact files into `.sapient/`, including `design-system.md`, `sapient-design-system-config.json`, `tokens.json`, and `sapient-components.json`.
16
+ - The CLI also generates a `sapient-theme.css` file next to the app's `globals.css` file and imports it automatically so the runtime theme matches the exported Sapient preset.
17
+ - The CLI installs a small Sapient foundation pack by default, then adds any additional registry components inferred from the preset.
18
+ - When the preset references supported Sapient registry items, the CLI also runs `shadcn add` for those components automatically.
19
+ - `add button` is rewritten to `add @sapient/button`.
20
+ - Pass fully-qualified names (`@scope/name`) unchanged.
21
+ - Internally, `init` still uses `shadcn` for project wiring, but it now supplies Sapient-owned defaults so the user does not have to answer the underlying `shadcn` prompts.
22
+
23
+ ## Local test
24
+
25
+ ```bash
26
+ npm pack ./packages/cli
27
+ npx --yes ./sapient-ai-0.0.1.tgz --help
28
+ ```
29
+
30
+ ## Registry URL
31
+
32
+ Default: `https://sapient-playground-design-system.vercel.app/r/{name}.json`
33
+
34
+ Override:
35
+
36
+ ```bash
37
+ SAPIENT_REGISTRY_URL="https://your-domain/r/{name}.json" npx sapient-ai@latest init
38
+ ```
39
+
40
+ ## Preset API URL
41
+
42
+ Default: `https://sapient-playground-design-system.vercel.app/api/presets`
43
+
44
+ Override for local development:
45
+
46
+ ```bash
47
+ SAPIENT_PRESET_BASE_URL="http://localhost:3002/api/presets" npx sapient-ai@latest init --preset ferrari --template next
48
+ ```
@@ -0,0 +1,623 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import readline from "node:readline/promises";
7
+ import { stdin as input, stdout as output } from "node:process";
8
+
9
+ const DEFAULT_REGISTRY_URL =
10
+ process.env.SAPIENT_REGISTRY_URL ??
11
+ "https://sapient-playground-design-system.vercel.app/r/{name}.json";
12
+ const DEFAULT_PRESET_BASE_URL =
13
+ process.env.SAPIENT_PRESET_BASE_URL ??
14
+ "https://sapient-playground-design-system.vercel.app/api/presets";
15
+ const SAPIENT_ARTIFACT_DIR = ".sapient";
16
+
17
+ const [, , ...argv] = process.argv;
18
+ const [command, ...rest] = argv;
19
+
20
+ function printHelp() {
21
+ process.stdout.write(`sapient-ai
22
+
23
+ Usage:
24
+ sapient-ai init [shadcn-init-args...]
25
+ sapient-ai init --preset <handle-or-id> [shadcn-init-args...]
26
+ sapient-ai add <component> [shadcn-add-args...]
27
+ sapient-ai shadcn [raw-shadcn-args...]
28
+
29
+ Examples:
30
+ sapient-ai init
31
+ sapient-ai init --preset ferrari --template next
32
+ sapient-ai add button
33
+ sapient-ai add @sapient/button
34
+ sapient-ai shadcn diff
35
+
36
+ Environment:
37
+ SAPIENT_REGISTRY_URL Override registry URL template
38
+ (default: ${DEFAULT_REGISTRY_URL})
39
+ SAPIENT_PRESET_BASE_URL Override preset API base URL
40
+ (default: ${DEFAULT_PRESET_BASE_URL})
41
+ `);
42
+ }
43
+
44
+ function runShadcn(args) {
45
+ const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
46
+ return spawnSync(npxCmd, ["--yes", "shadcn@latest", ...args], {
47
+ stdio: "inherit",
48
+ env: process.env,
49
+ cwd: process.cwd(),
50
+ });
51
+ }
52
+
53
+ function ensureSapientRegistry() {
54
+ const componentsPath = path.join(process.cwd(), "components.json");
55
+
56
+ if (!fs.existsSync(componentsPath)) {
57
+ return;
58
+ }
59
+
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(fs.readFileSync(componentsPath, "utf8"));
63
+ } catch (error) {
64
+ const message =
65
+ error instanceof Error ? error.message : "Unknown parse error";
66
+ process.stderr.write(
67
+ `Warning: could not update components.json registry (${message}).\n`
68
+ );
69
+ return;
70
+ }
71
+
72
+ const registries =
73
+ parsed.registries && typeof parsed.registries === "object"
74
+ ? parsed.registries
75
+ : {};
76
+
77
+ if (registries["@sapient"]) {
78
+ return;
79
+ }
80
+
81
+ parsed.registries = registries;
82
+ parsed.registries["@sapient"] = DEFAULT_REGISTRY_URL;
83
+
84
+ fs.writeFileSync(
85
+ componentsPath,
86
+ `${JSON.stringify(parsed, null, 2)}\n`,
87
+ "utf8"
88
+ );
89
+ process.stdout.write(
90
+ `Updated components.json with @sapient registry: ${DEFAULT_REGISTRY_URL}\n`
91
+ );
92
+ }
93
+
94
+ function parseInitArgs(args) {
95
+ let presetId = null;
96
+ const passthrough = [];
97
+
98
+ for (let index = 0; index < args.length; index += 1) {
99
+ const value = args[index];
100
+
101
+ if (value === "--preset") {
102
+ presetId = args[index + 1] ?? null;
103
+ index += 1;
104
+ continue;
105
+ }
106
+
107
+ if (value.startsWith("--preset=")) {
108
+ presetId = value.slice("--preset=".length);
109
+ continue;
110
+ }
111
+
112
+ passthrough.push(value);
113
+ }
114
+
115
+ return { presetId, passthrough };
116
+ }
117
+
118
+ async function fetchPreset(presetId) {
119
+ const baseUrl = DEFAULT_PRESET_BASE_URL.replace(/\/$/, "");
120
+ const response = await fetch(`${baseUrl}/${encodeURIComponent(presetId)}`);
121
+
122
+ if (!response.ok) {
123
+ throw new Error(`Preset fetch failed with status ${response.status}`);
124
+ }
125
+
126
+ return response.json();
127
+ }
128
+
129
+ async function fetchPresetList() {
130
+ const baseUrl = DEFAULT_PRESET_BASE_URL.replace(/\/$/, "");
131
+ const response = await fetch(baseUrl);
132
+
133
+ if (!response.ok) {
134
+ throw new Error(`Preset list fetch failed with status ${response.status}`);
135
+ }
136
+
137
+ const payload = await response.json();
138
+ return Array.isArray(payload?.presets) ? payload.presets : [];
139
+ }
140
+
141
+ async function promptForPresetSelection() {
142
+ const presets = await fetchPresetList();
143
+
144
+ if (presets.length === 0) {
145
+ return null;
146
+ }
147
+
148
+ process.stdout.write("Available Sapient Design Systems:\n");
149
+ presets.forEach((preset, index) => {
150
+ const label = preset?.projectName || preset?.name || preset?.presetHandle || preset?.presetId;
151
+ const handle = preset?.presetHandle ? ` (${preset.presetHandle})` : "";
152
+ const brand = preset?.brandName ? ` - ${preset.brandName}` : "";
153
+ process.stdout.write(` ${index + 1}. ${label}${handle}${brand}\n`);
154
+ });
155
+
156
+ const rl = readline.createInterface({ input, output });
157
+
158
+ try {
159
+ while (true) {
160
+ const answer = (await rl.question("Select a Design System by number (or press Enter to skip): ")).trim();
161
+
162
+ if (!answer) {
163
+ return null;
164
+ }
165
+
166
+ const selectedIndex = Number.parseInt(answer, 10);
167
+
168
+ if (!Number.isNaN(selectedIndex) && selectedIndex >= 1 && selectedIndex <= presets.length) {
169
+ return presets[selectedIndex - 1]?.presetHandle || presets[selectedIndex - 1]?.presetId || null;
170
+ }
171
+
172
+ process.stdout.write("Please enter a valid number from the list above.\n");
173
+ }
174
+ } finally {
175
+ rl.close();
176
+ }
177
+ }
178
+
179
+ function writePresetFiles(preset) {
180
+ if (!preset?.files || !Array.isArray(preset.files)) {
181
+ throw new Error("Preset payload does not include files.");
182
+ }
183
+
184
+ for (const file of preset.files) {
185
+ if (!file?.path || typeof file.path !== "string") {
186
+ continue;
187
+ }
188
+
189
+ const absolutePath = path.join(process.cwd(), file.path);
190
+ const directory = path.dirname(absolutePath);
191
+ fs.mkdirSync(directory, { recursive: true });
192
+ fs.writeFileSync(absolutePath, file.content ?? "", "utf8");
193
+ process.stdout.write(`Wrote ${file.path}\n`);
194
+ }
195
+ }
196
+
197
+ function readJsonFileIfExists(filePath) {
198
+ if (!fs.existsSync(filePath)) {
199
+ return null;
200
+ }
201
+
202
+ try {
203
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+
209
+ function findExistingArtifactPath(relativePath) {
210
+ const nextPath = path.join(process.cwd(), SAPIENT_ARTIFACT_DIR, relativePath);
211
+ if (fs.existsSync(nextPath)) {
212
+ return nextPath;
213
+ }
214
+
215
+ const legacyPath = path.join(process.cwd(), relativePath);
216
+ if (fs.existsSync(legacyPath)) {
217
+ return legacyPath;
218
+ }
219
+
220
+ return nextPath;
221
+ }
222
+
223
+ function writeJsonFile(filePath, value) {
224
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
225
+ }
226
+
227
+ function normalizeBaseColor(baseColor) {
228
+ if (typeof baseColor !== "string" || baseColor.length === 0) {
229
+ return null;
230
+ }
231
+
232
+ const match = baseColor.match(/^([a-z-]+)-\d+$/i);
233
+ return match ? match[1] : baseColor;
234
+ }
235
+
236
+ function findCssEntryFromComponents() {
237
+ const componentsPath = path.join(process.cwd(), "components.json");
238
+ const components = readJsonFileIfExists(componentsPath);
239
+ const configuredCss = components?.tailwind?.css;
240
+
241
+ if (typeof configuredCss === "string" && configuredCss.length > 0) {
242
+ return configuredCss;
243
+ }
244
+
245
+ const candidates = ["app/globals.css", "src/app/globals.css"];
246
+ return candidates.find((candidate) => fs.existsSync(path.join(process.cwd(), candidate))) ?? null;
247
+ }
248
+
249
+ function toTokenMap(tokens) {
250
+ return Array.isArray(tokens)
251
+ ? tokens.reduce((accumulator, token) => {
252
+ if (token?.name && typeof token.name === "string") {
253
+ accumulator[token.name] = token?.value ?? "";
254
+ }
255
+ return accumulator;
256
+ }, {})
257
+ : {};
258
+ }
259
+
260
+ function getTokenReference(tokenMap, name, fallback) {
261
+ return Object.prototype.hasOwnProperty.call(tokenMap, name) ? `var(${name})` : fallback;
262
+ }
263
+
264
+ function buildSapientThemeCss({ tokens, config }) {
265
+ const tokenMap = toTokenMap(tokens);
266
+ const baseNeutral = getTokenReference(tokenMap, "--neutral-50", "0 0% 98%");
267
+ const baseNeutralDark = getTokenReference(tokenMap, "--neutral-950", "0 0% 12%");
268
+ const surfaceMid = getTokenReference(tokenMap, "--neutral-200", "0 0% 88%");
269
+ const surfaceStrong = getTokenReference(tokenMap, "--neutral-700", "0 0% 35%");
270
+ const borderMid = getTokenReference(tokenMap, "--neutral-300", "0 0% 78%");
271
+ const foregroundPrimary = getTokenReference(tokenMap, "--neutral-950", "0 0% 12%");
272
+ const foregroundSecondary = getTokenReference(tokenMap, "--neutral-600", "0 0% 44%");
273
+ const foregroundTertiary = getTokenReference(tokenMap, "--neutral-400", "0 0% 64%");
274
+ const primary = getTokenReference(tokenMap, "--primary-500", "165 89% 53%");
275
+ const primaryDark = getTokenReference(tokenMap, "--primary-400", "165 88% 68%");
276
+ const secondary = getTokenReference(tokenMap, "--secondary-600", "248 87% 63%");
277
+ const secondaryDark = getTokenReference(tokenMap, "--secondary-400", "247 100% 82%");
278
+ const destructive = getTokenReference(tokenMap, "--red-600", "345 78% 50%");
279
+ const success = getTokenReference(tokenMap, "--green-600", "135 69% 36%");
280
+ const warning = getTokenReference(tokenMap, "--yellow-600", "27 100% 44%");
281
+ const radius = config?.radius ? `var(--radius-token-${config.radius})` : getTokenReference(tokenMap, "--radius-token-md", "13px");
282
+ const cardRadius = getTokenReference(tokenMap, "--card-radius", radius);
283
+ const basePadding = typeof config?.padding === "number" ? `${config.padding}px` : "16px";
284
+ const baseGap = typeof config?.gap === "number" ? `${config.gap}px` : "12px";
285
+ const fontFamily =
286
+ typeof config?.font === "string" && config.font.length > 0 && config.font !== "default"
287
+ ? `'${config.font}', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`
288
+ : `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
289
+
290
+ const tokenLines = Object.entries(tokenMap)
291
+ .map(([name, value]) => ` ${name}: ${String(value)};`)
292
+ .join("\n");
293
+
294
+ return `:root {
295
+ ${tokenLines}
296
+ --background: ${baseNeutral};
297
+ --foreground: ${foregroundPrimary};
298
+ --card: ${baseNeutral};
299
+ --card-foreground: ${foregroundPrimary};
300
+ --popover: 0 0% 100%;
301
+ --popover-foreground: ${foregroundPrimary};
302
+ --primary: ${primary};
303
+ --primary-foreground: 0 0% 0%;
304
+ --secondary: ${secondary};
305
+ --secondary-foreground: 0 0% 100%;
306
+ --muted: ${surfaceMid};
307
+ --muted-foreground: ${foregroundSecondary};
308
+ --accent: ${primary};
309
+ --accent-foreground: 0 0% 0%;
310
+ --destructive: ${destructive};
311
+ --destructive-foreground: 0 0% 100%;
312
+ --success: ${success};
313
+ --success-foreground: 0 0% 100%;
314
+ --warning: ${warning};
315
+ --warning-foreground: 0 0% 100%;
316
+ --border: ${borderMid};
317
+ --input: ${borderMid};
318
+ --ring: ${primary};
319
+ --surface-primary: ${baseNeutral};
320
+ --surface-secondary: 0 0% 100%;
321
+ --surface-tertiary: ${surfaceMid};
322
+ --surface-inverse: ${baseNeutralDark};
323
+ --surface-strong: ${surfaceStrong};
324
+ --border-low: ${getTokenReference(tokenMap, "--neutral-100", "0 0% 95%")};
325
+ --border-mid: ${borderMid};
326
+ --border-high: ${surfaceStrong};
327
+ --border-accent: ${primary};
328
+ --border-accent-alt: ${secondary};
329
+ --foreground-secondary: ${foregroundSecondary};
330
+ --foreground-tertiary: ${foregroundTertiary};
331
+ --foreground-strong: ${getTokenReference(tokenMap, "--primary-800", primary)};
332
+ --foreground-accent: ${getTokenReference(tokenMap, "--primary-600", primary)};
333
+ --foreground-accent-alt: ${secondary};
334
+ --sidebar: ${baseNeutral};
335
+ --sidebar-foreground: ${foregroundPrimary};
336
+ --sidebar-primary: ${primary};
337
+ --sidebar-primary-foreground: 0 0% 0%;
338
+ --sidebar-accent: ${surfaceMid};
339
+ --sidebar-accent-foreground: ${foregroundPrimary};
340
+ --sidebar-border: ${borderMid};
341
+ --sidebar-ring: ${primary};
342
+ --chart-1: ${primary};
343
+ --chart-2: ${getTokenReference(tokenMap, "--secondary-500", secondary)};
344
+ --chart-3: ${getTokenReference(tokenMap, "--neutral-700", surfaceStrong)};
345
+ --chart-4: ${getTokenReference(tokenMap, "--primary-300", primary)};
346
+ --chart-5: ${getTokenReference(tokenMap, "--neutral-500", foregroundSecondary)};
347
+ --radius: ${radius};
348
+ --card-radius: ${cardRadius};
349
+ --sapient-base-padding: ${basePadding};
350
+ --sapient-base-gap: ${baseGap};
351
+ --sapient-font-body: ${fontFamily};
352
+ }
353
+
354
+ .dark {
355
+ --background: ${baseNeutralDark};
356
+ --foreground: ${baseNeutral};
357
+ --card: ${getTokenReference(tokenMap, "--neutral-900", "0 0% 25%")};
358
+ --card-foreground: ${baseNeutral};
359
+ --popover: ${getTokenReference(tokenMap, "--neutral-900", "0 0% 25%")};
360
+ --popover-foreground: ${baseNeutral};
361
+ --primary: ${primaryDark};
362
+ --primary-foreground: 0 0% 0%;
363
+ --secondary: ${secondaryDark};
364
+ --secondary-foreground: 0 0% 100%;
365
+ --muted: ${getTokenReference(tokenMap, "--neutral-800", "0 0% 32%")};
366
+ --muted-foreground: ${getTokenReference(tokenMap, "--neutral-300", "0 0% 78%")};
367
+ --accent: ${primaryDark};
368
+ --accent-foreground: 0 0% 0%;
369
+ --border: ${getTokenReference(tokenMap, "--neutral-800", "0 0% 32%")};
370
+ --input: ${getTokenReference(tokenMap, "--neutral-800", "0 0% 32%")};
371
+ --ring: ${primaryDark};
372
+ --surface-primary: ${baseNeutralDark};
373
+ --surface-secondary: ${getTokenReference(tokenMap, "--neutral-900", "0 0% 25%")};
374
+ --surface-tertiary: ${getTokenReference(tokenMap, "--neutral-800", "0 0% 32%")};
375
+ --surface-inverse: ${baseNeutral};
376
+ --surface-strong: ${getTokenReference(tokenMap, "--neutral-200", "0 0% 88%")};
377
+ --border-accent: ${primaryDark};
378
+ --border-accent-alt: ${secondaryDark};
379
+ --foreground-secondary: ${getTokenReference(tokenMap, "--neutral-300", "0 0% 78%")};
380
+ --foreground-tertiary: ${getTokenReference(tokenMap, "--neutral-400", "0 0% 64%")};
381
+ --foreground-strong: ${getTokenReference(tokenMap, "--primary-200", primaryDark)};
382
+ --foreground-accent: ${primaryDark};
383
+ --foreground-accent-alt: ${secondaryDark};
384
+ --sidebar: ${getTokenReference(tokenMap, "--neutral-900", "0 0% 25%")};
385
+ --sidebar-foreground: ${baseNeutral};
386
+ --sidebar-primary: ${primaryDark};
387
+ --sidebar-primary-foreground: 0 0% 0%;
388
+ --sidebar-accent: ${getTokenReference(tokenMap, "--neutral-800", "0 0% 32%")};
389
+ --sidebar-accent-foreground: ${baseNeutral};
390
+ --sidebar-border: ${getTokenReference(tokenMap, "--neutral-800", "0 0% 32%")};
391
+ --sidebar-ring: ${primaryDark};
392
+ --chart-1: ${primaryDark};
393
+ --chart-2: ${secondaryDark};
394
+ }
395
+
396
+ html {
397
+ font-family: var(--sapient-font-body);
398
+ }
399
+ `;
400
+ }
401
+
402
+ function ensureCssImport(cssPath, importPath) {
403
+ const absoluteCssPath = path.join(process.cwd(), cssPath);
404
+
405
+ if (!fs.existsSync(absoluteCssPath)) {
406
+ return;
407
+ }
408
+
409
+ const currentContent = fs.readFileSync(absoluteCssPath, "utf8");
410
+
411
+ if (currentContent.includes(importPath)) {
412
+ return;
413
+ }
414
+
415
+ const lines = currentContent.split("\n");
416
+ let insertIndex = 0;
417
+
418
+ while (insertIndex < lines.length && lines[insertIndex].trim().startsWith("@import")) {
419
+ insertIndex += 1;
420
+ }
421
+
422
+ lines.splice(insertIndex, 0, `@import "${importPath}";`);
423
+ fs.writeFileSync(absoluteCssPath, lines.join("\n"), "utf8");
424
+ }
425
+
426
+ function applySapientPresetTheme() {
427
+ const configPath = findExistingArtifactPath("sapient-design-system-config.json");
428
+ const tokensPath = findExistingArtifactPath("tokens.json");
429
+ const componentsPath = path.join(process.cwd(), "components.json");
430
+ const config = readJsonFileIfExists(configPath);
431
+ const tokens = readJsonFileIfExists(tokensPath);
432
+ const components = readJsonFileIfExists(componentsPath);
433
+ const cssPath = findCssEntryFromComponents();
434
+
435
+ if (!config || !tokens) {
436
+ return;
437
+ }
438
+
439
+ if (components && typeof components === "object") {
440
+ const normalizedBaseColor = normalizeBaseColor(config.baseColor);
441
+ if (normalizedBaseColor) {
442
+ components.tailwind = components.tailwind && typeof components.tailwind === "object" ? components.tailwind : {};
443
+ components.tailwind.baseColor = normalizedBaseColor;
444
+ }
445
+
446
+ components.sapient =
447
+ components.sapient && typeof components.sapient === "object"
448
+ ? components.sapient
449
+ : {};
450
+ components.sapient.artifactsDir = SAPIENT_ARTIFACT_DIR;
451
+ components.sapient.configPath = `${SAPIENT_ARTIFACT_DIR}/sapient-design-system-config.json`;
452
+ components.sapient.tokensPath = `${SAPIENT_ARTIFACT_DIR}/tokens.json`;
453
+ components.sapient.themeCssPath = cssPath
454
+ ? path.join(path.dirname(cssPath), "sapient-theme.css")
455
+ : "app/sapient-theme.css";
456
+ components.sapient.baseColorToken =
457
+ typeof config.baseColor === "string" ? config.baseColor : null;
458
+ components.sapient.iconLibrary =
459
+ typeof config.iconLibrary === "string" ? config.iconLibrary : null;
460
+ components.sapient.font =
461
+ typeof config.font === "string" ? config.font : null;
462
+ components.sapient.radius =
463
+ typeof config.radius === "string" ? config.radius : null;
464
+ components.sapient.spacing =
465
+ typeof config.padding === "number" || typeof config.gap === "number"
466
+ ? {
467
+ padding: typeof config.padding === "number" ? config.padding : null,
468
+ gap: typeof config.gap === "number" ? config.gap : null,
469
+ }
470
+ : null;
471
+
472
+ writeJsonFile(componentsPath, components);
473
+ }
474
+
475
+ if (!cssPath) {
476
+ return;
477
+ }
478
+
479
+ const themeFilePath = path.join(path.dirname(cssPath), "sapient-theme.css");
480
+ const absoluteThemePath = path.join(process.cwd(), themeFilePath);
481
+ const themeCss = buildSapientThemeCss({ tokens, config });
482
+
483
+ fs.mkdirSync(path.dirname(absoluteThemePath), { recursive: true });
484
+ fs.writeFileSync(absoluteThemePath, themeCss, "utf8");
485
+ ensureCssImport(cssPath, "./sapient-theme.css");
486
+ process.stdout.write(`Applied Sapient theme to ${cssPath}\n`);
487
+ }
488
+
489
+ function installRegistryComponents(preset) {
490
+ if (!Array.isArray(preset?.registryComponents) || preset.registryComponents.length === 0) {
491
+ return;
492
+ }
493
+
494
+ const result = runShadcn(["add", ...preset.registryComponents]);
495
+
496
+ if (result.error) {
497
+ throw new Error(`Failed to install registry components: ${result.error.message}`);
498
+ }
499
+
500
+ if ((result.status ?? 1) !== 0) {
501
+ throw new Error("Registry component installation failed.");
502
+ }
503
+ }
504
+
505
+ function hasOption(args, optionName) {
506
+ return args.some((value) => value === optionName || value.startsWith(`${optionName}=`));
507
+ }
508
+
509
+ function buildHiddenShadcnInitArgs(passthrough) {
510
+ const args = ["init"];
511
+
512
+ if (!hasOption(passthrough, "--yes")) {
513
+ args.push("--yes");
514
+ }
515
+
516
+ if (!hasOption(passthrough, "--base")) {
517
+ args.push("--base", "radix");
518
+ }
519
+
520
+ if (!hasOption(passthrough, "--preset")) {
521
+ args.push("--preset", "nova");
522
+ }
523
+
524
+ return [...args, ...passthrough];
525
+ }
526
+
527
+ async function handleInit(args) {
528
+ const { presetId, passthrough } = parseInitArgs(args);
529
+ let selectedPreset = presetId;
530
+
531
+ if (!selectedPreset) {
532
+ try {
533
+ selectedPreset = await promptForPresetSelection();
534
+ } catch (error) {
535
+ const message = error instanceof Error ? error.message : "Unknown error";
536
+ process.stderr.write(`Warning: could not load Sapient Design Systems (${message}). Continuing without a preset.\n`);
537
+ }
538
+ }
539
+
540
+ const result = runShadcn(buildHiddenShadcnInitArgs(passthrough));
541
+
542
+ if (result.error) {
543
+ process.stderr.write(`Failed to run shadcn init: ${result.error.message}\n`);
544
+ process.exit(1);
545
+ }
546
+
547
+ if ((result.status ?? 1) !== 0) {
548
+ process.exit(result.status ?? 1);
549
+ }
550
+
551
+ ensureSapientRegistry();
552
+
553
+ if (!selectedPreset) {
554
+ process.exit(0);
555
+ }
556
+
557
+ try {
558
+ const preset = await fetchPreset(selectedPreset);
559
+ writePresetFiles(preset);
560
+ applySapientPresetTheme();
561
+ installRegistryComponents(preset);
562
+ process.stdout.write(`Applied preset ${preset.presetHandle || selectedPreset}\n`);
563
+ process.exit(0);
564
+ } catch (error) {
565
+ const message = error instanceof Error ? error.message : "Unknown error";
566
+ process.stderr.write(`Failed to apply preset ${selectedPreset}: ${message}\n`);
567
+ process.exit(1);
568
+ }
569
+ }
570
+
571
+ function handleAdd(args) {
572
+ if (args.length === 0) {
573
+ process.stderr.write("Missing component name. Example: sapient-ai add button\n");
574
+ process.exit(1);
575
+ }
576
+
577
+ const [rawComponent, ...remaining] = args;
578
+ const resolvedComponent =
579
+ rawComponent.startsWith("@") ||
580
+ rawComponent.includes("://") ||
581
+ rawComponent.includes("/")
582
+ ? rawComponent
583
+ : `@sapient/${rawComponent}`;
584
+
585
+ const result = runShadcn(["add", resolvedComponent, ...remaining]);
586
+
587
+ if (result.error) {
588
+ process.stderr.write(`Failed to run shadcn add: ${result.error.message}\n`);
589
+ process.exit(1);
590
+ }
591
+
592
+ process.exit(result.status ?? 1);
593
+ }
594
+
595
+ function handleRawShadcn(args) {
596
+ const result = runShadcn(args);
597
+
598
+ if (result.error) {
599
+ process.stderr.write(`Failed to run shadcn: ${result.error.message}\n`);
600
+ process.exit(1);
601
+ }
602
+
603
+ process.exit(result.status ?? 1);
604
+ }
605
+
606
+ if (!command || command === "-h" || command === "--help" || command === "help") {
607
+ printHelp();
608
+ process.exit(0);
609
+ }
610
+
611
+ if (command === "init") {
612
+ await handleInit(rest);
613
+ }
614
+
615
+ if (command === "add") {
616
+ handleAdd(rest);
617
+ }
618
+
619
+ if (command === "shadcn") {
620
+ handleRawShadcn(rest);
621
+ }
622
+
623
+ handleRawShadcn(argv);