sapient-ai 0.1.0 → 0.2.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 (29) hide show
  1. package/README.md +4 -1
  2. package/bin/sapient-ai.js +424 -61
  3. package/local-registry/r/button.json +6 -2
  4. package/local-registry/r/customer-satisfaction.json +1 -1
  5. package/local-registry/r/multiple-choice-card.json +2 -2
  6. package/local-registry/r/multiple-choice-grid.json +1 -1
  7. package/local-registry/r/multiple-choice-list.json +1 -1
  8. package/local-registry/r/news-card.json +1 -1
  9. package/local-registry/r/privacy-consent.json +1 -1
  10. package/local-registry/r/product-card.json +1 -1
  11. package/local-registry/r/profile-card.json +1 -1
  12. package/local-registry/r/promo-card.json +1 -1
  13. package/local-registry/r/video-card.json +2 -2
  14. package/local-registry/scripts/build-registry.mjs +1 -0
  15. package/local-registry/src/components/ui/sapient-button.tsx +5 -3
  16. package/local-registry/src/components/ui/sapient-customer-satisfaction.tsx +3 -3
  17. package/local-registry/src/components/ui/sapient-icon.tsx +44 -3
  18. package/local-registry/src/components/ui/sapient-multiple-choice-card.tsx +6 -6
  19. package/local-registry/src/components/ui/sapient-multiple-choice-grid.tsx +3 -3
  20. package/local-registry/src/components/ui/sapient-multiple-choice-list.tsx +1 -1
  21. package/local-registry/src/components/ui/sapient-news-card.tsx +9 -9
  22. package/local-registry/src/components/ui/sapient-privacy-consent.tsx +8 -8
  23. package/local-registry/src/components/ui/sapient-product-card.tsx +13 -13
  24. package/local-registry/src/components/ui/sapient-profile-card.tsx +7 -7
  25. package/local-registry/src/components/ui/sapient-promo-card.tsx +9 -9
  26. package/local-registry/src/components/ui/sapient-radio-button.tsx +1 -1
  27. package/local-registry/src/components/ui/sapient-video-card.tsx +3 -3
  28. package/local-registry/src/components/ui/sapient-video-controller.tsx +3 -3
  29. package/package.json +1 -1
package/README.md CHANGED
@@ -7,6 +7,7 @@ Thin wrapper around `shadcn` for Sapient registry usage.
7
7
  - `sapient-ai init`
8
8
  - `sapient-ai init --preset <handle-or-id> --template next`
9
9
  - `sapient-ai add <component>`
10
+ - `sapient-ai add-icons <design-system|lucide|remix>`
10
11
  - `sapient-ai shadcn <args...>`
11
12
 
12
13
  ## Behavior
@@ -16,6 +17,8 @@ Thin wrapper around `shadcn` for Sapient registry usage.
16
17
  - 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
18
  - The CLI installs a small Sapient foundation pack by default, then adds any additional registry components inferred from the preset.
18
19
  - When the preset references supported Sapient registry items, the CLI also runs `shadcn add` for those components automatically.
20
+ - When the preset uses `lucide` or `remix`, the CLI also installs the matching icon package automatically.
21
+ - `add-icons <library>` updates local Sapient config and installs the matching icon package for an existing project.
19
22
  - `add button` is rewritten to `add @sapient/button`.
20
23
  - Pass fully-qualified names (`@scope/name`) unchanged.
21
24
  - 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.
@@ -24,7 +27,7 @@ Thin wrapper around `shadcn` for Sapient registry usage.
24
27
 
25
28
  ```bash
26
29
  npm pack ./packages/cli
27
- npx --yes ./sapient-ai-0.0.1.tgz --help
30
+ npx --yes ./sapient-ai-0.2.0.tgz --help
28
31
  ```
29
32
 
30
33
  ## Registry URL
package/bin/sapient-ai.js CHANGED
@@ -24,12 +24,14 @@ Usage:
24
24
  sapient-ai init [shadcn-init-args...]
