bejamas 0.2.11 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,22 +1,30 @@
1
1
  #!/usr/bin/env node
2
- import { _ as getWorkspaceConfig, b as highlighter, d as parseJsDocMetadata, g as getConfig, o as extractFrontmatter, p as resolveUiRoot, v as spinner, y as logger } from "./utils-Dr0kJLcx.js";
2
+ import { A as renderSemanticIconSvgWithAttributeString, B as normalizeDesignSystemConfig, C as buildUiUrl, D as SEMANTIC_ICON_NAMES, E as ICON_LIBRARY_COLLECTIONS, F as getDocumentLanguage, H as encodePreset, I as getFontPackageName, L as getFontValue, M as RTL_LANGUAGE_VALUES, N as designSystemConfigSchema, O as getSemanticIconNameFromLucideExport, P as getDocumentDirection, R as getHeadingFontValue, S as BASE_COLORS, T as buildRegistryTheme, U as isPresetCode, V as decodePreset, W as fonts, _ as getConfig, b as highlighter, d as parseJsDocMetadata, g as spinner, j as DEFAULT_DESIGN_SYSTEM_CONFIG, k as getSemanticIconNameFromLucidePath, o as extractFrontmatter, p as resolveUiRoot, v as getWorkspaceConfig, w as resolveRegistryUrl, x as BEJAMAS_COMPONENTS_SCHEMA_URL, y as logger, z as getStyleId } from "./utils-BCEkpKMO.js";
3
3
  import { Command } from "commander";
4
4
  import { createRequire } from "module";
