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.
- package/README.md +4 -1
- package/bin/sapient-ai.js +424 -61
- package/local-registry/r/button.json +6 -2
- package/local-registry/r/customer-satisfaction.json +1 -1
- package/local-registry/r/multiple-choice-card.json +2 -2
- package/local-registry/r/multiple-choice-grid.json +1 -1
- package/local-registry/r/multiple-choice-list.json +1 -1
- package/local-registry/r/news-card.json +1 -1
- package/local-registry/r/privacy-consent.json +1 -1
- package/local-registry/r/product-card.json +1 -1
- package/local-registry/r/profile-card.json +1 -1
- package/local-registry/r/promo-card.json +1 -1
- package/local-registry/r/video-card.json +2 -2
- package/local-registry/scripts/build-registry.mjs +1 -0
- package/local-registry/src/components/ui/sapient-button.tsx +5 -3
- package/local-registry/src/components/ui/sapient-customer-satisfaction.tsx +3 -3
- package/local-registry/src/components/ui/sapient-icon.tsx +44 -3
- package/local-registry/src/components/ui/sapient-multiple-choice-card.tsx +6 -6
- package/local-registry/src/components/ui/sapient-multiple-choice-grid.tsx +3 -3
- package/local-registry/src/components/ui/sapient-multiple-choice-list.tsx +1 -1
- package/local-registry/src/components/ui/sapient-news-card.tsx +9 -9
- package/local-registry/src/components/ui/sapient-privacy-consent.tsx +8 -8
- package/local-registry/src/components/ui/sapient-product-card.tsx +13 -13
- package/local-registry/src/components/ui/sapient-profile-card.tsx +7 -7
- package/local-registry/src/components/ui/sapient-promo-card.tsx +9 -9
- package/local-registry/src/components/ui/sapient-radio-button.tsx +1 -1
- package/local-registry/src/components/ui/sapient-video-card.tsx +3 -3
- package/local-registry/src/components/ui/sapient-video-controller.tsx +3 -3
- 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.
|
|
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(
|
|
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"] =
|
|
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: ${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
}
|