25
25
  sapient-ai init --preset <handle-or-id> [shadcn-init-args...]
26
26
  sapient-ai add <component> [shadcn-add-args...]
27
+ sapient-ai add-icons <design-system|lucide|remix> [--cwd <path>]
27
28
  sapient-ai shadcn [raw-shadcn-args...]
28
29
 
29
30
  Examples:
30
31
  sapient-ai init
31
32
  sapient-ai init --preset ferrari --template next
32
33
  sapient-ai add button
34
+ sapient-ai add-icons remix
33
35
  sapient-ai add @sapient/button
34
36
  sapient-ai shadcn diff
35
37
 
@@ -41,17 +43,17 @@ Environment:
41
43
  `);
42
44
  }
43
45
 
44
- function runShadcn(args) {
46
+ function runShadcn(args, options = {}) {
45
47
  const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
46
48
  return spawnSync(npxCmd, ["--yes", "shadcn@latest", ...args], {
47
49
  stdio: "inherit",
48
50
  env: process.env,
49
- cwd: process.cwd(),
51
+ cwd: options.cwd ?? process.cwd(),
50
52
  });
51
53
  }
52
54
 
53
- function ensureSapientRegistry() {
54
- const componentsPath = path.join(process.cwd(), "components.json");
55
+ function ensureSapientRegistry(projectRoot = process.cwd(), registryUrl = DEFAULT_REGISTRY_URL) {
56
+ const componentsPath = path.join(projectRoot, "components.json");
55
57
 
56
58
  if (!fs.existsSync(componentsPath)) {
57
59
  return;
@@ -79,7 +81,7 @@ function ensureSapientRegistry() {
79
81
  }
80
82
 
81
83
  parsed.registries = registries;
82
- parsed.registries["@sapient"] = DEFAULT_REGISTRY_URL;
84
+ parsed.registries["@sapient"] = registryUrl;
83
85
 
84
86
  fs.writeFileSync(
85
87
  componentsPath,
@@ -87,7 +89,7 @@ function ensureSapientRegistry() {
87
89
  "utf8"
88
90
  );
89
91
  process.stdout.write(
90
- `Updated components.json with @sapient registry: ${DEFAULT_REGISTRY_URL}\n`
92
+ `Updated components.json with @sapient registry: ${registryUrl}\n`
91
93
  );
92
94
  }
93
95
 
@@ -115,6 +117,87 @@ function parseInitArgs(args) {
115
117
  return { presetId, passthrough };
116
118
  }
117
119
 
120
+ function getOptionValue(args, longName, shortName = null) {
121
+ for (let index = 0; index < args.length; index += 1) {
122
+ const value = args[index];
123
+
124
+ if (value === longName || (shortName && value === shortName)) {
125
+ return args[index + 1] ?? null;
126
+ }
127
+
128
+ if (value.startsWith(`${longName}=`)) {
129
+ return value.slice(longName.length + 1);
130
+ }
131
+
132
+ if (shortName && value.startsWith(`${shortName}=`)) {
133
+ return value.slice(shortName.length + 1);
134
+ }
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ function captureDirectoryEntries(directoryPath) {
141
+ try {
142
+ return new Set(fs.readdirSync(directoryPath));
143
+ } catch {
144
+ return new Set();
145
+ }
146
+ }
147
+
148
+ function hasProjectMarkers(directoryPath) {
149
+ return fs.existsSync(path.join(directoryPath, "components.json"));
150
+ }
151
+
152
+ function resolveProjectRoot({
153
+ invocationCwd,
154
+ passthrough,
155
+ entriesBefore,
156
+ }) {
157
+ const explicitCwd = getOptionValue(passthrough, "--cwd", "-c");
158
+ const explicitName = getOptionValue(passthrough, "--name", "-n");
159
+ const baseDirectory = explicitCwd
160
+ ? path.resolve(invocationCwd, explicitCwd)
161
+ : invocationCwd;
162
+
163
+ if (explicitName) {
164
+ const namedProjectDirectory = path.resolve(baseDirectory, explicitName);
165
+ if (hasProjectMarkers(namedProjectDirectory)) {
166
+ return namedProjectDirectory;
167
+ }
168
+ }
169
+
170
+ if (hasProjectMarkers(baseDirectory)) {
171
+ return baseDirectory;
172
+ }
173
+
174
+ const candidateDirectories = [];
175
+
176
+ try {
177
+ const entriesAfter = fs.readdirSync(baseDirectory, { withFileTypes: true });
178
+
179
+ for (const entry of entriesAfter) {
180
+ if (!entry.isDirectory()) {
181
+ continue;
182
+ }
183
+
184
+ const candidatePath = path.join(baseDirectory, entry.name);
185
+
186
+ if (!entriesBefore.has(entry.name) && hasProjectMarkers(candidatePath)) {
187
+ return candidatePath;
188
+ }
189
+
190
+ if (hasProjectMarkers(candidatePath)) {
191
+ candidateDirectories.push(candidatePath);
192
+ }
193
+ }
194
+ } catch {
195
+ return baseDirectory;
196
+ }
197
+
198
+ return candidateDirectories[0] ?? baseDirectory;
199
+ }
200
+
118
201
  async function fetchPreset(presetId) {
119
202
  const baseUrl = DEFAULT_PRESET_BASE_URL.replace(/\/$/, "");
120
203
  const response = await fetch(`${baseUrl}/${encodeURIComponent(presetId)}`);
@@ -176,7 +259,7 @@ async function promptForPresetSelection() {
176
259
  }
177
260
  }
178
261
 
179
- function writePresetFiles(preset) {
262
+ function writePresetFiles(preset, projectRoot = process.cwd()) {
180
263
  if (!preset?.files || !Array.isArray(preset.files)) {
181
264
  throw new Error("Preset payload does not include files.");
182
265
  }
@@ -186,7 +269,7 @@ function writePresetFiles(preset) {
186
269
  continue;
187
270
  }
188
271
 
189
- const absolutePath = path.join(process.cwd(), file.path);
272
+ const absolutePath = path.join(projectRoot, file.path);
190
273
  const directory = path.dirname(absolutePath);
191
274
  fs.mkdirSync(directory, { recursive: true });
192
275
  fs.writeFileSync(absolutePath, file.content ?? "", "utf8");
@@ -206,13 +289,13 @@ function readJsonFileIfExists(filePath) {
206
289
  }
207
290
  }
208
291
 
209
- function findExistingArtifactPath(relativePath) {
210
- const nextPath = path.join(process.cwd(), SAPIENT_ARTIFACT_DIR, relativePath);
292
+ function findExistingArtifactPath(relativePath, projectRoot = process.cwd()) {
293
+ const nextPath = path.join(projectRoot, SAPIENT_ARTIFACT_DIR, relativePath);
211
294
  if (fs.existsSync(nextPath)) {
212
295
  return nextPath;
213
296
  }
214
297
 
215
- const legacyPath = path.join(process.cwd(), relativePath);
298
+ const legacyPath = path.join(projectRoot, relativePath);
216
299
  if (fs.existsSync(legacyPath)) {
217
300
  return legacyPath;
218
301
  }
@@ -233,8 +316,8 @@ function normalizeBaseColor(baseColor) {
233
316
  return match ? match[1] : baseColor;
234
317
  }
235
318
 
236
- function findCssEntryFromComponents() {
237
- const componentsPath = path.join(process.cwd(), "components.json");
319
+ function findCssEntryFromComponents(projectRoot = process.cwd()) {
320
+ const componentsPath = path.join(projectRoot, "components.json");
238
321
  const components = readJsonFileIfExists(componentsPath);
239
322
  const configuredCss = components?.tailwind?.css;
240
323
 
@@ -243,7 +326,7 @@ function findCssEntryFromComponents() {
243
326
  }
244
327
 
245
328
  const candidates = ["app/globals.css", "src/app/globals.css"];
246
- return candidates.find((candidate) => fs.existsSync(path.join(process.cwd(), candidate))) ?? null;
329
+ return candidates.find((candidate) => fs.existsSync(path.join(projectRoot, candidate))) ?? null;
247
330
  }
248
331
 
249
332
  function toTokenMap(tokens) {
@@ -261,6 +344,73 @@ function getTokenReference(tokenMap, name, fallback) {
261
344
  return Object.prototype.hasOwnProperty.call(tokenMap, name) ? `var(${name})` : fallback;
262
345
  }
263
346
 
347
+ function resolveSapientExportFont(config) {
348
+ const fontFallback =
349
+ 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
350
+
351
+ if (config?.font === "custom-google" && typeof config?.customFontUrl === "string") {
352
+ try {
353
+ const parsedUrl = new URL(config.customFontUrl);
354
+ let family = "";
355
+ let importUrl = null;
356
+
357
+ if (parsedUrl.hostname === "fonts.googleapis.com") {
358
+ const familyParam = parsedUrl.searchParams.get("family");
359
+ family = familyParam
360
+ ? decodeURIComponent(familyParam.split(":")[0].replace(/\+/g, " ")).trim()
361
+ : "";
362
+ importUrl = parsedUrl.toString();
363
+ } else if (parsedUrl.hostname === "fonts.google.com") {
364
+ const specimenMatch = parsedUrl.pathname.match(/^\/specimen\/([^/]+)$/);
365
+ family = specimenMatch?.[1]
366
+ ? decodeURIComponent(specimenMatch[1].replace(/\+/g, " ")).trim()
367
+ : "";
368
+ importUrl = family
369
+ ? `https://fonts.googleapis.com/css2?family=${family.replace(/\s+/g, "+")}&display=swap`
370
+ : null;
371
+ }
372
+
373
+ if (importUrl && family) {
374
+ return {
375
+ importUrl,
376
+ fontFamily: `"${family}", ${fontFallback}`,
377
+ };
378
+ }
379
+ } catch {
380
+ // Fall through to non-custom handling.
381
+ }
382
+ }
383
+
384
+ if (config?.font === "inter") {
385
+ return {
386
+ importUrl: "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap",
387
+ fontFamily: `"Inter", ${fontFallback}`,
388
+ };
389
+ }
390
+
391
+ if (config?.font === "helvetica-neue") {
392
+ return { importUrl: null, fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif' };
393
+ }
394
+
395
+ if (config?.font === "georgia") {
396
+ return { importUrl: null, fontFamily: 'Georgia, "Times New Roman", serif' };
397
+ }
398
+
399
+ if (config?.font === "trebuchet") {
400
+ return { importUrl: null, fontFamily: '"Trebuchet MS", "Lucida Grande", sans-serif' };
401
+ }
402
+
403
+ if (typeof config?.customFontName === "string" && config.customFontName.trim().length > 0) {
404
+ return { importUrl: null, fontFamily: `"${config.customFontName.trim()}", ${fontFallback}` };
405
+ }
406
+
407
+ return {
408
+ importUrl: null,
409
+ fontFamily:
410
+ '"Neue Haas Grotesk Display Pro", ui-sans-serif, system-ui, sans-serif',
411
+ };
412
+ }
413
+
264
414
  function buildSapientThemeCss({ tokens, config }) {
265
415
  const tokenMap = toTokenMap(tokens);
266
416
  const baseNeutral = getTokenReference(tokenMap, "--neutral-50", "0 0% 98%");
@@ -282,16 +432,13 @@ function buildSapientThemeCss({ tokens, config }) {
282
432
  const cardRadius = getTokenReference(tokenMap, "--card-radius", radius);
283
433
  const basePadding = typeof config?.padding === "number" ? `${config.padding}px` : "16px";
284
434
  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`;
435
+ const { importUrl: fontImportUrl, fontFamily } = resolveSapientExportFont(config);
289
436
 
290
437
  const tokenLines = Object.entries(tokenMap)
291
438
  .map(([name, value]) => ` ${name}: ${String(value)};`)
292
439
  .join("\n");
293
440
 
294
- return `:root {
441
+ return `${fontImportUrl ? `@import url("${fontImportUrl}");\n\n` : ""}:root {
295
442
  ${tokenLines}
296
443
  --background: ${baseNeutral};
297
444
  --foreground: ${foregroundPrimary};
@@ -399,8 +546,8 @@ html {
399
546
  `;
400
547
  }
401
548
 
402
- function ensureCssImport(cssPath, importPath) {
403
- const absoluteCssPath = path.join(process.cwd(), cssPath);
549
+ function ensureCssImport(projectRoot, cssPath, importPath) {
550
+ const absoluteCssPath = path.join(projectRoot, cssPath);
404
551
 
405
552
  if (!fs.existsSync(absoluteCssPath)) {
406
553
  return;
@@ -423,14 +570,14 @@ function ensureCssImport(cssPath, importPath) {
423
570
  fs.writeFileSync(absoluteCssPath, lines.join("\n"), "utf8");
424
571
  }
425
572
 
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");
573
+ function applySapientPresetTheme(projectRoot = process.cwd()) {
574
+ const configPath = findExistingArtifactPath("sapient-design-system-config.json", projectRoot);
575
+ const tokensPath = findExistingArtifactPath("tokens.json", projectRoot);
576
+ const componentsPath = path.join(projectRoot, "components.json");
430
577
  const config = readJsonFileIfExists(configPath);
431
578
  const tokens = readJsonFileIfExists(tokensPath);
432
579
  const components = readJsonFileIfExists(componentsPath);
433
- const cssPath = findCssEntryFromComponents();
580
+ const cssPath = findCssEntryFromComponents(projectRoot);
434
581
 
435
582
  if (!config || !tokens) {
436
583
  return;
@@ -443,32 +590,6 @@ function applySapientPresetTheme() {
443
590
  components.tailwind.baseColor = normalizedBaseColor;
444
591
  }
445
592
 
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
593
  writeJsonFile(componentsPath, components);
473
594
  }
474
595
 
@@ -477,21 +598,202 @@ function applySapientPresetTheme() {
477
598
  }
478
599
 
479
600
  const themeFilePath = path.join(path.dirname(cssPath), "sapient-theme.css");
480
- const absoluteThemePath = path.join(process.cwd(), themeFilePath);
601
+ const absoluteThemePath = path.join(projectRoot, themeFilePath);
481
602
  const themeCss = buildSapientThemeCss({ tokens, config });
482
603
 
483
604
  fs.mkdirSync(path.dirname(absoluteThemePath), { recursive: true });
484
605
  fs.writeFileSync(absoluteThemePath, themeCss, "utf8");
485
- ensureCssImport(cssPath, "./sapient-theme.css");
606
+ ensureCssImport(projectRoot, cssPath, "./sapient-theme.css");
486
607
  process.stdout.write(`Applied Sapient theme to ${cssPath}\n`);
487
608
  }
488
609
 
489
- function installRegistryComponents(preset) {
610
+ function normalizeIconLibrary(value) {
611
+ if (value === "lucide" || value === "remix") {
612
+ return value;
613
+ }
614
+
615
+ if (value === "sapient" || value === "sapient-icons" || value === "design-system") {
616
+ return "design-system";
617
+ }
618
+
619
+ return null;
620
+ }
621
+
622
+ function collectIconLibrariesFromValue(value, foundLibraries = new Set()) {
623
+ if (Array.isArray(value)) {
624
+ for (const item of value) {
625
+ collectIconLibrariesFromValue(item, foundLibraries);
626
+ }
627
+ return foundLibraries;
628
+ }
629
+
630
+ if (!value || typeof value !== "object") {
631
+ return foundLibraries;
632
+ }
633
+
634
+ for (const [key, nestedValue] of Object.entries(value)) {
635
+ if (key === "iconLibrary" && typeof nestedValue === "string") {
636
+ const normalized = normalizeIconLibrary(nestedValue);
637
+ if (normalized) {
638
+ foundLibraries.add(normalized);
639
+ }
640
+ }
641
+
642
+ collectIconLibrariesFromValue(nestedValue, foundLibraries);
643
+ }
644
+
645
+ return foundLibraries;
646
+ }
647
+
648
+ function getPresetIconLibraries(projectRoot = process.cwd()) {
649
+ const configPath = findExistingArtifactPath("sapient-design-system-config.json", projectRoot);
650
+ const componentCatalogPath = findExistingArtifactPath("sapient-components.json", projectRoot);
651
+ const componentLibraryPath = findExistingArtifactPath("sapient-component-library.json", projectRoot);
652
+ const foundLibraries = new Set();
653
+
654
+ [configPath, componentCatalogPath, componentLibraryPath]
655
+ .map((filePath) => readJsonFileIfExists(filePath))
656
+ .filter(Boolean)
657
+ .forEach((artifact) => {
658
+ collectIconLibrariesFromValue(artifact, foundLibraries);
659
+ });
660
+
661
+ return foundLibraries;
662
+ }
663
+
664
+ function detectPackageManager(projectRoot = process.cwd()) {
665
+ if (fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) {
666
+ return { command: "pnpm", args: ["add"] };
667
+ }
668
+
669
+ if (fs.existsSync(path.join(projectRoot, "yarn.lock"))) {
670
+ return { command: "yarn", args: ["add"] };
671
+ }
672
+
673
+ if (
674
+ fs.existsSync(path.join(projectRoot, "bun.lockb")) ||
675
+ fs.existsSync(path.join(projectRoot, "bun.lock"))
676
+ ) {
677
+ return { command: "bun", args: ["add"] };
678
+ }
679
+
680
+ return { command: "npm", args: ["install"] };
681
+ }
682
+
683
+ function hasInstalledDependency(projectRoot, packageName) {
684
+ const packageJsonPath = path.join(projectRoot, "package.json");
685
+ const packageJson = readJsonFileIfExists(packageJsonPath);
686
+
687
+ if (!packageJson || typeof packageJson !== "object") {
688
+ return false;
689
+ }
690
+
691
+ return Boolean(
692
+ packageJson.dependencies?.[packageName] ||
693
+ packageJson.devDependencies?.[packageName] ||
694
+ packageJson.peerDependencies?.[packageName]
695
+ );
696
+ }
697
+
698
+ function installPresetIconDependencies(projectRoot = process.cwd()) {
699
+ const packageMap = {
700
+ lucide: "lucide-react",
701
+ remix: "@remixicon/react",
702
+ };
703
+
704
+ const requiredPackages = Array.from(getPresetIconLibraries(projectRoot))
705
+ .filter((library) => library !== "design-system")
706
+ .map((library) => packageMap[library])
707
+ .filter(Boolean)
708
+ .filter((packageName) => !hasInstalledDependency(projectRoot, packageName));
709
+
710
+ if (requiredPackages.length === 0) {
711
+ return;
712
+ }
713
+
714
+ const { command, args } = detectPackageManager(projectRoot);
715
+ const result = spawnSync(command, [...args, ...requiredPackages], {
716
+ stdio: "inherit",
717
+ env: process.env,
718
+ cwd: projectRoot,
719
+ });
720
+
721
+ if (result.error) {
722
+ throw new Error(`Failed to install icon dependencies: ${result.error.message}`);
723
+ }
724
+
725
+ if ((result.status ?? 1) !== 0) {
726
+ throw new Error("Icon dependency installation failed.");
727
+ }
728
+
729
+ process.stdout.write(`Installed icon dependencies: ${requiredPackages.join(", ")}\n`);
730
+ }
731
+
732
+ function installSpecificIconLibrary(projectRoot = process.cwd(), library) {
733
+ const packageMap = {
734
+ lucide: "lucide-react",
735
+ remix: "@remixicon/react",
736
+ };
737
+
738
+ const packageName = packageMap[library];
739
+ if (!packageName || hasInstalledDependency(projectRoot, packageName)) {
740
+ return;
741
+ }
742
+
743
+ const { command, args } = detectPackageManager(projectRoot);
744
+ const result = spawnSync(command, [...args, packageName], {
745
+ stdio: "inherit",
746
+ env: process.env,
747
+ cwd: projectRoot,
748
+ });
749
+
750
+ if (result.error) {
751
+ throw new Error(`Failed to install ${packageName}: ${result.error.message}`);
752
+ }
753
+
754
+ if ((result.status ?? 1) !== 0) {
755
+ throw new Error(`Installation failed for ${packageName}.`);
756
+ }
757
+
758
+ process.stdout.write(`Installed icon dependency: ${packageName}\n`);
759
+ }
760
+
761
+ function updateComponentsIconLibrary(projectRoot = process.cwd(), library) {
762
+ const componentsPath = path.join(projectRoot, "components.json");
763
+ const components = readJsonFileIfExists(componentsPath);
764
+
765
+ if (!components || typeof components !== "object") {
766
+ return false;
767
+ }
768
+
769
+ components.iconLibrary = library === "design-system" ? "lucide" : library;
770
+ writeJsonFile(componentsPath, components);
771
+ process.stdout.write(`Updated components.json iconLibrary to ${components.iconLibrary}\n`);
772
+ return true;
773
+ }
774
+
775
+ function updateSapientConfigIconLibrary(projectRoot = process.cwd(), library) {
776
+ const configPath = findExistingArtifactPath("sapient-design-system-config.json", projectRoot);
777
+ const config = readJsonFileIfExists(configPath);
778
+
779
+ if (!config || typeof config !== "object") {
780
+ return false;
781
+ }
782
+
783
+ config.iconLibrary = library;
784
+ writeJsonFile(configPath, config);
785
+ process.stdout.write(`Updated .sapient/sapient-design-system-config.json iconLibrary to ${library}\n`);
786
+ return true;
787
+ }
788
+
789
+ function installRegistryComponents(preset, projectRoot = process.cwd()) {
490
790
  if (!Array.isArray(preset?.registryComponents) || preset.registryComponents.length === 0) {
491
791
  return;
492
792
  }
493
793
 
494
- const result = runShadcn(["add", ...preset.registryComponents]);
794
+ const result = runShadcn(["add", ...preset.registryComponents], {
795
+ cwd: projectRoot,
796
+ });
495
797
 
496
798
  if (result.error) {
497
799
  throw new Error(`Failed to install registry components: ${result.error.message}`);
@@ -527,6 +829,12 @@ function buildHiddenShadcnInitArgs(passthrough) {
527
829
  async function handleInit(args) {
528
830
  const { presetId, passthrough } = parseInitArgs(args);
529
831
  let selectedPreset = presetId;
832
+ const invocationCwd = process.cwd();
833
+ const explicitCwd = getOptionValue(passthrough, "--cwd", "-c");
834
+ const baseDirectory = explicitCwd
835
+ ? path.resolve(invocationCwd, explicitCwd)
836
+ : invocationCwd;
837
+ const entriesBefore = captureDirectoryEntries(baseDirectory);
530
838
 
531
839
  if (!selectedPreset) {
532
840
  try {
@@ -537,7 +845,9 @@ async function handleInit(args) {
537
845
  }
538
846
  }
539
847
 
540
- const result = runShadcn(buildHiddenShadcnInitArgs(passthrough));
848
+ const result = runShadcn(buildHiddenShadcnInitArgs(passthrough), {
849
+ cwd: invocationCwd,
850
+ });
541
851
 
542
852
  if (result.error) {
543
853
  process.stderr.write(`Failed to run shadcn init: ${result.error.message}\n`);
@@ -548,7 +858,17 @@ async function handleInit(args) {
548
858
  process.exit(result.status ?? 1);
549
859
  }
550
860
 
551
- ensureSapientRegistry();
861
+ const projectRoot = resolveProjectRoot({
862
+ invocationCwd,
863
+ passthrough,
864
+ entriesBefore,
865
+ });
866
+
867
+ if (projectRoot !== invocationCwd) {
868
+ process.stdout.write(`Detected project root: ${projectRoot}\n`);
869
+ }
870
+
871
+ ensureSapientRegistry(projectRoot);
552
872
 
553
873
  if (!selectedPreset) {
554
874
  process.exit(0);
@@ -556,9 +876,11 @@ async function handleInit(args) {
556
876
 
557
877
  try {
558
878
  const preset = await fetchPreset(selectedPreset);
559
- writePresetFiles(preset);
560
- applySapientPresetTheme();
561
- installRegistryComponents(preset);
879
+ ensureSapientRegistry(projectRoot, preset?.registryUrl ?? DEFAULT_REGISTRY_URL);
880
+ writePresetFiles(preset, projectRoot);
881
+ applySapientPresetTheme(projectRoot);
882
+ installRegistryComponents(preset, projectRoot);
883
+ installPresetIconDependencies(projectRoot);
562
884
  process.stdout.write(`Applied preset ${preset.presetHandle || selectedPreset}\n`);
563
885
  process.exit(0);
564
886
  } catch (error) {
@@ -592,6 +914,43 @@ function handleAdd(args) {
592
914
  process.exit(result.status ?? 1);
593
915
  }
594
916
 
917
+ function handleAddIcons(args) {
918
+ if (args.length === 0) {
919
+ process.stderr.write("Missing icon library. Example: sapient-ai add-icons lucide\n");
920
+ process.exit(1);
921
+ }
922
+
923
+ const [rawLibrary] = args;
924
+ const library = normalizeIconLibrary(rawLibrary);
925
+
926
+ if (!library) {
927
+ process.stderr.write('Unsupported icon library. Use "design-system", "lucide", or "remix".\n');
928
+ process.exit(1);
929
+ }
930
+
931
+ const explicitCwd = getOptionValue(args, "--cwd", "-c");
932
+ const projectRoot = explicitCwd
933
+ ? path.resolve(process.cwd(), explicitCwd)
934
+ : process.cwd();
935
+
936
+ try {
937
+ updateComponentsIconLibrary(projectRoot, library);
938
+ updateSapientConfigIconLibrary(projectRoot, library);
939
+
940
+ if (library === "design-system") {
941
+ process.stdout.write("Sapient design-system icons selected. No external icon package is required.\n");
942
+ process.exit(0);
943
+ }
944
+
945
+ installSpecificIconLibrary(projectRoot, library);
946
+ process.exit(0);
947
+ } catch (error) {
948
+ const message = error instanceof Error ? error.message : "Unknown error";
949
+ process.stderr.write(`Failed to add icons: ${message}\n`);
950
+ process.exit(1);
951
+ }
952
+ }
953
+
595
954
  function handleRawShadcn(args) {
596
955
  const result = runShadcn(args);
597
956
 
@@ -616,6 +975,10 @@ if (command === "add") {
616
975
  handleAdd(rest);
617
976
  }
618
977
 
978
+ if (command === "add-icons") {
979
+ handleAddIcons(rest);
980
+ }
981
+
619
982
  if (command === "shadcn") {
620
983
  handleRawShadcn(rest);
621
984
  }