5
- import { promises } from "fs";
6
- import path from "path";
7
- import fsExtra from "fs-extra";
5
+ import path, { extname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { z } from "zod";
7
+ import path$1 from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { existsSync, promises, readFileSync, readdirSync } from "node:fs";
10
+ import fs from "fs-extra";
11
+ import fg from "fast-glob";
12
+ import fs$1 from "node:fs/promises";
13
+ import { bold, cyan, dim, green, magenta, red, white, yellow } from "kleur/colors";
8
14
  import os from "os";
9
15
  import dotenv from "dotenv";
10
16
  import { detect } from "@antfu/ni";
11
- import { z } from "zod";
12
- import { bold, cyan, dim, green, magenta, red, white, yellow } from "kleur/colors";
13
17
  import { execa } from "execa";
14
18
  import prompts from "prompts";
15
- import fg from "fast-glob";
16
- import path$1, { extname as extname$1, isAbsolute, join as join$1, relative as relative$1, resolve } from "node:path";
17
- import { existsSync as existsSync$1, readFileSync, readdirSync } from "node:fs";
18
- import fs from "node:fs/promises";
19
+ import os$1 from "node:os";
19
20
 
21
+ //#region src/registry/context.ts
22
+ let context = { headers: {} };
23
+ function clearRegistryContext() {
24
+ context.headers = {};
25
+ }
26
+
27
+ //#endregion
20
28
  //#region src/utils/errors.ts
21
29
  const MISSING_DIR_OR_EMPTY_PROJECT = "1";
22
30
 
@@ -24,7 +32,7 @@ const MISSING_DIR_OR_EMPTY_PROJECT = "1";
24
32
  //#region src/preflights/preflight-init.ts
25
33
  async function preFlightInit(options) {
26
34
  const errors = {};
27
- if (!fsExtra.existsSync(options.cwd) || !fsExtra.existsSync(path.resolve(options.cwd, "package.json"))) {
35
+ if (!fs.existsSync(options.cwd) || !fs.existsSync(path$1.resolve(options.cwd, "package.json"))) {
28
36
  errors[MISSING_DIR_OR_EMPTY_PROJECT] = true;
29
37
  return {
30
38
  errors,
@@ -38,10 +46,986 @@ async function preFlightInit(options) {
38
46
  }
39
47
 
40
48
  //#endregion
41
- //#region src/registry/context.ts
42
- let context = { headers: {} };
43
- function clearRegistryContext() {
44
- context.headers = {};
49
+ //#region src/utils/astro-fonts.ts
50
+ const ASTRO_CONFIG_BLOCK_START = "// bejamas:astro-fonts:start";
51
+ const ASTRO_CONFIG_BLOCK_END = "// bejamas:astro-fonts:end";
52
+ const ASTRO_LAYOUT_BLOCK_START = "<!-- bejamas:astro-fonts:start -->";
53
+ const ASTRO_LAYOUT_BLOCK_END = "<!-- bejamas:astro-fonts:end -->";
54
+ const ASTRO_FONT_CONSTANT = "BEJAMAS_ASTRO_FONTS";
55
+ const ASTRO_CONFIG_CANDIDATES = [
56
+ "astro.config.mjs",
57
+ "apps/web/astro.config.mjs",
58
+ "apps/docs/astro.config.mjs"
59
+ ];
60
+ const ASTRO_LAYOUT_CANDIDATES = ["src/layouts/Layout.astro", "apps/web/src/layouts/Layout.astro"];
61
+ const STARLIGHT_HEAD_RELATIVE_PATH = "apps/docs/src/components/Head.astro";
62
+ const ASTRO_FONT_PROVIDERS = ["fontsource", "google"];
63
+ const ASTRO_FONTSOURCE_FONT_NAMES = new Set(["geist", "geist-mono"]);
64
+ const MANAGED_BODY_FONT_CLASSES = new Set(["font-mono", "font-serif"]);
65
+ function compactSource$1(source) {
66
+ return source.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
67
+ }
68
+ function escapeRegExp$1(value) {
69
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
70
+ }
71
+ function upsertImport(source, moduleName, importedName, fallbackNames = []) {
72
+ const importPattern = /* @__PURE__ */ new RegExp(`import\\s*\\{([^}]+)\\}\\s*from\\s*["']${escapeRegExp$1(moduleName)}["'];?`);
73
+ if (importPattern.test(source)) return source.replace(importPattern, (_match, importsSource) => {
74
+ const imports = importsSource.split(",").map((value) => value.trim()).filter(Boolean);
75
+ if (!imports.includes(importedName)) imports.push(importedName);
76
+ for (const name of fallbackNames) if (!imports.includes(name)) imports.unshift(name);
77
+ return `import { ${Array.from(new Set(imports)).join(", ")} } from "${moduleName}";`;
78
+ });
79
+ const importLine = `import { ${Array.from(new Set([...fallbackNames, importedName])).join(", ")} } from "${moduleName}";`;
80
+ const frontmatterEnd = source.indexOf("---", 3);
81
+ if (source.startsWith("---") && frontmatterEnd !== -1) return `${source.slice(0, frontmatterEnd).trimEnd()}\n${importLine}\n${source.slice(frontmatterEnd)}`;
82
+ return `${importLine}\n${source}`;
83
+ }
84
+ function buildAstroConfigFontBlock(fontsToSerialize) {
85
+ return `${ASTRO_CONFIG_BLOCK_START}\n/** @type {NonNullable<import("astro/config").AstroUserConfig["fonts"]>} */\nconst ${ASTRO_FONT_CONSTANT} = [${fontsToSerialize.length === 0 ? "" : `\n${fontsToSerialize.map((font) => ` {\n provider: fontProviders.${font.provider}(),\n name: ${JSON.stringify(font.name)},\n cssVariable: ${JSON.stringify(font.cssVariable)},\n subsets: ${JSON.stringify(font.subsets)},\n },`).join("\n")}\n`}];\n${ASTRO_CONFIG_BLOCK_END}`;
86
+ }
87
+ function buildAstroLayoutFontBlock(fontsToSerialize) {
88
+ return `${ASTRO_LAYOUT_BLOCK_START}\n${fontsToSerialize.map((font) => `<Font cssVariable="${font.cssVariable}" />`).join("\n")}\n${ASTRO_LAYOUT_BLOCK_END}`;
89
+ }
90
+ function bodyFontClassFromCssVariable(cssVariable) {
91
+ if (cssVariable === "--font-mono" || cssVariable === "--font-serif") return fontClassFromCssVariable(cssVariable);
92
+ return "";
93
+ }
94
+ function upsertManagedBodyFontClass(source, cssVariable) {
95
+ const bodyClass = bodyFontClassFromCssVariable(cssVariable);
96
+ const bodyTagPattern = /<body\b([^>]*?)(\/?)>/m;
97
+ if (!bodyTagPattern.test(source)) return source;
98
+ return source.replace(bodyTagPattern, (_match, attrs, selfClosing) => {
99
+ const classPattern = /\sclass=(["'])(.*?)\1/m;
100
+ const classMatch = attrs.match(classPattern);
101
+ const quote = classMatch?.[1] ?? "\"";
102
+ const existingClasses = (classMatch?.[2] ?? "").split(/\s+/).filter(Boolean).filter((className) => !MANAGED_BODY_FONT_CLASSES.has(className));
103
+ const nextClasses = bodyClass ? Array.from(new Set([...existingClasses, bodyClass])) : existingClasses;
104
+ let nextAttrs = attrs.replace(classPattern, "");
105
+ if (nextClasses.length > 0) nextAttrs = `${nextAttrs} class=${quote}${nextClasses.join(" ")}${quote}`;
106
+ return `<body${nextAttrs}${selfClosing}>`;
107
+ });
108
+ }
109
+ function parseManagedAstroFonts(source) {
110
+ const markerPattern = /* @__PURE__ */ new RegExp(`${escapeRegExp$1(ASTRO_CONFIG_BLOCK_START)}[\\s\\S]*?const\\s+${ASTRO_FONT_CONSTANT}\\s*=\\s*\\[([\\s\\S]*?)\\];[\\s\\S]*?${escapeRegExp$1(ASTRO_CONFIG_BLOCK_END)}`);
111
+ const match = source.match(markerPattern);
112
+ if (!match) return [];
113
+ return (match[1].match(/\{[\s\S]*?\}/g) ?? []).map((entry) => {
114
+ const name = entry.match(/name:\s*"([^"]+)"/)?.[1];
115
+ const cssVariable = entry.match(/cssVariable:\s*"([^"]+)"/)?.[1];
116
+ const provider = entry.match(/provider:\s*fontProviders\.([a-z]+)\(\)/)?.[1];
117
+ const subsetsSource = entry.match(/subsets:\s*(\[[^\]]*\])/s)?.[1];
118
+ if (!name || !cssVariable || !provider || !ASTRO_FONT_PROVIDERS.includes(provider)) return null;
119
+ let subsets = ["latin"];
120
+ if (subsetsSource) try {
121
+ const parsed = JSON.parse(subsetsSource);
122
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((value) => typeof value === "string")) subsets = parsed;
123
+ } catch {
124
+ subsets = ["latin"];
125
+ }
126
+ return {
127
+ name,
128
+ cssVariable,
129
+ provider,
130
+ subsets
131
+ };
132
+ }).filter((value) => value !== null);
133
+ }
134
+ function normalizeManagedAstroFontSubsets(subsets) {
135
+ if (!subsets?.length) return ["latin"];
136
+ return [subsets[0], ...subsets.slice(1)];
137
+ }
138
+ function replaceManagedBlock(source, nextBlock, start, end) {
139
+ const blockPattern = new RegExp(`${escapeRegExp$1(start)}[\\s\\S]*?${escapeRegExp$1(end)}`, "m");
140
+ if (blockPattern.test(source)) return source.replace(blockPattern, nextBlock);
141
+ return null;
142
+ }
143
+ function stripExperimentalFontsProperty(source) {
144
+ const experimentalMatch = source.match(/experimental\s*:\s*\{/);
145
+ if (!experimentalMatch || experimentalMatch.index === void 0) return source;
146
+ const propertyStart = experimentalMatch.index;
147
+ const openingBraceIndex = propertyStart + experimentalMatch[0].lastIndexOf("{");
148
+ let depth = 0;
149
+ let closingBraceIndex = -1;
150
+ let quote = null;
151
+ for (let index = openingBraceIndex; index < source.length; index += 1) {
152
+ const character = source[index];
153
+ const previousCharacter = index > 0 ? source[index - 1] : "";
154
+ if (quote) {
155
+ if (character === quote && previousCharacter !== "\\") quote = null;
156
+ continue;
157
+ }
158
+ if (character === "\"" || character === "'" || character === "`") {
159
+ quote = character;
160
+ continue;
161
+ }
162
+ if (character === "{") {
163
+ depth += 1;
164
+ continue;
165
+ }
166
+ if (character === "}") {
167
+ depth -= 1;
168
+ if (depth === 0) {
169
+ closingBraceIndex = index;
170
+ break;
171
+ }
172
+ }
173
+ }
174
+ if (closingBraceIndex === -1) return source;
175
+ const nextBlockContent = source.slice(openingBraceIndex + 1, closingBraceIndex).replace(/(^|\n)\s*fonts\s*:\s*BEJAMAS_ASTRO_FONTS\s*,?\s*(?=\n|$)/g, "$1").replace(/\n{3,}/g, "\n\n");
176
+ let propertyEnd = closingBraceIndex + 1;
177
+ while (propertyEnd < source.length && /\s/.test(source[propertyEnd])) propertyEnd += 1;
178
+ if (source[propertyEnd] === ",") propertyEnd += 1;
179
+ if (nextBlockContent.trim().length === 0) return `${source.slice(0, propertyStart)}${source.slice(propertyEnd)}`;
180
+ const rebuiltProperty = `experimental: {${nextBlockContent}\n }`;
181
+ return `${source.slice(0, propertyStart)}${rebuiltProperty}${source.slice(closingBraceIndex + 1)}`;
182
+ }
183
+ function patchAstroConfigSource(source, fontsToSerialize, options = {}) {
184
+ let next = upsertImport(source, "astro/config", "fontProviders", ["defineConfig"]);
185
+ const fontBlock = buildAstroConfigFontBlock(fontsToSerialize);
186
+ const replacedBlock = replaceManagedBlock(next, fontBlock, ASTRO_CONFIG_BLOCK_START, ASTRO_CONFIG_BLOCK_END);
187
+ if (replacedBlock) next = replacedBlock;
188
+ else {
189
+ const lastImport = Array.from(next.matchAll(/^import .*$/gm)).at(-1);
190
+ if (lastImport?.index !== void 0) {
191
+ const insertAt = lastImport.index + lastImport[0].length;
192
+ next = `${next.slice(0, insertAt)}\n\n${fontBlock}${next.slice(insertAt)}`;
193
+ } else next = `${fontBlock}\n\n${next}`;
194
+ }
195
+ next = stripExperimentalFontsProperty(next);
196
+ if (!/^\s*fonts:\s*BEJAMAS_ASTRO_FONTS\b/m.test(next)) next = next.replace(/defineConfig\(\s*\{/, `defineConfig({\n fonts: ${ASTRO_FONT_CONSTANT},`);
197
+ if (options.starlightHeadOverride) {
198
+ if (next.includes("Head: \"./src/components/Head.astro\"")) return compactSource$1(next);
199
+ const starlightComponentsPattern = /starlight\(\{\s*([\s\S]*?)components:\s*\{([\s\S]*?)\}/m;
200
+ if (starlightComponentsPattern.test(next)) next = next.replace(starlightComponentsPattern, (_match, before, inner) => `starlight({${before}components: {\n Head: "./src/components/Head.astro",${inner ? `${inner}` : ""}\n }`);
201
+ else next = next.replace(/starlight\(\s*\{/, `starlight({\n components: {\n Head: "./src/components/Head.astro",\n },`);
202
+ }
203
+ return compactSource$1(next);
204
+ }
205
+ function patchAstroLayoutSource(source, fontsToSerialize, activeFontCssVariable) {
206
+ let next = upsertImport(source, "astro:assets", "Font");
207
+ const fontBlock = buildAstroLayoutFontBlock(fontsToSerialize);
208
+ const replacedBlock = replaceManagedBlock(next, fontBlock, ASTRO_LAYOUT_BLOCK_START, ASTRO_LAYOUT_BLOCK_END);
209
+ if (replacedBlock) next = replacedBlock;
210
+ else if (next.includes("<head>")) next = next.replace("<head>", `<head>\n ${fontBlock.replace(/\n/g, "\n ")}`);
211
+ else if (next.includes("</head>")) next = next.replace("</head>", ` ${fontBlock.replace(/\n/g, "\n ")}\n</head>`);
212
+ else next = `${next}\n${fontBlock}\n`;
213
+ next = upsertManagedBodyFontClass(next, activeFontCssVariable);
214
+ return compactSource$1(next);
215
+ }
216
+ function patchStarlightHeadSource(source, fontsToSerialize) {
217
+ let next = upsertImport(source, "@astrojs/starlight/components/Head.astro", "DefaultHead");
218
+ next = upsertImport(next, "astro:assets", "Font");
219
+ const fontBlock = buildAstroLayoutFontBlock(fontsToSerialize);
220
+ const replacedBlock = replaceManagedBlock(next, fontBlock, ASTRO_LAYOUT_BLOCK_START, ASTRO_LAYOUT_BLOCK_END);
221
+ if (replacedBlock) next = replacedBlock;
222
+ else if (next.includes("<DefaultHead")) next = next.replace(/<DefaultHead[^>]*\/>/, (match) => `${match}\n${fontBlock}`);
223
+ else next = `${next.trimEnd()}\n\n<DefaultHead />\n${fontBlock}\n`;
224
+ return compactSource$1(next);
225
+ }
226
+ function toManagedAstroFont(fontName) {
227
+ const normalized = fontName.replace(/^font-heading-/, "").replace(/^font-/, "");
228
+ const font = fontName.startsWith("font-heading-") ? getHeadingFontValue(normalized) : getFontValue(normalized);
229
+ if (!font || font.type !== "registry:font" || !ASTRO_FONT_PROVIDERS.includes(font.font.provider)) return null;
230
+ return {
231
+ name: font.title?.replace(/\s+\(Heading\)$/, "") ?? font.font.import.replace(/_/g, " "),
232
+ cssVariable: font.font.variable,
233
+ provider: ASTRO_FONTSOURCE_FONT_NAMES.has(normalized) ? "fontsource" : "google",
234
+ subsets: normalizeManagedAstroFontSubsets(font.font.subsets)
235
+ };
236
+ }
237
+ function mergeManagedAstroFonts(currentFonts, nextFont) {
238
+ return [...currentFonts.filter((font) => font.cssVariable !== nextFont.cssVariable), nextFont].sort((left, right) => left.cssVariable.localeCompare(right.cssVariable));
239
+ }
240
+ function fontClassFromCssVariable(cssVariable) {
241
+ return `font-${cssVariable.replace(/^--font-/, "")}`;
242
+ }
243
+ async function patchFileIfExists(filePath, transformer) {
244
+ if (!await fs.pathExists(filePath)) return;
245
+ const current = await fs.readFile(filePath, "utf8");
246
+ const next = transformer(current);
247
+ if (next !== current) await fs.writeFile(filePath, next, "utf8");
248
+ }
249
+ async function collectPackageJsonsFromComponents(projectPath) {
250
+ const componentJsonFiles = await fg("**/components.json", {
251
+ cwd: projectPath,
252
+ ignore: [
253
+ "**/node_modules/**",
254
+ "**/dist/**",
255
+ "**/.astro/**"
256
+ ]
257
+ });
258
+ const packageJsonFiles = /* @__PURE__ */ new Set();
259
+ for (const relativePath of componentJsonFiles) {
260
+ const absolutePath = path.resolve(projectPath, relativePath);
261
+ const cssPath = (await fs.readJson(absolutePath))?.tailwind?.css;
262
+ if (typeof cssPath !== "string" || cssPath.length === 0) continue;
263
+ let currentDir = path.dirname(path.resolve(path.dirname(absolutePath), cssPath));
264
+ while (true) {
265
+ const packageJsonPath = path.resolve(currentDir, "package.json");
266
+ if (await fs.pathExists(packageJsonPath)) {
267
+ packageJsonFiles.add(packageJsonPath);
268
+ break;
269
+ }
270
+ const parentDir = path.dirname(currentDir);
271
+ if (parentDir === currentDir) break;
272
+ currentDir = parentDir;
273
+ }
274
+ }
275
+ return packageJsonFiles;
276
+ }
277
+ async function upsertStarlightHeadFile(projectPath, fontsToSerialize) {
278
+ const headPath = path.resolve(projectPath, STARLIGHT_HEAD_RELATIVE_PATH);
279
+ const next = patchStarlightHeadSource(await fs.pathExists(headPath) ? await fs.readFile(headPath, "utf8") : "---\nimport DefaultHead from \"@astrojs/starlight/components/Head.astro\";\nimport { Font } from \"astro:assets\";\n---\n\n<DefaultHead />\n", fontsToSerialize);
280
+ await fs.ensureDir(path.dirname(headPath));
281
+ await fs.writeFile(headPath, next, "utf8");
282
+ }
283
+ async function readManagedAstroFontsFromProject(projectPath) {
284
+ for (const relativePath of ASTRO_CONFIG_CANDIDATES) {
285
+ const absolutePath = path.resolve(projectPath, relativePath);
286
+ if (!await fs.pathExists(absolutePath)) continue;
287
+ const parsed = parseManagedAstroFonts(await fs.readFile(absolutePath, "utf8"));
288
+ if (parsed.length > 0) return parsed;
289
+ }
290
+ return [];
291
+ }
292
+ async function cleanupAstroFontPackages(projectPath) {
293
+ const managedPackages = new Set(fonts.filter((font) => font.type === "registry:font").map((font) => getFontPackageName(font.name.replace(/^font-heading-/, "").replace(/^font-/, ""))));
294
+ const packageJsonFiles = await collectPackageJsonsFromComponents(projectPath);
295
+ await Promise.all(Array.from(packageJsonFiles).map(async (absolutePath) => {
296
+ const current = await fs.readJson(absolutePath);
297
+ let changed = false;
298
+ for (const bucket of ["dependencies", "devDependencies"]) {
299
+ const currentBucket = current[bucket];
300
+ if (!currentBucket || typeof currentBucket !== "object") continue;
301
+ for (const packageName of managedPackages) if (packageName in currentBucket) {
302
+ delete currentBucket[packageName];
303
+ changed = true;
304
+ }
305
+ if (Object.keys(currentBucket).length === 0) {
306
+ delete current[bucket];
307
+ changed = true;
308
+ }
309
+ }
310
+ if (changed) await fs.writeJson(absolutePath, current, { spaces: 2 });
311
+ }));
312
+ }
313
+ async function syncAstroFontsInProject(projectPath, fontsToSerialize, activeFontCssVariable) {
314
+ await Promise.all(ASTRO_CONFIG_CANDIDATES.map((relativePath) => patchFileIfExists(path.resolve(projectPath, relativePath), (source) => patchAstroConfigSource(source, fontsToSerialize, { starlightHeadOverride: relativePath === "apps/docs/astro.config.mjs" }))));
315
+ await Promise.all(ASTRO_LAYOUT_CANDIDATES.map((relativePath) => patchFileIfExists(path.resolve(projectPath, relativePath), (source) => patchAstroLayoutSource(source, fontsToSerialize, activeFontCssVariable))));
316
+ const docsConfigPath = path.resolve(projectPath, "apps/docs/astro.config.mjs");
317
+ if (await fs.pathExists(docsConfigPath)) await upsertStarlightHeadFile(projectPath, fontsToSerialize);
318
+ }
319
+
320
+ //#endregion
321
+ //#region src/utils/apply-design-system.ts
322
+ const CREATE_BLOCK_START = "/* bejamas:create:start */";
323
+ const CREATE_BLOCK_END = "/* bejamas:create:end */";
324
+ const SHADCN_TAILWIND_IMPORT = "@import \"shadcn/tailwind.css\";";
325
+ const BEJAMAS_TAILWIND_IMPORT = "@import \"bejamas/tailwind.css\";";
326
+ const MANAGED_TAILWIND_IMPORTS = new Set([SHADCN_TAILWIND_IMPORT, BEJAMAS_TAILWIND_IMPORT]);
327
+ const TEMPLATE_APP_UI_IMPORT = "import { appUi } from \"@/i18n/ui\";";
328
+ const TEMPLATE_I18N_SOURCES = {
329
+ astro: `const RTL_LANGUAGES = ["ar", "fa", "he"] as const;
330
+
331
+ export type TemplateLanguage = "en" | (typeof RTL_LANGUAGES)[number];
332
+
333
+ export const CURRENT_LANGUAGE: TemplateLanguage = "en";
334
+
335
+ const ui = {
336
+ en: {
337
+ metadataTitle: "bejamas/ui Astro project",
338
+ welcomeMessage: "Welcome to {project} Astro project.",
339
+ getStartedMessage: "Get started by editing src/pages/index.astro.",
340
+ readDocs: "Read docs",
341
+ getCustomDemo: "Get custom demo",
342
+ },
343
+ ar: {
344
+ metadataTitle: "مشروع Astro من bejamas/ui",
345
+ welcomeMessage: "مرحبًا بك في مشروع Astro الخاص بـ {project}.",
346
+ getStartedMessage: "ابدأ بتحرير src/pages/index.astro.",
347
+ readDocs: "اقرأ التوثيق",
348
+ getCustomDemo: "احصل على عرض توضيحي مخصص",
349
+ },
350
+ fa: {
351
+ metadataTitle: "پروژه Astro با bejamas/ui",
352
+ welcomeMessage: "به پروژه Astro با {project} خوش آمدید.",
353
+ getStartedMessage: "برای شروع src/pages/index.astro را ویرایش کنید.",
354
+ readDocs: "مطالعه مستندات",
355
+ getCustomDemo: "درخواست دموی سفارشی",
356
+ },
357
+ he: {
358
+ metadataTitle: "פרויקט Astro עם bejamas/ui",
359
+ welcomeMessage: "ברוכים הבאים לפרויקט Astro של {project}.",
360
+ getStartedMessage: "התחילו בעריכת src/pages/index.astro.",
361
+ readDocs: "קריאת התיעוד",
362
+ getCustomDemo: "קבלת דמו מותאם",
363
+ },
364
+ } as const satisfies Record<TemplateLanguage, Record<string, string>>;
365
+
366
+ export const appUi = {
367
+ lang: CURRENT_LANGUAGE,
368
+ dir: RTL_LANGUAGES.includes(
369
+ CURRENT_LANGUAGE as (typeof RTL_LANGUAGES)[number],
370
+ )
371
+ ? "rtl"
372
+ : "ltr",
373
+ ...ui[CURRENT_LANGUAGE],
374
+ };
375
+ `,
376
+ monorepo: `const RTL_LANGUAGES = ["ar", "fa", "he"] as const;
377
+
378
+ export type TemplateLanguage = "en" | (typeof RTL_LANGUAGES)[number];
379
+
380
+ export const CURRENT_LANGUAGE: TemplateLanguage = "en";
381
+
382
+ const ui = {
383
+ en: {
384
+ metadataTitle: "bejamas/ui Astro project",
385
+ welcomeMessage: "Welcome to {project} Astro monorepo.",
386
+ getStartedMessage: "Get started by editing apps/web/src/pages/index.astro.",
387
+ readDocs: "Read docs",
388
+ getCustomDemo: "Get custom demo",
389
+ },
390
+ ar: {
391
+ metadataTitle: "مشروع Astro من bejamas/ui",
392
+ welcomeMessage: "مرحبًا بك في مشروع Astro الأحادي من {project}.",
393
+ getStartedMessage: "ابدأ بتحرير apps/web/src/pages/index.astro.",
394
+ readDocs: "اقرأ التوثيق",
395
+ getCustomDemo: "احصل على عرض توضيحي مخصص",
396
+ },
397
+ fa: {
398
+ metadataTitle: "پروژه Astro با bejamas/ui",
399
+ welcomeMessage: "به مونوریپوی Astro با {project} خوش آمدید.",
400
+ getStartedMessage:
401
+ "برای شروع apps/web/src/pages/index.astro را ویرایش کنید.",
402
+ readDocs: "مطالعه مستندات",
403
+ getCustomDemo: "درخواست دموی سفارشی",
404
+ },
405
+ he: {
406
+ metadataTitle: "פרויקט Astro עם bejamas/ui",
407
+ welcomeMessage: "ברוכים הבאים למונורפו Astro של {project}.",
408
+ getStartedMessage: "התחילו בעריכת apps/web/src/pages/index.astro.",
409
+ readDocs: "קריאת התיעוד",
410
+ getCustomDemo: "קבלת דמו מותאם",
411
+ },
412
+ } as const satisfies Record<TemplateLanguage, Record<string, string>>;
413
+
414
+ export const appUi = {
415
+ lang: CURRENT_LANGUAGE,
416
+ dir: RTL_LANGUAGES.includes(
417
+ CURRENT_LANGUAGE as (typeof RTL_LANGUAGES)[number],
418
+ )
419
+ ? "rtl"
420
+ : "ltr",
421
+ ...ui[CURRENT_LANGUAGE],
422
+ };
423
+ `
424
+ };
425
+ const TEMPLATE_PAGE_VARIANTS = [
426
+ {
427
+ matchers: [
428
+ "import { Button } from \"@/ui/button\";",
429
+ "Astro project.",
430
+ "Get started by editing src/pages/index.astro."
431
+ ],
432
+ i18nSource: `---
433
+ import Layout from "@/layouts/Layout.astro";
434
+ import { appUi } from "@/i18n/ui";
435
+ import { Button } from "@/ui/button";
436
+
437
+ const [welcomePrefix, welcomeSuffix = ""] =
438
+ appUi.welcomeMessage.split("{project}");
439
+ ---
440
+
441
+ <Layout>
442
+ <div
443
+ class="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
444
+ >
445
+ <div class="flex flex-col gap-6 row-start-2 items-center sm:items-start">
446
+ <img src="/bejamas.svg" alt="bejamas/ui" class="w-8 dark:invert" />
447
+ <div class="space-y-2">
448
+ <p class="text-sm">
449
+ {welcomePrefix}<code
450
+ class="font-bold relative bg-muted rounded p-1"
451
+ >@bejamas/ui</code
452
+ >{welcomeSuffix}
453
+ </p>
454
+ <p class="text-sm">
455
+ {appUi.getStartedMessage}
456
+ </p>
457
+ </div>
458
+ <div class="flex gap-2">
459
+ <Button as="a" href="https://ui.bejamas.com/docs"
460
+ >{appUi.readDocs}</Button
461
+ >
462
+ <Button as="a" href="https://bejamas.com/get-in-touch" variant="outline"
463
+ >{appUi.getCustomDemo}</Button
464
+ >
465
+ </div>
466
+ </div>
467
+ </div>
468
+ </Layout>
469
+ `
470
+ },
471
+ {
472
+ matchers: [
473
+ "import { Button } from \"@repo/ui/components/button\";",
474
+ "width=\"32\"",
475
+ "Astro monorepo.",
476
+ "Get started by editing apps/web/src/pages/index.astro."
477
+ ],
478
+ i18nSource: `---
479
+ import Layout from "@/layouts/Layout.astro";
480
+ import { appUi } from "@/i18n/ui";
481
+ import { Button } from "@repo/ui/components/button";
482
+
483
+ const [welcomePrefix, welcomeSuffix = ""] =
484
+ appUi.welcomeMessage.split("{project}");
485
+ ---
486
+
487
+ <Layout>
488
+ <div
489
+ class="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
490
+ >
491
+ <div class="flex flex-col gap-6 row-start-2 items-center sm:items-start">
492
+ <img
493
+ src="/bejamas.svg"
494
+ alt="bejamas/ui"
495
+ class="w-8 dark:invert"
496
+ width="32"
497
+ height="32"
498
+ alt="bejamas/ui logo"
499
+ />
500
+ <div class="space-y-2">
501
+ <p class="text-sm">
502
+ {welcomePrefix}<code
503
+ class="font-bold relative bg-muted rounded p-1"
504
+ >@bejamas/ui</code
505
+ >{welcomeSuffix}
506
+ </p>
507
+ <p class="text-sm">
508
+ {appUi.getStartedMessage}
509
+ </p>
510
+ </div>
511
+ <div class="flex gap-2">
512
+ <Button as="a" href="https://ui.bejamas.com/docs"
513
+ >{appUi.readDocs}</Button
514
+ >
515
+ <Button as="a" href="https://bejamas.com/get-in-touch" variant="outline"
516
+ >{appUi.getCustomDemo}</Button
517
+ >
518
+ </div>
519
+ </div>
520
+ </div>
521
+ </Layout>
522
+ `
523
+ },
524
+ {
525
+ matchers: [
526
+ "import { Button } from \"@repo/ui/components/button\";",
527
+ "<img src=\"/bejamas.svg\" alt=\"bejamas/ui\" class=\"w-8 dark:invert\" />",
528
+ "Astro monorepo.",
529
+ "Get started by editing apps/web/src/pages/index.astro."
530
+ ],
531
+ i18nSource: `---
532
+ import Layout from "@/layouts/Layout.astro";
533
+ import { appUi } from "@/i18n/ui";
534
+ import { Button } from "@repo/ui/components/button";
535
+
536
+ const [welcomePrefix, welcomeSuffix = ""] =
537
+ appUi.welcomeMessage.split("{project}");
538
+ ---
539
+
540
+ <Layout>
541
+ <div
542
+ class="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
543
+ >
544
+ <div class="flex flex-col gap-6 row-start-2 items-center sm:items-start">
545
+ <img src="/bejamas.svg" alt="bejamas/ui" class="w-8 dark:invert" />
546
+ <div class="space-y-2">
547
+ <p class="text-sm">
548
+ {welcomePrefix}<code
549
+ class="font-bold relative bg-muted rounded p-1"
550
+ >@bejamas/ui</code
551
+ >{welcomeSuffix}
552
+ </p>
553
+ <p class="text-sm">
554
+ {appUi.getStartedMessage}
555
+ </p>
556
+ </div>
557
+ <div class="flex gap-2">
558
+ <Button as="a" href="https://ui.bejamas.com/docs"
559
+ >{appUi.readDocs}</Button
560
+ >
561
+ <Button as="a" href="https://bejamas.com/get-in-touch" variant="outline"
562
+ >{appUi.getCustomDemo}</Button
563
+ >
564
+ </div>
565
+ </div>
566
+ </div>
567
+ </Layout>
568
+ `
569
+ }
570
+ ];
571
+ function escapeRegExp(value) {
572
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
573
+ }
574
+ function compactCss(source) {
575
+ return source.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
576
+ }
577
+ function compactSource(source) {
578
+ return source.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
579
+ }
580
+ function stripLegacyCreateBlock(source) {
581
+ const fullBlockPattern = new RegExp(`\\n*${escapeRegExp(CREATE_BLOCK_START)}[\\s\\S]*?${escapeRegExp(CREATE_BLOCK_END)}\\n*`, "m");
582
+ const danglingBlockPattern = new RegExp(`\\n*${escapeRegExp(CREATE_BLOCK_START)}[\\s\\S]*$`, "m");
583
+ return compactCss(source.replace(fullBlockPattern, "\n\n").replace(danglingBlockPattern, "\n\n"));
584
+ }
585
+ function buildCssVarBlock(selector, vars) {
586
+ return [
587
+ `${selector} {`,
588
+ ...Object.entries(vars).map(([key, value]) => ` --${key}: ${value};`),
589
+ "}"
590
+ ].join("\n");
591
+ }
592
+ function replaceTopLevelBlock(source, selector, nextBlock) {
593
+ const pattern = new RegExp(`^${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?^\\}`, "m");
594
+ if (pattern.test(source)) return source.replace(pattern, nextBlock);
595
+ const layerIndex = source.indexOf("@layer base");
596
+ if (layerIndex !== -1) return `${source.slice(0, layerIndex).trimEnd()}\n\n${nextBlock}\n\n${source.slice(layerIndex).trimStart()}`;
597
+ return `${source.trimEnd()}\n\n${nextBlock}\n`;
598
+ }
599
+ function upsertImports(source, imports) {
600
+ const cleanedLines = source.split("\n").filter((line) => {
601
+ const trimmed = line.trim();
602
+ if (MANAGED_TAILWIND_IMPORTS.has(trimmed)) return false;
603
+ return !trimmed.startsWith("@import \"@fontsource-variable/");
604
+ });
605
+ let insertAt = -1;
606
+ for (let index = 0; index < cleanedLines.length; index += 1) {
607
+ if (cleanedLines[index].trim().startsWith("@import ")) {
608
+ insertAt = index;
609
+ continue;
610
+ }
611
+ if (insertAt !== -1 && cleanedLines[index].trim() !== "") break;
612
+ }
613
+ const uniqueImports = imports.filter((line, index) => imports.indexOf(line) === index);
614
+ if (insertAt === -1) return compactCss([
615
+ ...uniqueImports,
616
+ "",
617
+ ...cleanedLines
618
+ ].join("\n"));
619
+ cleanedLines.splice(insertAt + 1, 0, ...uniqueImports);
620
+ return compactCss(cleanedLines.join("\n"));
621
+ }
622
+ function upsertManagedTailwindImport(source) {
623
+ const nextImport = resolveManagedTailwindImport(source);
624
+ const cleanedLines = source.split("\n").filter((line) => !MANAGED_TAILWIND_IMPORTS.has(line.trim()));
625
+ let insertAt = -1;
626
+ for (let index = 0; index < cleanedLines.length; index += 1) {
627
+ if (cleanedLines[index].trim().startsWith("@import ")) {
628
+ insertAt = index;
629
+ continue;
630
+ }
631
+ if (insertAt !== -1 && cleanedLines[index].trim() !== "") break;
632
+ }
633
+ if (insertAt === -1) return compactCss([
634
+ nextImport,
635
+ "",
636
+ ...cleanedLines
637
+ ].join("\n"));
638
+ cleanedLines.splice(insertAt + 1, 0, nextImport);
639
+ return compactCss(cleanedLines.join("\n"));
640
+ }
641
+ function resolveManagedTailwindImport(source) {
642
+ if (source.includes(SHADCN_TAILWIND_IMPORT)) return SHADCN_TAILWIND_IMPORT;
643
+ if (source.includes(BEJAMAS_TAILWIND_IMPORT)) return BEJAMAS_TAILWIND_IMPORT;
644
+ return BEJAMAS_TAILWIND_IMPORT;
645
+ }
646
+ function normalizeSourceForComparison(source) {
647
+ return source.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
648
+ }
649
+ function upsertFrontmatterImport(source, importLine) {
650
+ if (source.includes(importLine)) return source;
651
+ const frontmatterEnd = source.indexOf("\n---", 3);
652
+ if (source.startsWith("---\n") && frontmatterEnd !== -1) return `${source.slice(0, frontmatterEnd).trimEnd()}\n${importLine}${source.slice(frontmatterEnd)}`;
653
+ return `${importLine}\n${source}`;
654
+ }
655
+ function getTemplateI18nVariant(filepath) {
656
+ return filepath.includes(`${path.sep}apps${path.sep}web${path.sep}`) ? "monorepo" : "astro";
657
+ }
658
+ function buildTemplateI18nSource(variant, language) {
659
+ return compactSource(TEMPLATE_I18N_SOURCES[variant].replace("export const CURRENT_LANGUAGE: TemplateLanguage = \"en\";", `export const CURRENT_LANGUAGE: TemplateLanguage = "${language}";`));
660
+ }
661
+ function transformTemplateLayoutToI18n(source) {
662
+ if (source.includes(TEMPLATE_APP_UI_IMPORT)) return source;
663
+ let next = upsertFrontmatterImport(source, TEMPLATE_APP_UI_IMPORT);
664
+ next = next.replace(/<html([^>]*?)\slang=(\"[^\"]*\"|\{[^}]+\})([^>]*)>/, "<html$1$3>").replace(/<html([^>]*?)\sdir=(\"[^\"]*\"|\{[^}]+\})([^>]*)>/, "<html$1$3>");
665
+ next = next.replace(/<html([^>]*)>/, "<html$1 lang={appUi.lang} dir={appUi.dir}>");
666
+ next = next.replace(/<title>[\s\S]*?<\/title>/, "<title>{appUi.metadataTitle}</title>");
667
+ return compactSource(next);
668
+ }
669
+ function transformStarterPageToI18n(source) {
670
+ if (source.includes(TEMPLATE_APP_UI_IMPORT)) return source;
671
+ const normalized = normalizeSourceForComparison(source);
672
+ const match = TEMPLATE_PAGE_VARIANTS.find((variant) => variant.matchers.every((matcher) => normalized.includes(normalizeSourceForComparison(matcher))));
673
+ if (!match) return source;
674
+ return compactSource(match.i18nSource);
675
+ }
676
+ function upsertThemeInlineFont(source, fontVariable) {
677
+ const pattern = /@theme inline\s*\{[\s\S]*?\n\}/m;
678
+ if (!pattern.test(source)) {
679
+ const block = `@theme inline {\n${[fontVariable ? ` ${fontVariable}: var(${fontVariable});` : null, " --font-heading: var(--font-heading);"].filter(Boolean).join("\n")}\n}`;
680
+ const customVariantIndex = source.indexOf("@custom-variant");
681
+ if (customVariantIndex !== -1) {
682
+ const nextLineIndex = source.indexOf("\n", customVariantIndex);
683
+ return `${source.slice(0, nextLineIndex + 1).trimEnd()}\n\n${block}\n\n${source.slice(nextLineIndex + 1).trimStart()}`;
684
+ }
685
+ return `${block}\n\n${source.trimStart()}`;
686
+ }
687
+ return source.replace(pattern, (block) => {
688
+ const withoutManagedFonts = block.replace(/\n\s*--font-(sans|serif|mono|heading):[^\n]+/g, "").replace(/\n\s*--bejamas-font-family:[^\n]+/g, "").replace(/@theme inline\s*\{\n?/, "@theme inline {\n");
689
+ const declarations = [fontVariable ? ` ${fontVariable}: var(${fontVariable});` : null, " --font-heading: var(--font-heading);"].filter(Boolean).join("\n");
690
+ return withoutManagedFonts.replace("@theme inline {\n", `@theme inline {\n${declarations}\n`);
691
+ });
692
+ }
693
+ function upsertBaseLayerHtmlFont(source, fontClass) {
694
+ const pattern = /@layer base\s*\{[\s\S]*?\n\}/m;
695
+ if (!pattern.test(source)) return `${source.trimEnd()}\n\n@layer base {\n .cn-font-heading {\n @apply font-heading;\n }\n html {\n @apply ${fontClass};\n }\n}\n`;
696
+ return source.replace(pattern, (block) => {
697
+ return block.replace(/\n\s*html\s*\{[\s\S]*?\n\s*\}/m, "").replace(/\n\s*\.cn-font-heading\s*\{[\s\S]*?\n\s*\}/m, "").replace(/\n\}$/, `\n .cn-font-heading {\n @apply font-heading;\n }\n html {\n @apply ${fontClass};\n }\n}`);
698
+ });
699
+ }
700
+ function transformDesignSystemCss(source, config, themeVars) {
701
+ const effectiveThemeVars = themeVars ?? buildRegistryTheme(config).cssVars;
702
+ const rootVars = {
703
+ ...Object.fromEntries(Object.entries(effectiveThemeVars.theme ?? {}).filter(([key]) => key !== "bejamas-font-family")),
704
+ ...effectiveThemeVars.light ?? {}
705
+ };
706
+ const darkVars = { ...effectiveThemeVars.dark ?? {} };
707
+ const font = getFontValue(config.font);
708
+ const tailwindImport = resolveManagedTailwindImport(source);
709
+ let next = stripLegacyCreateBlock(source);
710
+ next = upsertImports(next, [tailwindImport]);
711
+ next = replaceTopLevelBlock(next, ":root", buildCssVarBlock(":root", rootVars));
712
+ next = replaceTopLevelBlock(next, ".dark", buildCssVarBlock(".dark", darkVars));
713
+ next = upsertThemeInlineFont(next, font?.font.variable);
714
+ if (font) next = upsertBaseLayerHtmlFont(next, fontClassFromCssVariable(font.font.variable));
715
+ return compactCss(next);
716
+ }
717
+ function transformAstroManagedFontCss(source, fontVariable) {
718
+ const tailwindImport = resolveManagedTailwindImport(source);
719
+ let next = stripLegacyCreateBlock(source);
720
+ next = upsertImports(next, [tailwindImport]);
721
+ next = upsertThemeInlineFont(next, fontVariable);
722
+ if (fontVariable) next = upsertBaseLayerHtmlFont(next, fontClassFromCssVariable(fontVariable));
723
+ return compactCss(next);
724
+ }
725
+ function transformManagedTailwindImportCss(source) {
726
+ return upsertManagedTailwindImport(source);
727
+ }
728
+ async function patchComponentsJson(filepath, config) {
729
+ const current = await fs.readJson(filepath);
730
+ const { rsc: _rsc, tsx: _tsx,...normalizedCurrent } = current;
731
+ const next = {
732
+ ...normalizedCurrent,
733
+ $schema: BEJAMAS_COMPONENTS_SCHEMA_URL,
734
+ style: getStyleId(config.style),
735
+ iconLibrary: config.iconLibrary,
736
+ rtl: config.rtl,
737
+ tailwind: {
738
+ ...current.tailwind ?? {},
739
+ baseColor: config.baseColor,
740
+ cssVariables: true
741
+ }
742
+ };
743
+ await fs.writeJson(filepath, next, { spaces: 2 });
744
+ }
745
+ async function patchCssFileWithTheme(filepath, config, themeVars) {
746
+ const next = transformDesignSystemCss(await fs.readFile(filepath, "utf8"), config, themeVars);
747
+ await fs.writeFile(filepath, next, "utf8");
748
+ }
749
+ async function patchCssFileWithAstroFont(filepath, fontVariable) {
750
+ const next = transformAstroManagedFontCss(await fs.readFile(filepath, "utf8"), fontVariable);
751
+ await fs.writeFile(filepath, next, "utf8");
752
+ }
753
+ async function patchCssFileManagedTailwindImport(filepath) {
754
+ const next = transformManagedTailwindImportCss(await fs.readFile(filepath, "utf8"));
755
+ await fs.writeFile(filepath, next, "utf8");
756
+ }
757
+ async function syncManagedTailwindCss(projectPath) {
758
+ const componentJsonFiles = await fg("**/components.json", {
759
+ cwd: projectPath,
760
+ absolute: true,
761
+ ignore: ["**/node_modules/**"]
762
+ });
763
+ await Promise.all(componentJsonFiles.map(async (componentJsonPath) => {
764
+ const cssRelativePath = (await fs.readJson(componentJsonPath))?.tailwind?.css;
765
+ if (typeof cssRelativePath !== "string" || cssRelativePath.length === 0) return;
766
+ const cssPath = path.resolve(path.dirname(componentJsonPath), cssRelativePath);
767
+ if (!await fs.pathExists(cssPath)) return;
768
+ await patchCssFileManagedTailwindImport(cssPath);
769
+ }));
770
+ }
771
+ async function syncAstroManagedFontCss(projectPath, fontVariable) {
772
+ const componentJsonFiles = await fg("**/components.json", {
773
+ cwd: projectPath,
774
+ ignore: [
775
+ "**/node_modules/**",
776
+ "**/dist/**",
777
+ "**/.astro/**"
778
+ ]
779
+ });
780
+ const cssFiles = /* @__PURE__ */ new Set();
781
+ for (const relativePath of componentJsonFiles) {
782
+ const absolutePath = path.resolve(projectPath, relativePath);
783
+ const cssPath = (await fs.readJson(absolutePath))?.tailwind?.css;
784
+ if (typeof cssPath === "string" && cssPath.length > 0) cssFiles.add(path.resolve(path.dirname(absolutePath), cssPath));
785
+ }
786
+ await Promise.all(Array.from(cssFiles).map((filepath) => patchCssFileWithAstroFont(filepath, fontVariable)));
787
+ }
788
+ async function patchTemplateI18nFile(filepath, config) {
789
+ if (!config.rtl) return;
790
+ const nextLanguage = getDocumentLanguage(config);
791
+ const nextSource = buildTemplateI18nSource(getTemplateI18nVariant(filepath), nextLanguage);
792
+ if (!await fs.pathExists(filepath)) {
793
+ await fs.ensureDir(path.dirname(filepath));
794
+ await fs.writeFile(filepath, nextSource, "utf8");
795
+ return;
796
+ }
797
+ const current = await fs.readFile(filepath, "utf8");
798
+ const next = current.includes("export const CURRENT_LANGUAGE") ? current.replace(/export const CURRENT_LANGUAGE: TemplateLanguage = "[^"]+";/, `export const CURRENT_LANGUAGE: TemplateLanguage = "${nextLanguage}";`) : nextSource;
799
+ if (next !== current) await fs.writeFile(filepath, next, "utf8");
800
+ }
801
+ async function patchLayoutFile(filepath, config) {
802
+ if (!await fs.pathExists(filepath)) return;
803
+ const current = await fs.readFile(filepath, "utf8");
804
+ if (current.includes("from \"@/i18n/ui\"")) return;
805
+ if (config.rtl) {
806
+ const next$1 = transformTemplateLayoutToI18n(current);
807
+ if (next$1 !== current) await fs.writeFile(filepath, next$1, "utf8");
808
+ return;
809
+ }
810
+ const nextLanguage = getDocumentLanguage(config);
811
+ const nextDirection = getDocumentDirection(config);
812
+ let next = current.replace(/<html([^>]*?)lang="[^"]*"([^>]*)>/, "<html$1$2>").replace(/<html([^>]*?)dir="[^"]*"([^>]*)>/, "<html$1$2>");
813
+ next = next.replace(/<html([^>]*)>/, `<html$1 lang="${nextLanguage}"${config.rtl ? ` dir="${nextDirection}"` : ""}>`);
814
+ if (next !== current) await fs.writeFile(filepath, next, "utf8");
815
+ }
816
+ async function patchStarterPageFile(filepath, config) {
817
+ if (!config.rtl || !await fs.pathExists(filepath)) return;
818
+ const current = await fs.readFile(filepath, "utf8");
819
+ const next = transformStarterPageToI18n(current);
820
+ if (next !== current) await fs.writeFile(filepath, next, "utf8");
821
+ }
822
+ async function applyDesignSystemToProject(projectPath, config, options = {}) {
823
+ const componentJsonFiles = await fg("**/components.json", {
824
+ cwd: projectPath,
825
+ ignore: [
826
+ "**/node_modules/**",
827
+ "**/dist/**",
828
+ "**/.astro/**"
829
+ ]
830
+ });
831
+ const cssFiles = /* @__PURE__ */ new Set();
832
+ for (const relativePath of componentJsonFiles) {
833
+ const absolutePath = path.resolve(projectPath, relativePath);
834
+ await patchComponentsJson(absolutePath, config);
835
+ const cssPath = (await fs.readJson(absolutePath))?.tailwind?.css;
836
+ if (typeof cssPath === "string" && cssPath.length > 0) {
837
+ const absoluteCssPath = path.resolve(path.dirname(absolutePath), cssPath);
838
+ cssFiles.add(absoluteCssPath);
839
+ }
840
+ }
841
+ await Promise.all(Array.from(cssFiles).map((filepath) => patchCssFileWithTheme(filepath, config, options.themeVars)));
842
+ await Promise.all([path.resolve(projectPath, "src/i18n/ui.ts"), path.resolve(projectPath, "apps/web/src/i18n/ui.ts")].map((filepath) => patchTemplateI18nFile(filepath, config)));
843
+ await Promise.all([path.resolve(projectPath, "src/layouts/Layout.astro"), path.resolve(projectPath, "apps/web/src/layouts/Layout.astro")].map((filepath) => patchLayoutFile(filepath, config)));
844
+ await Promise.all([path.resolve(projectPath, "src/pages/index.astro"), path.resolve(projectPath, "apps/web/src/pages/index.astro")].map((filepath) => patchStarterPageFile(filepath, config)));
845
+ const managedFonts = [toManagedAstroFont(config.font), config.fontHeading !== "inherit" ? toManagedAstroFont(`font-heading-${config.fontHeading}`) : null].filter((font) => font !== null);
846
+ const managedFont = managedFonts.find((font) => font.cssVariable !== "--font-heading");
847
+ if (managedFonts.length > 0 && managedFont) {
848
+ await syncAstroFontsInProject(projectPath, managedFonts, managedFont.cssVariable);
849
+ await cleanupAstroFontPackages(projectPath);
850
+ }
851
+ }
852
+
853
+ //#endregion
854
+ //#region src/utils/icon-transform.ts
855
+ const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---/;
856
+ function rewriteAstroIcons(content, iconLibrary) {
857
+ if (!(iconLibrary in ICON_LIBRARY_COLLECTIONS)) return content;
858
+ const library = iconLibrary;
859
+ const shouldRewriteLucideImports = library !== "lucide";
860
+ const frontmatterMatch = content.match(FRONTMATTER_PATTERN);
861
+ if (!frontmatterMatch) return content;
862
+ let frontmatter = frontmatterMatch[1];
863
+ let body = content.slice(frontmatterMatch[0].length);
864
+ const iconUsages = [];
865
+ const semanticIconImports = [];
866
+ if (shouldRewriteLucideImports) {
867
+ frontmatter = frontmatter.replace(/^\s*import\s+([A-Za-z0-9_$]+)\s+from\s+["']@lucide\/astro\/icons\/([^"']+)["'];?\s*$/gm, (full, localName, iconPath) => {
868
+ const semanticName = getSemanticIconNameFromLucidePath(iconPath);
869
+ if (!semanticName) return full;
870
+ iconUsages.push({
871
+ localName,
872
+ semanticName
873
+ });
874
+ return "";
875
+ });
876
+ frontmatter = frontmatter.replace(/import\s+\{([^}]*)\}\s+from\s+["']@lucide\/astro["'];?/g, (full, importsBlock) => {
877
+ const keptImports = [];
878
+ for (const rawImport of importsBlock.split(",")) {
879
+ const trimmedImport = rawImport.trim();
880
+ if (!trimmedImport) continue;
881
+ const aliasMatch = trimmedImport.match(/^([A-Za-z0-9_$]+)(?:\s+as\s+([A-Za-z0-9_$]+))?$/);
882
+ if (!aliasMatch) {
883
+ keptImports.push(trimmedImport);
884
+ continue;
885
+ }
886
+ const [, importedName, localAlias] = aliasMatch;
887
+ const semanticName = getSemanticIconNameFromLucideExport(importedName);
888
+ if (!semanticName) {
889
+ keptImports.push(trimmedImport);
890
+ continue;
891
+ }
892
+ iconUsages.push({
893
+ localName: localAlias ?? importedName,
894
+ semanticName
895
+ });
896
+ }
897
+ if (!keptImports.length) return "";
898
+ return `import { ${keptImports.join(", ")} } from "@lucide/astro";`;
899
+ });
900
+ }
901
+ frontmatter = frontmatter.replace(/^\s*import\s+([A-Za-z0-9_$]+)\s+from\s+["'][^"']*SemanticIcon\.astro["'];?\s*$/gm, (_full, localName) => {
902
+ semanticIconImports.push(localName);
903
+ return "";
904
+ });
905
+ if (!iconUsages.length && !semanticIconImports.length) return content;
906
+ for (const { localName, semanticName } of iconUsages.sort((left, right) => right.localName.length - left.localName.length)) {
907
+ const selfClosingPattern = new RegExp(`<${localName}(\\s[^>]*?)?\\s*/>`, "g");
908
+ const pairedPattern = new RegExp(`<${localName}(\\s[^>]*?)?>([\\s\\S]*?)<\\/${localName}>`, "g");
909
+ body = body.replace(selfClosingPattern, (_full, attributeString = "") => renderSemanticIconSvgWithAttributeString(library, semanticName, attributeString));
910
+ body = body.replace(pairedPattern, (_full, attributeString = "") => renderSemanticIconSvgWithAttributeString(library, semanticName, attributeString));
911
+ }
912
+ for (const localName of semanticIconImports.sort((left, right) => right.length - left.length)) {
913
+ const selfClosingPattern = new RegExp(`<${localName}(\\s[^>]*?)?\\s*/>`, "g");
914
+ const pairedPattern = new RegExp(`<${localName}(\\s[^>]*?)?>([\\s\\S]*?)<\\/${localName}>`, "g");
915
+ body = body.replace(selfClosingPattern, (full, attributeString = "") => renderSemanticIconUsage(full, library, attributeString));
916
+ body = body.replace(pairedPattern, (full, attributeString = "") => renderSemanticIconUsage(full, library, attributeString));
917
+ }
918
+ return `---\n${frontmatter.split("\n").filter((line, index, lines) => {
919
+ if (line.trim().length > 0) return true;
920
+ return lines[index - 1]?.trim().length > 0;
921
+ }).join("\n").trim()}\n---${body}`;
922
+ }
923
+ function renderSemanticIconUsage(original, library, attributeString = "") {
924
+ const semanticName = attributeString.match(/\sname=(["'])([^"']+)\1/)?.[2];
925
+ if (!semanticName || !SEMANTIC_ICON_NAMES.includes(semanticName)) return original;
926
+ return renderSemanticIconSvgWithAttributeString(library, semanticName, attributeString.replace(/\sname=(["'])[^"']+\1/g, "").replace(/\slibrary=(["'])[^"']+\1/g, "").replace(/\sdata-icon-library=(["'])[^"']+\1/g, "").trim());
927
+ }
928
+
929
+ //#endregion
930
+ //#region src/utils/menu-transform.ts
931
+ const TRANSLUCENT_CLASSES = [
932
+ "animate-none!",
933
+ "relative",
934
+ "bg-popover/70",
935
+ "before:pointer-events-none",
936
+ "before:absolute",
937
+ "before:inset-0",
938
+ "before:-z-1",
939
+ "before:rounded-[inherit]",
940
+ "before:backdrop-blur-2xl",
941
+ "before:backdrop-saturate-150",
942
+ "**:data-[slot$=-item]:focus:bg-foreground/10",
943
+ "**:data-[slot$=-item]:data-highlighted:bg-foreground/10",
944
+ "**:data-[slot$=-separator]:bg-foreground/5",
945
+ "**:data-[slot$=-trigger]:focus:bg-foreground/10",
946
+ "**:data-[slot$=-trigger]:aria-expanded:bg-foreground/10!",
947
+ "**:data-[variant=destructive]:focus:bg-foreground/10!",
948
+ "**:data-[variant=destructive]:text-accent-foreground!",
949
+ "**:data-[variant=destructive]:**:text-accent-foreground!"
950
+ ];
951
+ function transformMenuTokens(tokens, menuColor) {
952
+ const transformed = [];
953
+ for (const token of tokens) {
954
+ if (token === "cn-menu-target") {
955
+ if (menuColor === "inverted") transformed.push("dark");
956
+ else if (menuColor === "default-translucent") transformed.push(...TRANSLUCENT_CLASSES);
957
+ else if (menuColor === "inverted-translucent") transformed.push("dark", ...TRANSLUCENT_CLASSES);
958
+ continue;
959
+ }
960
+ if (token === "cn-menu-translucent") {
961
+ if (menuColor === "default-translucent" || menuColor === "inverted-translucent") transformed.push(...TRANSLUCENT_CLASSES);
962
+ continue;
963
+ }
964
+ transformed.push(token);
965
+ }
966
+ return Array.from(new Set(transformed));
967
+ }
968
+ function rewriteAstroMenus(content, menuColor) {
969
+ if (!content.includes("cn-menu-target") && !content.includes("cn-menu-translucent")) return content;
970
+ return content.replace(/(["'])([^"']*\bcn-menu-(?:target|translucent)\b[^"']*)\1/g, (_match, quote, classes) => {
971
+ return `${quote}${transformMenuTokens(String(classes).split(/\s+/).filter(Boolean), menuColor).join(" ")}${quote}`;
972
+ });
973
+ }
974
+
975
+ //#endregion
976
+ //#region src/utils/astro-imports.ts
977
+ function updateImportAliases(moduleSpecifier, config, isRemote = false) {
978
+ if (!moduleSpecifier.startsWith("@/") && !isRemote) return moduleSpecifier;
979
+ let specifier = moduleSpecifier;
980
+ if (isRemote && specifier.startsWith("@/")) specifier = specifier.replace(/^@\//, `@/registry/${config.style || "bejamas-juno"}/`);
981
+ if (!specifier.startsWith("@/registry/")) {
982
+ const alias = config.aliases.components.split("/")[0];
983
+ return specifier.replace(/^@\//, `${alias}/`);
984
+ }
985
+ if (specifier.match(/^@\/registry\/(.+)\/ui/)) return specifier.replace(/^@\/registry\/(.+)\/ui/, config.aliases.ui ?? `${config.aliases.components}/ui`);
986
+ if (config.aliases.components && specifier.match(/^@\/registry\/(.+)\/components/)) return specifier.replace(/^@\/registry\/(.+)\/components/, config.aliases.components);
987
+ if (config.aliases.lib && specifier.match(/^@\/registry\/(.+)\/lib/)) return specifier.replace(/^@\/registry\/(.+)\/lib/, config.aliases.lib);
988
+ if (config.aliases.hooks && specifier.match(/^@\/registry\/(.+)\/hooks/)) return specifier.replace(/^@\/registry\/(.+)\/hooks/, config.aliases.hooks);
989
+ return specifier.replace(/^@\/registry\/[^/]+/, config.aliases.components);
990
+ }
991
+ function rewriteAstroImports(content, config) {
992
+ let updated = content;
993
+ const utilsAlias = config.aliases?.utils;
994
+ const utilsImport = `${typeof utilsAlias === "string" && utilsAlias.includes("/") ? utilsAlias.split("/")[0] : "@"}/lib/utils`;
995
+ updated = updated.replace(/import\s+([\s\S]*?)\s+from\s+["']([^"']+)["']/g, (full, importsPart, specifier) => {
996
+ const next = updateImportAliases(specifier, config, false);
997
+ let finalSpec = next;
998
+ if (typeof importsPart === "string" && importsPart.split(/[{},\s]/).some((part) => part === "cn") && config.aliases.utils && (next === utilsImport || next === "@/lib/utils")) finalSpec = utilsImport === next ? next.replace(utilsImport, config.aliases.utils) : config.aliases.utils;
999
+ if (finalSpec === specifier) return full;
1000
+ return full.replace(specifier, finalSpec);
1001
+ });
1002
+ updated = updated.replace(/import\s+["']([^"']+)["']/g, (full, specifier) => {
1003
+ const next = updateImportAliases(specifier, config, false);
1004
+ if (next === specifier) return full;
1005
+ return full.replace(specifier, next);
1006
+ });
1007
+ updated = rewriteAstroMenus(updated, config.menuColor);
1008
+ return rewriteAstroIcons(updated, config.iconLibrary);
1009
+ }
1010
+ async function fixAstroImports(cwd, isVerbose) {
1011
+ const config = await getConfig(cwd);
1012
+ if (!config) return;
1013
+ const searchRoots = new Set([config.resolvedPaths.components, config.resolvedPaths.ui]);
1014
+ for (const root of Array.from(searchRoots)) {
1015
+ if (!root) continue;
1016
+ const astroFiles = await fg("**/*.astro", {
1017
+ cwd: root,
1018
+ absolute: true,
1019
+ dot: false
1020
+ });
1021
+ for (const filePath of astroFiles) {
1022
+ const original = await fs$1.readFile(filePath, "utf8");
1023
+ const rewritten = rewriteAstroImports(original, config);
1024
+ if (rewritten === original) continue;
1025
+ await fs$1.writeFile(filePath, rewritten, "utf8");
1026
+ if (isVerbose) logger.info(`[bejamas-ui] fixed imports in ${path.relative(cwd, filePath)}`);
1027
+ }
1028
+ }
45
1029
  }
46
1030
 
47
1031
  //#endregion
@@ -62,12 +1046,6 @@ async function getPackageManager(targetDir, { withFallback } = { withFallback: f
62
1046
  if (userAgent.startsWith("bun")) return "bun";
63
1047
  return "npm";
64
1048
  }
65
- async function getPackageRunner(cwd) {
66
- const packageManager = await getPackageManager(cwd);
67
- if (packageManager === "pnpm") return "pnpm dlx";
68
- if (packageManager === "bun") return "bunx";
69
- return "npx";
70
- }
71
1049
 
72
1050
  //#endregion
73
1051
  //#region src/registry/errors.ts
@@ -161,7 +1139,39 @@ const TEMPLATES = {
161
1139
  "astro-monorepo": "astro-monorepo",
162
1140
  "astro-with-component-docs-monorepo": "astro-with-component-docs-monorepo"
163
1141
  };
164
- const MONOREPO_TEMPLATE_URL = "https://codeload.github.com/bejamas/ui/tar.gz/main";
1142
+ const __filename = fileURLToPath(import.meta.url);
1143
+ const __dirname = path$1.dirname(__filename);
1144
+ function resolveLocalTemplatesDir() {
1145
+ for (const relativePath of [
1146
+ "../../../../templates",
1147
+ "../../../templates",
1148
+ "../../templates"
1149
+ ]) {
1150
+ const candidate = path$1.resolve(__dirname, relativePath);
1151
+ if (fs.existsSync(candidate)) return candidate;
1152
+ }
1153
+ return path$1.resolve(__dirname, "../../../../templates");
1154
+ }
1155
+ const LOCAL_TEMPLATES_DIR = resolveLocalTemplatesDir();
1156
+ async function applyLocalPackageOverrides(projectPath) {
1157
+ const bejamasPackageOverride = process.env.BEJAMAS_PACKAGE_OVERRIDE;
1158
+ if (!bejamasPackageOverride) return;
1159
+ const packageJsonPaths = await fg("**/package.json", {
1160
+ cwd: projectPath,
1161
+ absolute: true,
1162
+ ignore: ["**/node_modules/**"]
1163
+ });
1164
+ const normalizedOverride = bejamasPackageOverride.replace(/\\/g, "/");
1165
+ await Promise.all(packageJsonPaths.map(async (packageJsonPath) => {
1166
+ const packageJson = await fs.readJson(packageJsonPath);
1167
+ let changed = false;
1168
+ for (const field of ["dependencies", "devDependencies"]) if (packageJson[field]?.bejamas) {
1169
+ packageJson[field].bejamas = `file:${normalizedOverride}`;
1170
+ changed = true;
1171
+ }
1172
+ if (changed) await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1173
+ }));
1174
+ }
165
1175
  async function createProject(options) {
166
1176
  options = {
167
1177
  srcDir: false,
@@ -206,7 +1216,7 @@ async function createProject(options) {
206
1216
  const packageManager = await getPackageManager(options.cwd, { withFallback: true });
207
1217
  const projectPath = `${options.cwd}/${projectName}`;
208
1218
  try {
209
- await fsExtra.access(options.cwd, fsExtra.constants.W_OK);
1219
+ await fs.access(options.cwd, fs.constants.W_OK);
210
1220
  } catch (error) {
211
1221
  logger.break();
212
1222
  logger.error(`The path ${highlighter.info(options.cwd)} is not writable.`);
@@ -214,7 +1224,7 @@ async function createProject(options) {
214
1224
  logger.break();
215
1225
  process.exit(1);
216
1226
  }
217
- if (fsExtra.existsSync(path.resolve(options.cwd, projectName, "package.json"))) {
1227
+ if (fs.existsSync(path$1.resolve(options.cwd, projectName, "package.json"))) {
218
1228
  logger.break();
219
1229
  logger.error(`A project with the name ${highlighter.info(projectName)} already exists.`);
220
1230
  logger.error(`Please choose a different name and try again.`);
@@ -234,40 +1244,23 @@ async function createProject(options) {
234
1244
  }
235
1245
  async function createProjectFromTemplate(projectPath, options) {
236
1246
  const createSpinner = spinner(`Creating a new project from template. This may take a few minutes.`).start();
237
- const TEMPLATE_TAR_SUBPATH = {
238
- astro: "ui-main/templates/astro",
239
- "astro-monorepo": "ui-main/templates/monorepo-astro",
240
- "astro-with-component-docs-monorepo": "ui-main/templates/monorepo-astro-with-docs"
1247
+ const TEMPLATE_DIRNAME = {
1248
+ astro: "astro",
1249
+ "astro-monorepo": "monorepo-astro",
1250
+ "astro-with-component-docs-monorepo": "monorepo-astro-with-docs"
241
1251
  };
242
1252
  try {
243
1253
  dotenv.config({ quiet: true });
244
- const templatePath = path.join(os.tmpdir(), `bejamas-template-${Date.now()}`);
245
- await fsExtra.ensureDir(templatePath);
246
- const authToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
247
- const usedAuth = Boolean(authToken);
248
- const headers = { "User-Agent": "bejamas-cli" };
249
- if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
250
- const response = await fetch(MONOREPO_TEMPLATE_URL, { headers });
251
- if (!response.ok) {
252
- if (response.status === 401 || response.status === 403 || !usedAuth && response.status === 404) throw new Error("Unauthorized to access private template. Set GITHUB_TOKEN or GH_TOKEN (in .env or env) with repo access and try again.");
253
- if (response.status === 404) throw new Error("Failed to download template: not found.");
254
- throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
255
- }
256
- const tarPath = path.resolve(templatePath, "template.tar.gz");
257
- await fsExtra.writeFile(tarPath, Buffer.from(await response.arrayBuffer()));
258
- const tarSubpath = TEMPLATE_TAR_SUBPATH[options.templateKey];
259
- const leafName = tarSubpath.split("/").pop();
260
- await execa("tar", [
261
- "-xzf",
262
- tarPath,
263
- "-C",
264
- templatePath,
265
- "--strip-components=2",
266
- tarSubpath
267
- ]);
268
- const extractedPath = path.resolve(templatePath, leafName);
269
- await fsExtra.move(extractedPath, projectPath);
270
- await fsExtra.remove(templatePath);
1254
+ const templatePath = path$1.join(os.tmpdir(), `bejamas-template-${Date.now()}`);
1255
+ const templateSource = path$1.resolve(LOCAL_TEMPLATES_DIR, TEMPLATE_DIRNAME[options.templateKey]);
1256
+ if (!await fs.pathExists(templateSource)) throw new Error(`Local template not found: ${templateSource}`);
1257
+ await fs.copy(templateSource, projectPath, { filter: (source) => {
1258
+ const basename = path$1.basename(source);
1259
+ return basename !== "node_modules" && basename !== ".astro";
1260
+ } });
1261
+ await removeEmptyTemplateI18nDirs(projectPath);
1262
+ await applyLocalPackageOverrides(projectPath);
1263
+ await fs.remove(templatePath);
271
1264
  await execa(options.packageManager, ["install"], { cwd: projectPath });
272
1265
  try {
273
1266
  const { stdout } = await execa("git", ["rev-parse", "--is-inside-work-tree"], { cwd: projectPath });
@@ -287,16 +1280,288 @@ async function createProjectFromTemplate(projectPath, options) {
287
1280
  handleError(error);
288
1281
  }
289
1282
  }
1283
+ async function removeEmptyTemplateI18nDirs(projectPath) {
1284
+ const candidates = [path$1.resolve(projectPath, "src/i18n"), path$1.resolve(projectPath, "apps/web/src/i18n")];
1285
+ await Promise.all(candidates.map(async (candidate) => {
1286
+ if (!await fs.pathExists(candidate)) return;
1287
+ if ((await fs.readdir(candidate)).length === 0) await fs.remove(candidate);
1288
+ }));
1289
+ }
1290
+
1291
+ //#endregion
1292
+ //#region src/utils/installed-ui-components.ts
1293
+ function toRegistryName(filename) {
1294
+ return filename.replace(/\.[^.]+$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
1295
+ }
1296
+ async function pathExists$1(filepath) {
1297
+ try {
1298
+ await promises.access(filepath);
1299
+ return true;
1300
+ } catch {
1301
+ return false;
1302
+ }
1303
+ }
1304
+ async function detectInstalledUiComponents(uiDir) {
1305
+ if (!await pathExists$1(uiDir)) return [];
1306
+ const entries = await promises.readdir(uiDir, { withFileTypes: true });
1307
+ const components = /* @__PURE__ */ new Set();
1308
+ for (const entry of entries) {
1309
+ if (entry.name.startsWith(".")) continue;
1310
+ const entryPath = path.resolve(uiDir, entry.name);
1311
+ if (entry.isDirectory()) {
1312
+ if ((await promises.readdir(entryPath, { withFileTypes: true })).some((child) => child.isFile() && (/\.(astro|tsx|jsx)$/.test(child.name) || /^(index\.(ts|js|tsx|jsx))$/.test(child.name)))) components.add(entry.name);
1313
+ continue;
1314
+ }
1315
+ if (entry.isFile() && /\.(astro|tsx|jsx)$/.test(entry.name) && !entry.name.startsWith("index.")) components.add(toRegistryName(entry.name));
1316
+ }
1317
+ return Array.from(components).sort();
1318
+ }
1319
+ async function getInstalledUiComponents(cwd) {
1320
+ const config = await getConfig(cwd);
1321
+ if (!config) return [];
1322
+ return detectInstalledUiComponents(config.resolvedPaths.ui);
1323
+ }
1324
+
1325
+ //#endregion
1326
+ //#region src/utils/reorganize-components.ts
1327
+ async function fetchRegistryItem(componentName, registryUrl, style = "bejamas-juno") {
1328
+ const url = `${registryUrl}/styles/${style}/${componentName}.json`;
1329
+ try {
1330
+ const response = await fetch(url);
1331
+ if (!response.ok) {
1332
+ const fallbackUrl = `${registryUrl}/${componentName}.json`;
1333
+ const fallbackResponse = await fetch(fallbackUrl);
1334
+ if (!fallbackResponse.ok) return null;
1335
+ return await fallbackResponse.json();
1336
+ }
1337
+ return await response.json();
1338
+ } catch {
1339
+ return null;
1340
+ }
1341
+ }
1342
+ function getSubfolderFromPaths(files) {
1343
+ if (!files || files.length === 0) return null;
1344
+ const uiFiles = files.filter((file) => file.type === "registry:ui");
1345
+ if (uiFiles.length < 2) return null;
1346
+ const subfolders = /* @__PURE__ */ new Set();
1347
+ for (const file of uiFiles) {
1348
+ const parts = file.path.split("/");
1349
+ const uiIndex = parts.indexOf("ui");
1350
+ if (uiIndex !== -1 && parts.length > uiIndex + 2) subfolders.add(parts[uiIndex + 1]);
1351
+ }
1352
+ if (subfolders.size === 1) return Array.from(subfolders)[0];
1353
+ if (uiFiles.length > 0) {
1354
+ const dirname$1 = path.dirname(uiFiles[0].path);
1355
+ const folderName = path.basename(dirname$1);
1356
+ if (folderName && folderName !== "ui") return folderName;
1357
+ }
1358
+ return null;
1359
+ }
1360
+ async function pathExists(filePath) {
1361
+ try {
1362
+ await fs$1.access(filePath);
1363
+ return true;
1364
+ } catch {
1365
+ return false;
1366
+ }
1367
+ }
1368
+ function resolveShadcnUiRelativePath(filePath, uiDir) {
1369
+ const normalizedFilePath = filePath.replace(/^\/|\/$/g, "");
1370
+ const lastTargetSegment = path.basename(uiDir.replace(/^\/|\/$/g, ""));
1371
+ if (!lastTargetSegment) return path.basename(normalizedFilePath);
1372
+ const fileSegments = normalizedFilePath.split("/");
1373
+ const commonDirIndex = fileSegments.findIndex((segment) => segment === lastTargetSegment);
1374
+ if (commonDirIndex === -1) return fileSegments[fileSegments.length - 1];
1375
+ return fileSegments.slice(commonDirIndex + 1).join("/");
1376
+ }
1377
+ /**
1378
+ * Current upstream shadcn workspace installs flatten `ui/foo/Bar.astro` when
1379
+ * the resolved ui target ends in a segment like `components` instead of `ui`.
1380
+ * We keep reorganization only for that compatibility case.
1381
+ */
1382
+ function shouldReorganizeRegistryUiFiles(files, uiDir) {
1383
+ if (!uiDir) return false;
1384
+ if (!getSubfolderFromPaths(files)) return false;
1385
+ return files.filter((file) => file.type === "registry:ui").some((file) => {
1386
+ return !resolveShadcnUiRelativePath(file.path, uiDir).includes("/");
1387
+ });
1388
+ }
1389
+ /**
1390
+ * Reorganizes one multi-file registry:ui item from flat output into its
1391
+ * expected subfolder. This is the filesystem-level compatibility shim.
1392
+ */
1393
+ async function reorganizeRegistryUiFiles(files, uiDir, verbose, overwriteExisting = false) {
1394
+ const result = {
1395
+ totalMoved: 0,
1396
+ movedFiles: [],
1397
+ skippedFiles: []
1398
+ };
1399
+ if (!uiDir || !files || files.length === 0) return result;
1400
+ const subfolder = getSubfolderFromPaths(files);
1401
+ if (!subfolder) return result;
1402
+ const uiFiles = files.filter((file) => file.type === "registry:ui");
1403
+ const targetDir = path.join(uiDir, subfolder);
1404
+ for (const file of uiFiles) {
1405
+ const filename = path.basename(file.path);
1406
+ const flatPath = path.join(uiDir, filename);
1407
+ const targetPath = path.join(targetDir, filename);
1408
+ if (!await pathExists(flatPath)) continue;
1409
+ if (await pathExists(targetPath)) {
1410
+ if (overwriteExisting) {
1411
+ await fs$1.mkdir(targetDir, { recursive: true });
1412
+ await fs$1.unlink(targetPath);
1413
+ await fs$1.rename(flatPath, targetPath);
1414
+ result.totalMoved++;
1415
+ result.movedFiles.push(`${subfolder}/${filename}`);
1416
+ if (verbose) logger.info(`[bejamas-ui] Replaced ${subfolder}/${filename} with the reinstalled version`);
1417
+ continue;
1418
+ }
1419
+ try {
1420
+ await fs$1.unlink(flatPath);
1421
+ result.skippedFiles.push(`${subfolder}/${filename}`);
1422
+ if (verbose) logger.info(`[bejamas-ui] Removed flat duplicate: ${filename} (${subfolder}/${filename} exists)`);
1423
+ } catch {
1424
+ result.skippedFiles.push(`${subfolder}/${filename}`);
1425
+ }
1426
+ continue;
1427
+ }
1428
+ await fs$1.mkdir(targetDir, { recursive: true });
1429
+ await fs$1.rename(flatPath, targetPath);
1430
+ result.totalMoved++;
1431
+ result.movedFiles.push(`${subfolder}/${filename}`);
1432
+ if (verbose) logger.info(`[bejamas-ui] Moved ${filename} -> ${subfolder}/${filename}`);
1433
+ }
1434
+ return result;
1435
+ }
1436
+ async function reorganizeComponents(components, uiDir, registryUrl, verbose, style = "bejamas-juno", overwriteExisting = false) {
1437
+ const result = {
1438
+ totalMoved: 0,
1439
+ movedFiles: [],
1440
+ skippedFiles: []
1441
+ };
1442
+ if (!uiDir || components.length === 0) return result;
1443
+ for (const componentName of components) try {
1444
+ const registryItem = await fetchRegistryItem(componentName, registryUrl, style);
1445
+ if (!registryItem) {
1446
+ if (verbose) logger.info(`[bejamas-ui] Could not fetch registry for ${componentName}, skipping reorganization`);
1447
+ continue;
1448
+ }
1449
+ if (!shouldReorganizeRegistryUiFiles(registryItem.files, uiDir)) continue;
1450
+ const componentResult = await reorganizeRegistryUiFiles(registryItem.files, uiDir, verbose, overwriteExisting);
1451
+ result.totalMoved += componentResult.totalMoved;
1452
+ result.movedFiles.push(...componentResult.movedFiles);
1453
+ result.skippedFiles.push(...componentResult.skippedFiles);
1454
+ if (componentResult.totalMoved > 0 && verbose) {
1455
+ const subfolder = getSubfolderFromPaths(registryItem.files);
1456
+ logger.info(`[bejamas-ui] Reorganized ${componentName} into ${subfolder}/`);
1457
+ }
1458
+ } catch (err) {
1459
+ if (verbose) logger.warn(`[bejamas-ui] Failed to reorganize ${componentName}: ${err}`);
1460
+ }
1461
+ return result;
1462
+ }
1463
+
1464
+ //#endregion
1465
+ //#region src/utils/shadcn-cli.ts
1466
+ const PINNED_SHADCN_VERSION = "4.1.1";
1467
+ const PINNED_SHADCN_PACKAGE = `shadcn@${PINNED_SHADCN_VERSION}`;
1468
+ const PINNED_SHADCN_EXEC_PREFIX = path.join(os$1.tmpdir(), "bejamas-shadcn", PINNED_SHADCN_VERSION);
1469
+ async function ensurePinnedShadcnExecPrefix() {
1470
+ await fs$1.mkdir(PINNED_SHADCN_EXEC_PREFIX, { recursive: true });
1471
+ return PINNED_SHADCN_EXEC_PREFIX;
1472
+ }
1473
+ function getNpmExecutable() {
1474
+ return process.platform === "win32" ? "npm.cmd" : "npm";
1475
+ }
1476
+ function buildPinnedShadcnInvocation(shadcnArgs) {
1477
+ return {
1478
+ cmd: getNpmExecutable(),
1479
+ args: [
1480
+ "exec",
1481
+ "--yes",
1482
+ "--prefix",
1483
+ PINNED_SHADCN_EXEC_PREFIX,
1484
+ `--package=${PINNED_SHADCN_PACKAGE}`,
1485
+ "--",
1486
+ "shadcn",
1487
+ ...shadcnArgs
1488
+ ],
1489
+ source: "isolated"
1490
+ };
1491
+ }
1492
+
1493
+ //#endregion
1494
+ //#region src/utils/shadcn-command.ts
1495
+ function extractPassthroughArgs(rawArgv, commandName) {
1496
+ const commandIndex = rawArgv.findIndex((arg) => arg === commandName);
1497
+ if (commandIndex === -1) return [];
1498
+ const rest = rawArgv.slice(commandIndex + 1);
1499
+ const doubleDashIndex = rest.indexOf("--");
1500
+ if (doubleDashIndex === -1) return [];
1501
+ return rest.slice(doubleDashIndex + 1);
1502
+ }
1503
+ async function runShadcnCommand({ cwd, args, env }) {
1504
+ await ensurePinnedShadcnExecPrefix();
1505
+ const invocation = buildPinnedShadcnInvocation(args);
1506
+ await execa(invocation.cmd, invocation.args, {
1507
+ cwd,
1508
+ env: {
1509
+ ...process.env,
1510
+ ...env
1511
+ },
1512
+ stdio: "inherit"
1513
+ });
1514
+ }
290
1515
 
291
1516
  //#endregion
292
1517
  //#region src/commands/init.ts
293
- const DEFAULT_REGISTRY_URL$1 = "https://ui.bejamas.com/r";
294
- const DEFAULT_COMPONENTS_BASE_COLOR = "neutral";
295
- const SHADCN_INIT_ARGS = ["init"];
1518
+ function resolveDesignSystemConfig(options) {
1519
+ const rtlLanguage = options.rtl && options.lang ? options.lang : DEFAULT_DESIGN_SYSTEM_CONFIG.rtlLanguage;
1520
+ if (options.preset && isPresetCode(options.preset)) {
1521
+ const decoded = decodePreset(options.preset);
1522
+ if (decoded) return normalizeDesignSystemConfig(designSystemConfigSchema.parse({
1523
+ ...DEFAULT_DESIGN_SYSTEM_CONFIG,
1524
+ ...decoded,
1525
+ template: options.template ?? DEFAULT_DESIGN_SYSTEM_CONFIG.template,
1526
+ rtl: options.rtl ?? false,
1527
+ rtlLanguage
1528
+ }));
1529
+ }
1530
+ const baseColor = options.baseColor ?? DEFAULT_DESIGN_SYSTEM_CONFIG.baseColor;
1531
+ return normalizeDesignSystemConfig(designSystemConfigSchema.parse({
1532
+ ...DEFAULT_DESIGN_SYSTEM_CONFIG,
1533
+ template: options.template ?? DEFAULT_DESIGN_SYSTEM_CONFIG.template,
1534
+ rtl: options.rtl ?? false,
1535
+ rtlLanguage,
1536
+ baseColor,
1537
+ theme: baseColor === DEFAULT_DESIGN_SYSTEM_CONFIG.baseColor ? DEFAULT_DESIGN_SYSTEM_CONFIG.theme : baseColor
1538
+ }));
1539
+ }
1540
+ function buildInitUrl(config, themeRef, env = process.env) {
1541
+ const params = new URLSearchParams({
1542
+ preset: encodePreset(config),
1543
+ template: config.template
1544
+ });
1545
+ if (config.rtl) {
1546
+ params.set("rtl", "true");
1547
+ params.set("lang", config.rtlLanguage);
1548
+ }
1549
+ if (themeRef) params.set("themeRef", themeRef);
1550
+ return `${buildUiUrl("/init", env)}?${params.toString()}`;
1551
+ }
1552
+ function buildThemeVarsFromRegistryItem(item) {
1553
+ return item?.cssVars ?? null;
1554
+ }
1555
+ async function fetchInitThemeVars(initUrl) {
1556
+ const response = await fetch(initUrl);
1557
+ if (!response.ok) return null;
1558
+ return buildThemeVarsFromRegistryItem(await response.json());
1559
+ }
296
1560
  const initOptionsSchema = z.object({
297
1561
  cwd: z.string(),
298
1562
  components: z.array(z.string()).optional(),
299
1563
  yes: z.boolean(),
1564
+ reinstall: z.boolean().optional(),
300
1565
  defaults: z.boolean(),
301
1566
  force: z.boolean(),
302
1567
  silent: z.boolean(),
@@ -306,85 +1571,98 @@ const initOptionsSchema = z.object({
306
1571
  template: z.string().optional().refine((val) => {
307
1572
  if (val) return TEMPLATES[val];
308
1573
  return true;
309
- }, { message: "Invalid template. Please use 'next' or 'next-monorepo'." }),
310
- baseColor: z.string().optional(),
311
- baseStyle: z.boolean()
1574
+ }, { message: "Invalid template. Please use 'astro', 'astro-monorepo', or 'astro-with-component-docs-monorepo'." }),
1575
+ preset: z.string().optional(),
1576
+ baseColor: z.string().optional().refine((val) => {
1577
+ if (val) return BASE_COLORS.find((color) => color.name === val);
1578
+ return true;
1579
+ }, { message: `Invalid base color. Please use '${BASE_COLORS.map((color) => color.name).join("', '")}'` }),
1580
+ baseStyle: z.boolean(),
1581
+ rtl: z.boolean().default(false),
1582
+ lang: z.enum(RTL_LANGUAGE_VALUES).optional(),
1583
+ themeRef: z.string().optional()
312
1584
  });
313
- function buildShadcnInitInvocation(localShadcnPath, hasLocalShadcn, localShadcnVersion) {
314
- if (hasLocalShadcn) {
315
- const args = [...SHADCN_INIT_ARGS];
316
- if (usesLegacyBaseColorFlag(localShadcnVersion)) args.push("--base-color", DEFAULT_COMPONENTS_BASE_COLOR);
317
- return {
318
- cmd: localShadcnPath,
319
- args
320
- };
321
- }
322
- return {
323
- cmd: "npx",
324
- args: [
325
- "-y",
326
- "shadcn@latest",
327
- ...SHADCN_INIT_ARGS
328
- ]
329
- };
1585
+ function shouldReinstallExistingComponents(options) {
1586
+ return options.reinstall ?? Boolean(options.preset);
330
1587
  }
331
- function usesLegacyBaseColorFlag(version) {
332
- if (!version) return false;
333
- const major = Number.parseInt(version.split(".")[0] ?? "", 10);
334
- return Number.isFinite(major) && major > 0 && major < 4;
335
- }
336
- async function getLocalShadcnVersion(cwd) {
337
- const filePath = path.resolve(cwd, "node_modules", "shadcn", "package.json");
338
- try {
339
- const contents = await promises.readFile(filePath, "utf8");
340
- const parsed = JSON.parse(contents);
341
- return typeof parsed.version === "string" ? parsed.version : null;
342
- } catch {
343
- return null;
344
- }
345
- }
346
- function getDeprecatedBaseColorWarning(baseColor) {
347
- if (!baseColor) return null;
1588
+ function ensureShadcnReinstallFlag(forwardedOptions, shouldReinstall) {
1589
+ if (!shouldReinstall || forwardedOptions.includes("--reinstall") || forwardedOptions.includes("--no-reinstall")) return forwardedOptions;
1590
+ const passthroughIndex = forwardedOptions.indexOf("--");
1591
+ if (passthroughIndex === -1) return [...forwardedOptions, "--reinstall"];
348
1592
  return [
349
- "The --base-color option is deprecated and ignored.",
350
- "Bejamas now aligns with shadcn CLI v4.",
351
- `Edit ${highlighter.info("components.json")} to change`,
352
- `${highlighter.info("tailwind.baseColor")} after init.`
353
- ].join(" ");
354
- }
355
- async function normalizeComponentsBaseColor(cwd, baseColor = DEFAULT_COMPONENTS_BASE_COLOR) {
356
- const filePath = path.resolve(cwd, "components.json");
357
- let originalContents;
358
- try {
359
- originalContents = await promises.readFile(filePath, "utf8");
360
- } catch {
361
- throw new Error(`Failed to read ${highlighter.info(filePath)} after shadcn init. Make sure the command completed successfully and created a valid components.json file.`);
362
- }
363
- let parsedConfig;
364
- try {
365
- const parsed = JSON.parse(originalContents);
366
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected a JSON object.");
367
- parsedConfig = parsed;
368
- } catch {
369
- throw new Error(`Failed to parse ${highlighter.info(filePath)} after shadcn init. Expected a valid JSON object with a tailwind configuration.`);
1593
+ ...forwardedOptions.slice(0, passthroughIndex),
1594
+ "--reinstall",
1595
+ ...forwardedOptions.slice(passthroughIndex)
1596
+ ];
1597
+ }
1598
+ function extractOptionsForShadcnInit(rawArgv, cmd) {
1599
+ if (typeof cmd.getOptionValueSource === "function") {
1600
+ const opts = cmd.optsWithGlobals();
1601
+ const forwarded$1 = [];
1602
+ const getSource = (key) => cmd.getOptionValueSource(key);
1603
+ const addBoolean = (key, flag, negateFlag) => {
1604
+ if (getSource(key) !== "cli") return;
1605
+ const value = opts[key];
1606
+ if (typeof value !== "boolean") return;
1607
+ if (value) forwarded$1.push(flag);
1608
+ else if (negateFlag) forwarded$1.push(negateFlag);
1609
+ };
1610
+ addBoolean("yes", "--yes");
1611
+ addBoolean("force", "--force");
1612
+ addBoolean("silent", "--silent");
1613
+ addBoolean("reinstall", "--reinstall", "--no-reinstall");
1614
+ const initIndex$1 = rawArgv.findIndex((arg) => arg === "init");
1615
+ if (initIndex$1 !== -1) {
1616
+ const rest$1 = rawArgv.slice(initIndex$1 + 1);
1617
+ const doubleDashIndex = rest$1.indexOf("--");
1618
+ if (doubleDashIndex !== -1) forwarded$1.push(...rest$1.slice(doubleDashIndex));
1619
+ }
1620
+ return forwarded$1;
370
1621
  }
371
- const tailwind = parsedConfig.tailwind;
372
- if (!tailwind || typeof tailwind !== "object" || Array.isArray(tailwind)) throw new Error(`Invalid ${highlighter.info(filePath)} generated by shadcn init. Expected a ${highlighter.info("tailwind")} object so Bejamas can normalize ${highlighter.info("baseColor")}.`);
373
- const normalizedConfig = {
374
- ...parsedConfig,
375
- tailwind: {
376
- ...tailwind,
377
- baseColor
1622
+ const initIndex = rawArgv.findIndex((arg) => arg === "init");
1623
+ if (initIndex === -1) return [];
1624
+ const rest = rawArgv.slice(initIndex + 1);
1625
+ const forwarded = [];
1626
+ const filteredFlags = new Set([
1627
+ "-v",
1628
+ "--verbose",
1629
+ "-t",
1630
+ "--template",
1631
+ "-b",
1632
+ "--base-color",
1633
+ "-p",
1634
+ "--preset",
1635
+ "--theme-ref",
1636
+ "-c",
1637
+ "--cwd",
1638
+ "-d",
1639
+ "--defaults",
1640
+ "--src-dir",
1641
+ "--no-src-dir",
1642
+ "--css-variables",
1643
+ "--no-css-variables",
1644
+ "--no-base-style",
1645
+ "--rtl",
1646
+ "--lang"
1647
+ ]);
1648
+ for (let index = 0; index < rest.length; index += 1) {
1649
+ const token = rest[index];
1650
+ if (token === "--") {
1651
+ forwarded.push("--", ...rest.slice(index + 1));
1652
+ break;
378
1653
  }
379
- };
380
- const normalizedContents = `${JSON.stringify(normalizedConfig, null, 2)}\n`;
381
- if (normalizedContents === originalContents) return false;
382
- await promises.writeFile(filePath, normalizedContents, "utf8");
383
- return true;
1654
+ if (!token.startsWith("-")) continue;
1655
+ if (filteredFlags.has(token)) continue;
1656
+ forwarded.push(token);
1657
+ }
1658
+ return forwarded;
384
1659
  }
385
- const init = new Command().name("init").description("initialize your project and install dependencies").argument("[components...]", "names, url or local path to component").option("-t, --template <template>", "the template to use. (next, next-monorepo)").option("-b, --base-color <base-color>", "deprecated: accepted for compatibility but ignored. Edit components.json to change tailwind.baseColor.", void 0).option("-y, --yes", "skip confirmation prompt.", true).option("-d, --defaults,", "use default configuration.", false).option("-f, --force", "force overwrite of existing configuration.", false).option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("-s, --silent", "mute output.", false).option("--src-dir", "use the src directory when creating a new project.", false).option("--no-src-dir", "do not use the src directory when creating a new project.").option("--css-variables", "use css variables for theming.", true).option("--no-css-variables", "do not use css variables for theming.").option("--no-base-style", "do not install the base shadcn style.").action(async (_components, opts) => {
1660
+ const init = new Command().name("init").description("initialize your project and install dependencies").argument("[components...]", "names, url or local path to component").option("-t, --template <template>", "the template to use. (astro, astro-monorepo, astro-with-component-docs-monorepo)").option("-b, --base-color <base-color>", "the base color to use. (neutral, gray, zinc, stone, slate)", void 0).option("-p, --preset <preset>", "the encoded create preset to use").option("--theme-ref <theme-ref>", "the custom theme ref to use").option("-y, --yes", "skip confirmation prompt.", false).option("-d, --defaults", "use default configuration.", false).option("-f, --force", "force overwrite of existing configuration.", false).option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("-s, --silent", "mute output.", false).option("--src-dir", "use the src directory when creating a new project.", false).option("--no-src-dir", "do not use the src directory when creating a new project.").option("--css-variables", "use css variables for theming.", true).option("--no-css-variables", "do not use css variables for theming.").option("--no-base-style", "do not install the base shadcn style.").option("--rtl", "enable right-to-left output", false).option("--lang <lang>", "set the RTL language. (ar, fa, he)").option("--reinstall", "re-install existing UI components. Enabled by default for preset switching.").option("--no-reinstall", "do not re-install existing UI components during preset switching.").action(async (_components, opts, cmd) => {
386
1661
  try {
387
- await runInit(opts);
1662
+ await runInit({
1663
+ ...opts,
1664
+ forwardedOptions: extractOptionsForShadcnInit(process.argv.slice(2), cmd)
1665
+ });
388
1666
  } catch (error) {
389
1667
  logger.break();
390
1668
  handleError(error);
@@ -393,8 +1671,7 @@ const init = new Command().name("init").description("initialize your project and
393
1671
  }
394
1672
  });
395
1673
  async function runInit(options) {
396
- const baseColorWarning = getDeprecatedBaseColorWarning(options.baseColor);
397
- if (baseColorWarning && !options.silent) logger.warn(baseColorWarning);
1674
+ const designConfig = resolveDesignSystemConfig(options);
398
1675
  let newProjectTemplate;
399
1676
  if (!options.skipPreflight) {
400
1677
  if ((await preFlightInit(options)).errors[MISSING_DIR_OR_EMPTY_PROJECT]) {
@@ -406,34 +1683,86 @@ async function runInit(options) {
406
1683
  }
407
1684
  }
408
1685
  if (newProjectTemplate) {
409
- options.cwd = path.resolve(options.cwd, {
1686
+ const projectPath = {
410
1687
  "astro-monorepo": "apps/web",
411
1688
  "astro-with-component-docs-monorepo": "apps/web",
412
1689
  astro: ""
413
- }[newProjectTemplate]);
1690
+ };
1691
+ await applyDesignSystemToProject(options.cwd, {
1692
+ ...designConfig,
1693
+ template: newProjectTemplate
1694
+ }, { themeVars: options.themeRef ? await fetchInitThemeVars(buildInitUrl({
1695
+ ...designConfig,
1696
+ template: newProjectTemplate
1697
+ }, options.themeRef)) ?? void 0 : void 0 });
1698
+ options.cwd = path.resolve(options.cwd, projectPath[newProjectTemplate]);
414
1699
  logger.log(`${highlighter.success("Success!")} Project initialization completed.\nYou may now add components.`);
415
1700
  return await getConfig(options.cwd);
416
1701
  }
417
- const shadcnBin = process.platform === "win32" ? "shadcn.cmd" : "shadcn";
418
- const localShadcnPath = path.resolve(options.cwd, "node_modules", ".bin", shadcnBin);
419
1702
  try {
420
1703
  const env = {
421
1704
  ...process.env,
422
- REGISTRY_URL: process.env.REGISTRY_URL || DEFAULT_REGISTRY_URL$1
1705
+ REGISTRY_URL: resolveRegistryUrl()
423
1706
  };
424
- const hasLocalShadcn = await fsExtra.pathExists(localShadcnPath);
425
- const invocation = buildShadcnInitInvocation(localShadcnPath, hasLocalShadcn, hasLocalShadcn ? await getLocalShadcnVersion(options.cwd) : null);
426
- await execa(invocation.cmd, invocation.args, {
427
- stdio: "inherit",
1707
+ const initUrl = buildInitUrl(designConfig, options.themeRef);
1708
+ const shouldReinstall = shouldReinstallExistingComponents(options);
1709
+ const reinstallComponents = shouldReinstall ? await getInstalledUiComponents(options.cwd) : [];
1710
+ const forwardedOptions = ensureShadcnReinstallFlag(options.forwardedOptions ?? [], shouldReinstall);
1711
+ await runShadcnCommand({
428
1712
  cwd: options.cwd,
1713
+ args: [
1714
+ "init",
1715
+ initUrl,
1716
+ ...reinstallComponents,
1717
+ ...forwardedOptions
1718
+ ],
429
1719
  env
430
1720
  });
431
- } catch (err) {
1721
+ await syncManagedTailwindCss(options.cwd);
1722
+ const managedFonts = [toManagedAstroFont(designConfig.font), designConfig.fontHeading !== "inherit" ? toManagedAstroFont(`font-heading-${designConfig.fontHeading}`) : null].filter((font) => font !== null);
1723
+ const managedFont = managedFonts.find((font) => font.cssVariable !== "--font-heading");
1724
+ if (managedFonts.length > 0 && managedFont) {
1725
+ await syncAstroFontsInProject(options.cwd, managedFonts, managedFont.cssVariable);
1726
+ await syncAstroManagedFontCss(options.cwd, managedFont.cssVariable);
1727
+ await cleanupAstroFontPackages(options.cwd);
1728
+ }
1729
+ if (reinstallComponents.length > 0) {
1730
+ const config = await getConfig(options.cwd);
1731
+ const uiDir = config?.resolvedPaths.ui ?? "";
1732
+ const activeStyle = config?.style ?? "bejamas-juno";
1733
+ if (uiDir) await reorganizeComponents(reinstallComponents, uiDir, resolveRegistryUrl(), false, activeStyle, true);
1734
+ await fixAstroImports(options.cwd, false);
1735
+ }
1736
+ } catch {
432
1737
  process.exit(1);
433
1738
  }
434
- await normalizeComponentsBaseColor(options.cwd);
435
1739
  }
436
1740
 
1741
+ //#endregion
1742
+ //#region src/commands/docs.ts
1743
+ const docs = new Command().name("docs").description("proxy to shadcn docs").argument("<components...>", "component names").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("-b, --base <base>", "the base to use either 'base' or 'radix'. defaults to project base.").option("--json", "output as JSON.", false).action(async (components, opts) => {
1744
+ const cwd = path.resolve(opts.cwd ?? process.cwd());
1745
+ const rawArgv = process.argv.slice(2);
1746
+ const args = [
1747
+ "docs",
1748
+ ...components,
1749
+ "--cwd",
1750
+ cwd
1751
+ ];
1752
+ if (opts.base) args.push("--base", opts.base);
1753
+ if (opts.json) args.push("--json");
1754
+ const passthroughArgs = extractPassthroughArgs(rawArgv, "docs");
1755
+ if (passthroughArgs.length > 0) args.push(...passthroughArgs);
1756
+ try {
1757
+ await runShadcnCommand({
1758
+ cwd,
1759
+ args
1760
+ });
1761
+ } catch {
1762
+ process.exit(1);
1763
+ }
1764
+ });
1765
+
437
1766
  //#endregion
438
1767
  //#region src/utils/tsconfig-utils.ts
439
1768
  /**
@@ -442,7 +1771,7 @@ async function runInit(options) {
442
1771
  function readTsConfig(projectRoot) {
443
1772
  try {
444
1773
  const tsconfigPath = resolve(projectRoot, "tsconfig.json");
445
- if (!existsSync$1(tsconfigPath)) return null;
1774
+ if (!existsSync(tsconfigPath)) return null;
446
1775
  const raw = readFileSync(tsconfigPath, "utf-8");
447
1776
  return JSON.parse(raw);
448
1777
  } catch {
@@ -471,7 +1800,7 @@ function resolveAliasPathUsingTsConfig(inputPath, projectRoot) {
471
1800
  }
472
1801
 
473
1802
  //#endregion
474
- //#region src/commands/docs.ts
1803
+ //#region src/commands/docs-build.ts
475
1804
  async function generateDocs({ cwd, outDir, verbose }) {
476
1805
  const DEBUG = process.env.BEJAMAS_DEBUG === "1" || process.env.BEJAMAS_DEBUG === "true" || verbose;
477
1806
  try {
@@ -480,7 +1809,7 @@ async function generateDocs({ cwd, outDir, verbose }) {
480
1809
  let probe = shellCwd;
481
1810
  for (let i = 0; i < 6 && probe; i += 1) {
482
1811
  const candidate = resolve(probe, "components.json");
483
- if (existsSync$1(candidate)) {
1812
+ if (existsSync(candidate)) {
484
1813
  projectRoot = probe;
485
1814
  try {
486
1815
  const raw = readFileSync(candidate, "utf-8");
@@ -510,7 +1839,7 @@ async function generateDocs({ cwd, outDir, verbose }) {
510
1839
  if (mapped.startsWith("./") || mapped.startsWith("../") || isAbsolute(mapped)) outResolved = mapped;
511
1840
  else {
512
1841
  const abs = resolveAliasPathUsingTsConfig(mapped, projectRoot);
513
- if (abs) outResolved = relative$1(projectRoot, abs);
1842
+ if (abs) outResolved = relative(projectRoot, abs);
514
1843
  }
515
1844
  if (!outResolved && mapped.startsWith("@/")) outResolved = mapped.replace(/^@\//, "src/");
516
1845
  const finalOut = outResolved ?? mapped;
@@ -534,23 +1863,23 @@ async function generateDocs({ cwd, outDir, verbose }) {
534
1863
  initial: (() => {
535
1864
  let current = shellCwd;
536
1865
  for (let i = 0; i < 6; i += 1) {
537
- if (existsSync$1(resolve(current, "packages/ui/package.json"))) {
1866
+ if (existsSync(resolve(current, "packages/ui/package.json"))) {
538
1867
  const abs = resolve(current, "packages/ui");
539
- return relative$1(shellCwd, abs) || abs;
1868
+ return relative(shellCwd, abs) || abs;
540
1869
  }
541
1870
  const parent = resolve(current, "..");
542
1871
  if (parent === current) break;
543
1872
  current = parent;
544
1873
  }
545
- if (existsSync$1(resolve(shellCwd, "node_modules/@bejamas/ui/package.json"))) {
1874
+ if (existsSync(resolve(shellCwd, "node_modules/@bejamas/ui/package.json"))) {
546
1875
  const abs = resolve(shellCwd, "node_modules/@bejamas/ui");
547
- return relative$1(shellCwd, abs) || abs;
1876
+ return relative(shellCwd, abs) || abs;
548
1877
  }
549
1878
  return "packages/ui";
550
1879
  })(),
551
1880
  validate: (val) => {
552
1881
  const p = resolve(shellCwd, val);
553
- return existsSync$1(resolve(p, "package.json")) ? true : `No package.json found in ${p}`;
1882
+ return existsSync(resolve(p, "package.json")) ? true : `No package.json found in ${p}`;
554
1883
  }
555
1884
  });
556
1885
  if (!uiRoot) {
@@ -583,7 +1912,7 @@ async function generateDocs({ cwd, outDir, verbose }) {
583
1912
  if (process.env.BEJAMAS_DOCS_CWD) logger.info(`Docs CWD: ${process.env.BEJAMAS_DOCS_CWD}`);
584
1913
  if (process.env.BEJAMAS_DOCS_OUT_DIR) logger.info(`Docs out: ${process.env.BEJAMAS_DOCS_OUT_DIR}`);
585
1914
  }
586
- const mod = await import("./generate-mdx-C8gXBOHp.js");
1915
+ const mod = await import("./generate-mdx-PPNpe9_J.js");
587
1916
  if (typeof mod.runDocsGenerator === "function") await mod.runDocsGenerator();
588
1917
  else throw new Error("Failed to load docs generator. Export 'runDocsGenerator' not found.");
589
1918
  } catch (err) {
@@ -591,7 +1920,7 @@ async function generateDocs({ cwd, outDir, verbose }) {
591
1920
  process.exit(1);
592
1921
  }
593
1922
  }
594
- const docs = new Command().name("docs:build").alias("docs").description("generate docs from @bejamas/ui components").option("-c, --cwd <cwd>", "path to UI working directory").option("-o, --out <outDir>", "output directory for generated MDX files").action(async (opts) => {
1923
+ const docsBuild = new Command().name("docs:build").description("generate docs from @bejamas/ui components").option("-c, --cwd <cwd>", "path to UI working directory").option("-o, --out <outDir>", "output directory for generated MDX files").action(async (opts) => {
595
1924
  await generateDocs({
596
1925
  cwd: opts.cwd,
597
1926
  outDir: opts.out,
@@ -651,7 +1980,7 @@ async function checkDocs({ cwd, json }) {
651
1980
  let probe = shellCwd;
652
1981
  for (let i = 0; i < 6 && probe; i += 1) {
653
1982
  const candidate = resolve(probe, "components.json");
654
- if (existsSync$1(candidate)) {
1983
+ if (existsSync(candidate)) {
655
1984
  projectRoot = probe;
656
1985
  try {
657
1986
  const raw = readFileSync(candidate, "utf-8");
@@ -689,19 +2018,19 @@ async function checkDocs({ cwd, json }) {
689
2018
  logger.error("Unable to locate @bejamas/ui. Use --cwd to specify the UI package path.");
690
2019
  process.exit(1);
691
2020
  }
692
- const componentsDir = join$1(uiRoot, "src", "components");
693
- if (!existsSync$1(componentsDir)) {
2021
+ const componentsDir = join(uiRoot, "src", "components");
2022
+ if (!existsSync(componentsDir)) {
694
2023
  logger.error(`Components directory not found: ${componentsDir}\n\nExpected structure: <uiRoot>/src/components/*.astro\nUse --cwd to specify a different UI package root.`);
695
2024
  process.exit(1);
696
2025
  }
697
- const files = readdirSync(componentsDir, { withFileTypes: true }).filter((e) => e.isFile() && extname$1(e.name).toLowerCase() === ".astro").map((e) => e.name).sort();
2026
+ const files = readdirSync(componentsDir, { withFileTypes: true }).filter((e) => e.isFile() && extname(e.name).toLowerCase() === ".astro").map((e) => e.name).sort();
698
2027
  if (files.length === 0) {
699
2028
  logger.warn("No .astro component files found.");
700
2029
  process.exit(0);
701
2030
  }
702
2031
  const results = [];
703
2032
  for (const file of files) {
704
- const status = checkComponentDocs(join$1(componentsDir, file), file);
2033
+ const status = checkComponentDocs(join(componentsDir, file), file);
705
2034
  results.push(status);
706
2035
  }
707
2036
  const complete = results.filter((r) => r.status === "complete");
@@ -768,174 +2097,8 @@ const docsCheck = new Command().name("docs:check").description("check documentat
768
2097
  });
769
2098
  });
770
2099
 
771
- //#endregion
772
- //#region src/utils/astro-imports.ts
773
- function updateImportAliases(moduleSpecifier, config, isRemote = false) {
774
- if (!moduleSpecifier.startsWith("@/") && !isRemote) return moduleSpecifier;
775
- let specifier = moduleSpecifier;
776
- if (isRemote && specifier.startsWith("@/")) specifier = specifier.replace(/^@\//, "@/registry/new-york/");
777
- if (!specifier.startsWith("@/registry/")) {
778
- const alias = config.aliases.components.split("/")[0];
779
- return specifier.replace(/^@\//, `${alias}/`);
780
- }
781
- if (specifier.match(/^@\/registry\/(.+)\/ui/)) return specifier.replace(/^@\/registry\/(.+)\/ui/, config.aliases.ui ?? `${config.aliases.components}/ui`);
782
- if (config.aliases.components && specifier.match(/^@\/registry\/(.+)\/components/)) return specifier.replace(/^@\/registry\/(.+)\/components/, config.aliases.components);
783
- if (config.aliases.lib && specifier.match(/^@\/registry\/(.+)\/lib/)) return specifier.replace(/^@\/registry\/(.+)\/lib/, config.aliases.lib);
784
- if (config.aliases.hooks && specifier.match(/^@\/registry\/(.+)\/hooks/)) return specifier.replace(/^@\/registry\/(.+)\/hooks/, config.aliases.hooks);
785
- return specifier.replace(/^@\/registry\/[^/]+/, config.aliases.components);
786
- }
787
- function rewriteAstroImports(content, config) {
788
- let updated = content;
789
- const utilsAlias = config.aliases?.utils;
790
- const utilsImport = `${typeof utilsAlias === "string" && utilsAlias.includes("/") ? utilsAlias.split("/")[0] : "@"}/lib/utils`;
791
- updated = updated.replace(/import\s+([\s\S]*?)\s+from\s+["']([^"']+)["']/g, (full, importsPart, specifier) => {
792
- const next = updateImportAliases(specifier, config, false);
793
- let finalSpec = next;
794
- if (typeof importsPart === "string" && importsPart.split(/[{},\s]/).some((part) => part === "cn") && config.aliases.utils && (next === utilsImport || next === "@/lib/utils")) finalSpec = utilsImport === next ? next.replace(utilsImport, config.aliases.utils) : config.aliases.utils;
795
- if (finalSpec === specifier) return full;
796
- return full.replace(specifier, finalSpec);
797
- });
798
- updated = updated.replace(/import\s+["']([^"']+)["']/g, (full, specifier) => {
799
- const next = updateImportAliases(specifier, config, false);
800
- if (next === specifier) return full;
801
- return full.replace(specifier, next);
802
- });
803
- return updated;
804
- }
805
- async function fixAstroImports(cwd, isVerbose) {
806
- const config = await getConfig(cwd);
807
- if (!config) return;
808
- const searchRoots = new Set([config.resolvedPaths.components, config.resolvedPaths.ui]);
809
- for (const root of Array.from(searchRoots)) {
810
- if (!root) continue;
811
- const astroFiles = await fg("**/*.astro", {
812
- cwd: root,
813
- absolute: true,
814
- dot: false
815
- });
816
- for (const filePath of astroFiles) {
817
- const original = await fs.readFile(filePath, "utf8");
818
- const rewritten = rewriteAstroImports(original, config);
819
- if (rewritten === original) continue;
820
- await fs.writeFile(filePath, rewritten, "utf8");
821
- if (isVerbose) logger.info(`[bejamas-ui] fixed imports in ${path$1.relative(cwd, filePath)}`);
822
- }
823
- }
824
- }
825
-
826
- //#endregion
827
- //#region src/utils/reorganize-components.ts
828
- /**
829
- * Fetches a registry item JSON from the registry URL.
830
- */
831
- async function fetchRegistryItem(componentName, registryUrl) {
832
- const url = `${registryUrl}/styles/new-york-v4/${componentName}.json`;
833
- try {
834
- const response = await fetch(url);
835
- if (!response.ok) {
836
- const fallbackUrl = `${registryUrl}/${componentName}.json`;
837
- const fallbackResponse = await fetch(fallbackUrl);
838
- if (!fallbackResponse.ok) return null;
839
- return await fallbackResponse.json();
840
- }
841
- return await response.json();
842
- } catch {
843
- return null;
844
- }
845
- }
846
- /**
847
- * Extracts the subfolder name from registry file paths.
848
- * E.g., "components/ui/avatar/Avatar.astro" → "avatar"
849
- */
850
- function getSubfolderFromPaths(files) {
851
- const uiFiles = files.filter((f) => f.type === "registry:ui");
852
- if (uiFiles.length < 2) return null;
853
- const subfolders = /* @__PURE__ */ new Set();
854
- for (const file of uiFiles) {
855
- const parts = file.path.split("/");
856
- const uiIndex = parts.indexOf("ui");
857
- if (uiIndex !== -1 && parts.length > uiIndex + 2) subfolders.add(parts[uiIndex + 1]);
858
- }
859
- if (subfolders.size === 1) return Array.from(subfolders)[0];
860
- if (uiFiles.length > 0) {
861
- const firstPath = uiFiles[0].path;
862
- const dirname$1 = path$1.dirname(firstPath);
863
- const folderName = path$1.basename(dirname$1);
864
- if (folderName && folderName !== "ui") return folderName;
865
- }
866
- return null;
867
- }
868
- /**
869
- * Checks if a path exists.
870
- */
871
- async function pathExists(filePath) {
872
- try {
873
- await fs.access(filePath);
874
- return true;
875
- } catch {
876
- return false;
877
- }
878
- }
879
- /**
880
- * Reorganizes multi-file components into correct subfolders.
881
- * Only moves files from FLAT location to subfolder.
882
- * Does NOT touch files already in subfolders.
883
- * E.g., moves `uiDir/Avatar.astro` to `uiDir/avatar/Avatar.astro`.
884
- * Returns info about moved files for display purposes.
885
- */
886
- async function reorganizeComponents(components, uiDir, registryUrl, verbose) {
887
- const result = {
888
- totalMoved: 0,
889
- movedFiles: [],
890
- skippedFiles: []
891
- };
892
- if (!uiDir || components.length === 0) return result;
893
- for (const componentName of components) try {
894
- const registryItem = await fetchRegistryItem(componentName, registryUrl);
895
- if (!registryItem) {
896
- if (verbose) logger.info(`[bejamas-ui] Could not fetch registry for ${componentName}, skipping reorganization`);
897
- continue;
898
- }
899
- const subfolder = getSubfolderFromPaths(registryItem.files);
900
- if (!subfolder) {
901
- if (verbose) logger.info(`[bejamas-ui] ${componentName} is single-file or has no subfolder, skipping`);
902
- continue;
903
- }
904
- const uiFiles = registryItem.files.filter((f) => f.type === "registry:ui");
905
- const targetDir = path$1.join(uiDir, subfolder);
906
- let movedCount = 0;
907
- for (const file of uiFiles) {
908
- const filename = path$1.basename(file.path);
909
- const flatPath = path$1.join(uiDir, filename);
910
- const targetPath = path$1.join(targetDir, filename);
911
- if (!await pathExists(flatPath)) continue;
912
- if (await pathExists(targetPath)) {
913
- try {
914
- await fs.unlink(flatPath);
915
- result.skippedFiles.push(`${subfolder}/${filename}`);
916
- if (verbose) logger.info(`[bejamas-ui] Removed flat duplicate: ${filename} (${subfolder}/${filename} exists)`);
917
- } catch {
918
- result.skippedFiles.push(`${subfolder}/${filename}`);
919
- }
920
- continue;
921
- }
922
- await fs.mkdir(targetDir, { recursive: true });
923
- await fs.rename(flatPath, targetPath);
924
- movedCount++;
925
- result.totalMoved++;
926
- result.movedFiles.push(`${subfolder}/${filename}`);
927
- if (verbose) logger.info(`[bejamas-ui] Moved ${filename} → ${subfolder}/${filename}`);
928
- }
929
- if (movedCount > 0 && verbose) logger.info(`[bejamas-ui] Reorganized ${componentName} into ${subfolder}/`);
930
- } catch (err) {
931
- if (verbose) logger.warn(`[bejamas-ui] Failed to reorganize ${componentName}: ${err}`);
932
- }
933
- return result;
934
- }
935
-
936
2100
  //#endregion
937
2101
  //#region src/commands/add.ts
938
- const DEFAULT_REGISTRY_URL = "https://ui.bejamas.com/r";
939
2102
  function extractOptionsForShadcn(rawArgv, cmd) {
940
2103
  if (typeof cmd.getOptionValueSource === "function") {
941
2104
  const opts = cmd.optsWithGlobals();
@@ -953,13 +2116,25 @@ function extractOptionsForShadcn(rawArgv, cmd) {
953
2116
  const value = opts[key];
954
2117
  if (typeof value === "string") forwarded$1.push(flag, value);
955
2118
  };
2119
+ const addOptionalString = (key, flag) => {
2120
+ if (getSource(key) !== "cli") return;
2121
+ const value = opts[key];
2122
+ if (value === true) {
2123
+ forwarded$1.push(flag);
2124
+ return;
2125
+ }
2126
+ if (typeof value === "string") forwarded$1.push(flag, value);
2127
+ };
956
2128
  addBoolean("yes", "--yes");
957
2129
  addBoolean("overwrite", "--overwrite");
2130
+ addBoolean("dryRun", "--dry-run");
958
2131
  addString("cwd", "--cwd");
959
2132
  addBoolean("all", "--all");
960
2133
  addString("path", "--path");
961
2134
  addBoolean("silent", "--silent");
962
2135
  addBoolean("srcDir", "--src-dir", "--no-src-dir");
2136
+ addOptionalString("diff", "--diff");
2137
+ addOptionalString("view", "--view");
963
2138
  const addIndex$1 = rawArgv.findIndex((arg) => arg === "add");
964
2139
  if (addIndex$1 !== -1) {
965
2140
  const rest$1 = rawArgv.slice(addIndex$1 + 1);
@@ -976,13 +2151,15 @@ function extractOptionsForShadcn(rawArgv, cmd) {
976
2151
  "-c",
977
2152
  "--cwd",
978
2153
  "-p",
979
- "--path"
2154
+ "--path",
2155
+ "--diff",
2156
+ "--view"
980
2157
  ]);
981
2158
  const filteredFlags = new Set(["-v", "--verbose"]);
982
- for (let i = 0; i < rest.length; i += 1) {
983
- const token = rest[i];
2159
+ for (let index = 0; index < rest.length; index += 1) {
2160
+ const token = rest[index];
984
2161
  if (token === "--") {
985
- forwarded.push("--", ...rest.slice(i + 1));
2162
+ forwarded.push("--", ...rest.slice(index + 1));
986
2163
  break;
987
2164
  }
988
2165
  if (!token.startsWith("-")) continue;
@@ -990,36 +2167,38 @@ function extractOptionsForShadcn(rawArgv, cmd) {
990
2167
  forwarded.push(token);
991
2168
  if (token.includes("=")) continue;
992
2169
  if (optionsWithValues.has(token)) {
993
- const next = rest[i + 1];
2170
+ const next = rest[index + 1];
994
2171
  if (next) {
995
2172
  forwarded.push(next);
996
- i += 1;
2173
+ index += 1;
997
2174
  }
998
2175
  }
999
2176
  }
1000
2177
  return forwarded;
1001
2178
  }
1002
- /** Build maps for path rewriting, handling filename collisions */
1003
- async function buildSubfolderMap(components, registryUrl) {
2179
+ function hasInspectionFlags(forwardedOptions) {
2180
+ return forwardedOptions.includes("--dry-run") || forwardedOptions.includes("--diff") || forwardedOptions.includes("--view");
2181
+ }
2182
+ function formatSkippedFilesHeading(count, overwriteUsed) {
2183
+ const noun = `file${count === 1 ? "" : "s"}`;
2184
+ if (overwriteUsed) return `Skipped ${count} ${noun}: (files might be identical)`;
2185
+ return `Skipped ${count} ${noun}: (files might be identical, use --overwrite to overwrite)`;
2186
+ }
2187
+ async function buildSubfolderMap(components, uiDir, registryUrl, style) {
1004
2188
  const filenameToSubfolders = /* @__PURE__ */ new Map();
1005
- const componentInfo = /* @__PURE__ */ new Map();
2189
+ let requiresReorganization = false;
1006
2190
  for (const componentName of components) {
1007
- const registryItem = await fetchRegistryItem(componentName, registryUrl);
2191
+ const registryItem = await fetchRegistryItem(componentName, registryUrl, style);
1008
2192
  if (!registryItem) continue;
2193
+ if (shouldReorganizeRegistryUiFiles(registryItem.files, uiDir)) requiresReorganization = true;
1009
2194
  const subfolder = getSubfolderFromPaths(registryItem.files);
1010
2195
  if (!subfolder) continue;
1011
- const files = [];
1012
2196
  for (const file of registryItem.files) if (file.type === "registry:ui") {
1013
- const filename = path$1.basename(file.path);
1014
- files.push(filename);
2197
+ const filename = path.basename(file.path);
1015
2198
  const subfolders = filenameToSubfolders.get(filename) || [];
1016
2199
  subfolders.push(subfolder);
1017
2200
  filenameToSubfolders.set(filename, subfolders);
1018
2201
  }
1019
- componentInfo.set(subfolder, {
1020
- subfolder,
1021
- files
1022
- });
1023
2202
  }
1024
2203
  const uniqueMap = /* @__PURE__ */ new Map();
1025
2204
  const sharedFilenames = /* @__PURE__ */ new Set();
@@ -1029,35 +2208,29 @@ async function buildSubfolderMap(components, registryUrl) {
1029
2208
  });
1030
2209
  return {
1031
2210
  uniqueMap,
1032
- componentInfo,
1033
- sharedFilenames
2211
+ sharedFilenames,
2212
+ requiresReorganization
1034
2213
  };
1035
2214
  }
1036
- /**
1037
- * Rewrite file paths to include correct subfolders.
1038
- * Handles shared filenames (like index.ts) by tracking current component context.
1039
- */
1040
2215
  function rewritePaths(paths, mapResult) {
1041
- const { uniqueMap, componentInfo, sharedFilenames } = mapResult;
2216
+ const { uniqueMap, sharedFilenames } = mapResult;
1042
2217
  let currentSubfolder = null;
1043
2218
  return paths.map((filePath) => {
1044
- const filename = path$1.basename(filePath);
1045
- const parentDir = path$1.basename(path$1.dirname(filePath));
2219
+ const filename = path.basename(filePath);
2220
+ const parentDir = path.basename(path.dirname(filePath));
1046
2221
  const uniqueMapping = uniqueMap.get(filename);
1047
2222
  if (uniqueMapping) {
1048
- const expectedSubfolder = path$1.dirname(uniqueMapping);
2223
+ const expectedSubfolder = path.dirname(uniqueMapping);
1049
2224
  currentSubfolder = expectedSubfolder;
1050
- if (parentDir !== expectedSubfolder) return `${path$1.dirname(filePath)}/${uniqueMapping}`;
2225
+ if (parentDir !== expectedSubfolder) return `${path.dirname(filePath)}/${uniqueMapping}`;
1051
2226
  return filePath;
1052
2227
  }
1053
2228
  if (sharedFilenames.has(filename) && currentSubfolder) {
1054
- const expectedSubfolder = currentSubfolder;
1055
- if (parentDir !== expectedSubfolder) return `${path$1.dirname(filePath)}/${expectedSubfolder}/${filename}`;
2229
+ if (parentDir !== currentSubfolder) return `${path.dirname(filePath)}/${currentSubfolder}/${filename}`;
1056
2230
  }
1057
2231
  return filePath;
1058
2232
  });
1059
2233
  }
1060
- /** Fetch available components from the registry */
1061
2234
  async function fetchAvailableComponents(registryUrl) {
1062
2235
  const indexUrl = `${registryUrl}/index.json`;
1063
2236
  const response = await fetch(indexUrl);
@@ -1065,14 +2238,13 @@ async function fetchAvailableComponents(registryUrl) {
1065
2238
  const data = await response.json();
1066
2239
  return Array.isArray(data) ? data : [];
1067
2240
  }
1068
- /** Prompt user to select components interactively */
1069
2241
  async function promptForComponents(registryUrl) {
1070
2242
  const checkingSpinner = spinner("Checking registry.").start();
1071
2243
  let components;
1072
2244
  try {
1073
2245
  components = await fetchAvailableComponents(registryUrl);
1074
2246
  checkingSpinner.succeed();
1075
- } catch (error) {
2247
+ } catch {
1076
2248
  checkingSpinner.fail();
1077
2249
  logger.error("Failed to fetch available components from registry.");
1078
2250
  return null;
@@ -1085,9 +2257,9 @@ async function promptForComponents(registryUrl) {
1085
2257
  type: "autocompleteMultiselect",
1086
2258
  name: "selected",
1087
2259
  message: "Which components would you like to add?",
1088
- choices: components.filter((c) => !c.type || c.type === "registry:ui").map((c) => ({
1089
- title: c.name,
1090
- value: c.name
2260
+ choices: components.filter((component) => !component.type || component.type === "registry:ui").map((component) => ({
2261
+ title: component.name,
2262
+ value: component.name
1091
2263
  })),
1092
2264
  hint: "- Space to select. Return to submit.",
1093
2265
  instructions: false
@@ -1095,7 +2267,6 @@ async function promptForComponents(registryUrl) {
1095
2267
  if (!selected) return null;
1096
2268
  return selected;
1097
2269
  }
1098
- /** Parse shadcn output to extract file lists (stdout has paths, stderr has headers) */
1099
2270
  function parseShadcnOutput(stdout, stderr) {
1100
2271
  const result = {
1101
2272
  created: [],
@@ -1122,44 +2293,34 @@ function parseShadcnOutput(stdout, stderr) {
1122
2293
  if (!allPaths.includes(filePath)) allPaths.push(filePath);
1123
2294
  }
1124
2295
  }
1125
- let idx = 0;
1126
- for (let i = 0; i < createdCount && idx < allPaths.length; i++) result.created.push(allPaths[idx++]);
1127
- for (let i = 0; i < updatedCount && idx < allPaths.length; i++) result.updated.push(allPaths[idx++]);
1128
- for (let i = 0; i < skippedCount && idx < allPaths.length; i++) result.skipped.push(allPaths[idx++]);
2296
+ let index = 0;
2297
+ for (let count = 0; count < createdCount && index < allPaths.length; count += 1) {
2298
+ result.created.push(allPaths[index]);
2299
+ index += 1;
2300
+ }
2301
+ for (let count = 0; count < updatedCount && index < allPaths.length; count += 1) {
2302
+ result.updated.push(allPaths[index]);
2303
+ index += 1;
2304
+ }
2305
+ for (let count = 0; count < skippedCount && index < allPaths.length; count += 1) {
2306
+ result.skipped.push(allPaths[index]);
2307
+ index += 1;
2308
+ }
1129
2309
  return result;
1130
2310
  }
1131
- async function addComponents(packages, forwardedOptions, isVerbose, isSilent, subfolderMapResult) {
1132
- const runner = await getPackageRunner(process.cwd());
2311
+ async function addComponents(cwd, packages, forwardedOptions, isVerbose, isSilent, inspectionMode) {
1133
2312
  const env = {
1134
2313
  ...process.env,
1135
- REGISTRY_URL: process.env.REGISTRY_URL || DEFAULT_REGISTRY_URL
2314
+ REGISTRY_URL: resolveRegistryUrl()
1136
2315
  };
1137
- const autoFlags = [];
1138
- if (!forwardedOptions.includes("--yes")) autoFlags.push("--yes");
1139
- const baseArgs = [
1140
- "shadcn@latest",
1141
- "add",
1142
- ...packages,
1143
- ...autoFlags,
1144
- ...forwardedOptions
1145
- ];
1146
- let cmd = "npx";
1147
- let args = ["-y", ...baseArgs];
1148
- if (runner === "bunx") {
1149
- cmd = "bunx";
1150
- args = baseArgs;
1151
- } else if (runner === "pnpm dlx") {
1152
- cmd = "pnpm";
1153
- args = ["dlx", ...baseArgs];
1154
- } else if (runner === "npx") {
1155
- cmd = "npx";
1156
- args = ["-y", ...baseArgs];
1157
- }
1158
- if (isVerbose) logger.info(`[bejamas-ui] ${cmd} ${args.join(" ")}`);
2316
+ await ensurePinnedShadcnExecPrefix();
2317
+ const invocation = buildPinnedShadcnInvocation(buildShadcnAddArgs(packages, forwardedOptions));
2318
+ if (isVerbose) logger.info(`[bejamas-ui] ${invocation.cmd} ${invocation.args.join(" ")}`);
1159
2319
  const registrySpinner = spinner("Checking registry.", { silent: isSilent });
1160
2320
  registrySpinner.start();
1161
2321
  try {
1162
- const result = await execa(cmd, args, {
2322
+ const result = await execa(invocation.cmd, invocation.args, {
2323
+ cwd,
1163
2324
  env,
1164
2325
  input: "n\nn\nn\nn\nn\nn\nn\nn\nn\nn\n",
1165
2326
  stdout: "pipe",
@@ -1174,34 +2335,54 @@ async function addComponents(packages, forwardedOptions, isVerbose, isSilent, su
1174
2335
  logger.info(`[bejamas-ui] Raw stdout: ${stdout}`);
1175
2336
  logger.info(`[bejamas-ui] Raw stderr: ${stderr}`);
1176
2337
  }
1177
- const parsed = parseShadcnOutput(stdout, stderr);
2338
+ if (inspectionMode) {
2339
+ if (stdout) process.stdout.write(stdout.endsWith("\n") ? stdout : `${stdout}\n`);
2340
+ if (stderr) process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
2341
+ }
2342
+ const parsed = inspectionMode ? {
2343
+ created: [],
2344
+ updated: [],
2345
+ skipped: []
2346
+ } : parseShadcnOutput(stdout, stderr);
1178
2347
  if (result.exitCode !== 0) {
1179
2348
  if (result.stderr) logger.error(result.stderr);
1180
2349
  process.exit(result.exitCode);
1181
2350
  }
1182
2351
  return parsed;
1183
- } catch (err) {
2352
+ } catch {
1184
2353
  registrySpinner.fail();
1185
2354
  logger.error("Failed to add components");
1186
2355
  process.exit(1);
1187
2356
  }
1188
2357
  }
1189
- const add = new Command().name("add").description("Add components via shadcn@latest using registry URLs").argument("[components...]", "Component package names to add").option("-y, --yes", "skip confirmation prompt.", false).option("-o, --overwrite", "overwrite existing files.", false).option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("-a, --all", "add all available components", false).option("-p, --path <path>", "the path to add the component to.").option("-s, --silent", "mute output.", false).option("--src-dir", "use the src directory when creating a new project.", false).option("--no-src-dir", "do not use the src directory when creating a new project.").action(async function action(packages, _opts, cmd) {
2358
+ function buildShadcnAddArgs(packages, forwardedOptions) {
2359
+ const autoFlags = [];
2360
+ if (!forwardedOptions.includes("--yes")) autoFlags.push("--yes");
2361
+ return [
2362
+ "add",
2363
+ ...packages,
2364
+ ...autoFlags,
2365
+ ...forwardedOptions
2366
+ ];
2367
+ }
2368
+ const add = new Command().name("add").description("Add components via the Bejamas-managed shadcn registry flow").argument("[components...]", "Component package names to add").option("-y, --yes", "skip confirmation prompt.", false).option("-o, --overwrite", "overwrite existing files.", false).option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("-a, --all", "add all available components", false).option("-p, --path <path>", "the path to add the component to.").option("-s, --silent", "mute output.", false).option("--dry-run", "preview changes without writing files.", false).option("--diff [path]", "show diff for a file.").option("--view [path]", "show file contents.").option("--src-dir", "use the src directory when creating a new project.", false).option("--no-src-dir", "do not use the src directory when creating a new project.").action(async function action(packages, _opts, cmd) {
1190
2369
  const root = cmd?.parent;
1191
2370
  const verbose = Boolean(root?.opts?.().verbose);
1192
2371
  const forwardedOptions = extractOptionsForShadcn(process.argv.slice(2), cmd);
1193
2372
  const opts = typeof cmd.optsWithGlobals === "function" ? cmd.optsWithGlobals() : cmd.opts?.() ?? {};
2373
+ const inspectionMode = hasInspectionFlags(forwardedOptions);
2374
+ const overwriteUsed = forwardedOptions.includes("--overwrite") || forwardedOptions.includes("-o");
1194
2375
  const cwd = opts.cwd || process.cwd();
1195
2376
  let componentsToAdd = packages || [];
1196
2377
  const wantsAll = Boolean(opts.all);
1197
2378
  const isSilent = opts.silent || false;
1198
- const registryUrl = process.env.REGISTRY_URL || DEFAULT_REGISTRY_URL;
2379
+ const registryUrl = resolveRegistryUrl();
1199
2380
  if (wantsAll && componentsToAdd.length === 0) {
1200
2381
  const fetchingSpinner = spinner("Fetching available components.", { silent: isSilent }).start();
1201
2382
  try {
1202
- componentsToAdd = (await fetchAvailableComponents(registryUrl)).filter((c) => !c.type || c.type === "registry:ui").map((c) => c.name);
2383
+ componentsToAdd = (await fetchAvailableComponents(registryUrl)).filter((component) => !component.type || component.type === "registry:ui").map((component) => component.name);
1203
2384
  fetchingSpinner.succeed();
1204
- } catch (error) {
2385
+ } catch {
1205
2386
  fetchingSpinner.fail();
1206
2387
  logger.error("Failed to fetch available components from registry.");
1207
2388
  process.exit(1);
@@ -1227,19 +2408,35 @@ const add = new Command().name("add").description("Add components via shadcn@lat
1227
2408
  logger.info(`[bejamas-ui] uiDir: ${uiDir}`);
1228
2409
  logger.info(`[bejamas-ui] aliases.ui: ${uiConfig?.aliases?.ui || "not set"}`);
1229
2410
  }
2411
+ const activeStyle = uiConfig?.style || config?.style || "bejamas-juno";
1230
2412
  const totalComponents = componentsToAdd.length;
1231
- for (let i = 0; i < componentsToAdd.length; i++) {
1232
- const component = componentsToAdd[i];
2413
+ for (let index = 0; index < componentsToAdd.length; index += 1) {
2414
+ const component = componentsToAdd[index];
1233
2415
  if (totalComponents > 1 && !isSilent) {
1234
2416
  logger.break();
1235
- logger.info(highlighter.info(`[${i + 1}/${totalComponents}]`) + ` Adding ${highlighter.success(component)}...`);
2417
+ logger.info(highlighter.info(`[${index + 1}/${totalComponents}]`) + ` Adding ${highlighter.success(component)}...`);
2418
+ }
2419
+ const subfolderMapResult = inspectionMode ? {
2420
+ uniqueMap: /* @__PURE__ */ new Map(),
2421
+ sharedFilenames: /* @__PURE__ */ new Set(),
2422
+ requiresReorganization: false
2423
+ } : await buildSubfolderMap([component], uiDir, registryUrl, activeStyle);
2424
+ const parsed = await addComponents(cwd, [component], forwardedOptions, verbose, isSilent, inspectionMode);
2425
+ if (!inspectionMode) {
2426
+ await syncManagedTailwindCss(cwd);
2427
+ const registryItem = await fetchRegistryItem(component, registryUrl, activeStyle);
2428
+ if (registryItem?.type === "registry:font") {
2429
+ const nextFont = toManagedAstroFont(registryItem.name);
2430
+ if (nextFont) {
2431
+ await syncAstroFontsInProject(cwd, mergeManagedAstroFonts(await readManagedAstroFontsFromProject(cwd), nextFont), nextFont.cssVariable);
2432
+ await syncAstroManagedFontCss(cwd, nextFont.cssVariable);
2433
+ await cleanupAstroFontPackages(cwd);
2434
+ }
2435
+ }
1236
2436
  }
1237
- const subfolderMapResult = await buildSubfolderMap([component], registryUrl);
1238
- const parsed = await addComponents([component], forwardedOptions, verbose, isSilent, subfolderMapResult);
1239
2437
  let skippedCount = 0;
1240
- if (uiDir) skippedCount = (await reorganizeComponents([component], uiDir, registryUrl, verbose)).skippedFiles.length;
1241
- if (!isSilent) {
1242
- uiDir && path$1.relative(cwd, uiDir);
2438
+ if (!inspectionMode && uiDir && subfolderMapResult.requiresReorganization) skippedCount = (await reorganizeComponents([component], uiDir, registryUrl, verbose, activeStyle)).skippedFiles.length;
2439
+ if (!isSilent && !inspectionMode) {
1243
2440
  const actuallyCreated = Math.max(0, parsed.created.length - skippedCount);
1244
2441
  if (actuallyCreated > 0) {
1245
2442
  const createdPaths = rewritePaths(parsed.created.slice(0, actuallyCreated), subfolderMapResult);
@@ -1254,13 +2451,36 @@ const add = new Command().name("add").description("Add components via shadcn@lat
1254
2451
  if (skippedCount > 0) logger.info(`Skipped ${skippedCount} file${skippedCount > 1 ? "s" : ""}: (already exists)`);
1255
2452
  if (parsed.skipped.length > 0) {
1256
2453
  const skippedPaths = rewritePaths(parsed.skipped, subfolderMapResult);
1257
- logger.info(`Skipped ${skippedPaths.length} file${skippedPaths.length > 1 ? "s" : ""}: (use --overwrite)`);
2454
+ logger.info(formatSkippedFilesHeading(skippedPaths.length, overwriteUsed));
1258
2455
  for (const file of skippedPaths) logger.log(` ${highlighter.info("-")} ${file}`);
1259
2456
  }
1260
2457
  if (actuallyCreated === 0 && parsed.updated.length === 0 && skippedCount === 0 && parsed.skipped.length === 0) logger.info("Already up to date.");
1261
2458
  }
1262
2459
  }
1263
- await fixAstroImports(cwd, verbose);
2460
+ if (!inspectionMode) await fixAstroImports(cwd, verbose);
2461
+ });
2462
+
2463
+ //#endregion
2464
+ //#region src/commands/info.ts
2465
+ const info = new Command().name("info").description("proxy to shadcn info").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--json", "output as JSON.", false).action(async (opts) => {
2466
+ const cwd = path.resolve(opts.cwd ?? process.cwd());
2467
+ const rawArgv = process.argv.slice(2);
2468
+ const args = [
2469
+ "info",
2470
+ "--cwd",
2471
+ cwd
2472
+ ];
2473
+ if (opts.json) args.push("--json");
2474
+ const passthroughArgs = extractPassthroughArgs(rawArgv, "info");
2475
+ if (passthroughArgs.length > 0) args.push(...passthroughArgs);
2476
+ try {
2477
+ await runShadcnCommand({
2478
+ cwd,
2479
+ args
2480
+ });
2481
+ } catch {
2482
+ process.exit(1);
2483
+ }
1264
2484
  });
1265
2485
 
1266
2486
  //#endregion
@@ -1269,7 +2489,9 @@ const pkg = createRequire(import.meta.url)("../package.json");
1269
2489
  const program = new Command().name("bejamas").description("bejamas/ui cli").configureHelp({ helpWidth: Math.min(100, process.stdout.columns || 100) }).version(pkg.version, "-v, --version", "output the version number");
1270
2490
  program.addCommand(init);
1271
2491
  program.addCommand(add);
2492
+ program.addCommand(info);
1272
2493
  program.addCommand(docs);
2494
+ program.addCommand(docsBuild);
1273
2495
  program.addCommand(docsCheck);
1274
2496
  program.parse(process.argv);
1275
2497
  var src_default = program;