mirror of
https://github.com/cosscom/coss.git
synced 2026-09-14 20:06:50 +08:00
320871cdbf
* wip * various improvements * improve agent skills per agentskills.io review - Rename all 45 primitive dirs to match name fields (e.g. dialog/ -> coss-dialog/) - Write unique, trigger-optimized descriptions for all primitives - Replace user-invocable with compatibility field and metadata block - Add 'When NOT to use' sections to 19 confusable primitives - Convert rule files to proper skill dirs with SKILL.md frontmatter - Split base-vs-radix.md into concise skill + references/component-index.md - Add component registry index reference (references/component-registry.md) - Move cli.md to references/ directory - Update root SKILL.md with updated paths, component discovery section, metadata Co-Authored-By: pasquale <pasqualevitiello@gmail.com> * remove redundant coss- prefix from skill directory names and name fields The skills already live under apps/ui/skills/coss/, so the coss- prefix on each directory/name was redundant. Reverts to simple names like dialog/, select/, styling/, migration/, etc. Co-Authored-By: pasquale <pasqualevitiello@gmail.com> * refactoring * mc * mc * mc * changes * fix * update shadcn cli + reg font + mc * fix * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
218 lines
5.1 KiB
TypeScript
218 lines
5.1 KiB
TypeScript
import { promises as fs } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { Project, ScriptKind } from "ts-morph";
|
|
import { Index } from "@/registry/__index__";
|
|
|
|
type RegistryFile = {
|
|
path: string;
|
|
type?: string;
|
|
target?: string;
|
|
content?: string;
|
|
};
|
|
|
|
export function getRegistryComponent(name: string) {
|
|
return Index[name]?.component;
|
|
}
|
|
|
|
export async function getRegistryItem(name: string) {
|
|
const item = Index[name];
|
|
|
|
if (!item) {
|
|
return null;
|
|
}
|
|
|
|
// Convert all file paths to object.
|
|
// TODO: remove when we migrate to new registry.
|
|
if (Array.isArray(item.files)) {
|
|
item.files = item.files.map((file: unknown) =>
|
|
typeof file === "string" ? { path: file } : file,
|
|
);
|
|
}
|
|
|
|
const files: RegistryFile[] = Array.isArray(item.files) ? item.files : [];
|
|
const processedFiles: RegistryFile[] = [];
|
|
|
|
for (const file of files) {
|
|
const content = await getFileContent(file);
|
|
const relativePath = path.relative(process.cwd(), file.path);
|
|
|
|
processedFiles.push({
|
|
...file,
|
|
content,
|
|
path: relativePath,
|
|
});
|
|
}
|
|
|
|
// Fix file paths.
|
|
const finalFiles = fixFilePaths(processedFiles);
|
|
|
|
return {
|
|
...item,
|
|
files: finalFiles,
|
|
};
|
|
}
|
|
|
|
async function getFileContent(file: RegistryFile) {
|
|
const raw = await fs.readFile(file.path, "utf-8");
|
|
|
|
const project = new Project({
|
|
compilerOptions: {},
|
|
});
|
|
|
|
const tempFile = await createTempSourceFile(file.path);
|
|
const sourceFile = project.createSourceFile(tempFile, raw, {
|
|
scriptKind: ScriptKind.TSX,
|
|
});
|
|
|
|
// Remove meta variables.
|
|
// removeVariable(sourceFile, "iframeHeight")
|
|
// removeVariable(sourceFile, "containerClassName")
|
|
// removeVariable(sourceFile, "description")
|
|
|
|
let code = sourceFile.getFullText();
|
|
|
|
// Some registry items uses default export.
|
|
// We want to use named export instead.
|
|
// TODO: do we really need this? - @shadcn.
|
|
// if (file.type !== "registry:page") {
|
|
// code = code.replaceAll("export default", "export")
|
|
// }
|
|
|
|
// Fix imports.
|
|
code = fixImport(code);
|
|
|
|
return code;
|
|
}
|
|
|
|
function getFileTarget(file: RegistryFile) {
|
|
let target = file.target;
|
|
|
|
if (!target || target === "") {
|
|
const fileName = file.path.split("/").pop();
|
|
if (
|
|
file.type === "registry:block" ||
|
|
file.type === "registry:component" ||
|
|
file.type === "registry:example"
|
|
) {
|
|
target = `components/${fileName}`;
|
|
}
|
|
|
|
if (file.type === "registry:ui") {
|
|
target = `components/ui/${fileName}`;
|
|
}
|
|
|
|
if (file.type === "registry:hook") {
|
|
target = `hooks/${fileName}`;
|
|
}
|
|
|
|
if (file.type === "registry:lib") {
|
|
target = file.path.includes("base-ui/")
|
|
? `base-ui/${fileName}`
|
|
: `lib/${fileName}`;
|
|
}
|
|
}
|
|
|
|
return target ?? "";
|
|
}
|
|
|
|
async function createTempSourceFile(filename: string) {
|
|
const dir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-"));
|
|
return path.join(dir, filename);
|
|
}
|
|
|
|
function fixFilePaths(files: RegistryFile[]) {
|
|
if (!files) {
|
|
return [];
|
|
}
|
|
|
|
// Resolve all paths relative to the first file's directory.
|
|
const firstFilePath = files[0]?.path;
|
|
if (!firstFilePath) {
|
|
return [];
|
|
}
|
|
const firstFilePathDir = path.dirname(firstFilePath);
|
|
|
|
return files.map((file) => {
|
|
return {
|
|
...file,
|
|
path: path.relative(firstFilePathDir, file.path),
|
|
target: getFileTarget(file),
|
|
};
|
|
});
|
|
}
|
|
|
|
export function fixImport(content: string) {
|
|
const regex = /@\/(.+?)\/((?:.*?\/)?(?:components|ui|hooks|lib))\/([\w-]+)/g;
|
|
|
|
const replacement = (
|
|
match: string,
|
|
_path: string,
|
|
type: string,
|
|
component: string,
|
|
) => {
|
|
if (type.endsWith("components")) {
|
|
return `@/components/${component}`;
|
|
}
|
|
if (type.endsWith("ui")) {
|
|
return `@/components/ui/${component}`;
|
|
}
|
|
if (type.endsWith("hooks")) {
|
|
return `@/hooks/${component}`;
|
|
}
|
|
if (type.endsWith("lib")) {
|
|
return `@/lib/${component}`;
|
|
}
|
|
|
|
return match;
|
|
};
|
|
|
|
return content.replace(regex, replacement);
|
|
}
|
|
|
|
export type FileTree = {
|
|
name: string;
|
|
path?: string;
|
|
children?: FileTree[];
|
|
};
|
|
|
|
export function createFileTreeForRegistryItemFiles(
|
|
files: Array<{ path: string; target?: string }>,
|
|
) {
|
|
const root: FileTree[] = [];
|
|
|
|
for (const file of files) {
|
|
const path = file.target ?? file.path;
|
|
const parts = path.split("/");
|
|
let currentLevel = root;
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const part = parts[i];
|
|
const isFile = i === parts.length - 1;
|
|
const existingNode = currentLevel.find((node) => node.name === part);
|
|
|
|
if (existingNode) {
|
|
if (isFile) {
|
|
// Update existing file node with full path
|
|
existingNode.path = path;
|
|
} else if (existingNode.children) {
|
|
// Move to next level in the tree
|
|
currentLevel = existingNode.children;
|
|
}
|
|
} else if (part) {
|
|
const newNode: FileTree = isFile
|
|
? { name: part, path }
|
|
: { children: [], name: part };
|
|
|
|
currentLevel.push(newNode);
|
|
|
|
if (!isFile && newNode.children) {
|
|
currentLevel = newNode.children;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return root;
|
|
}
|