components-differ 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,63 @@
1
- # ShadCN Project Differ
1
+ ## components-differ
2
2
 
3
- This CLI tool figures out the difference between the initial commit of a ShadCN project and the current state of the project and creates a new ShadCN JSON output file with the changes. This ShadCN JSON file can then be used with the ShadCN CLI to generate a new project or add to an existing project.
3
+ CLI to generate shadcn-compatible registry items (`registry:block`) from your project.
4
4
 
5
- # Steps
5
+ You can use it in two ways:
6
6
 
7
- 1. Create a new NextJS app
8
- 2. Add ShadCN to the app
9
- 3. Create a new initial commit; `rm -fr .git && git init && git add . && git commit -m "Initial commit"` or `npx components-differ --init`
10
- 4. Make your updates to the project
11
- 5. Run the CLI tool; `npx components-differ`
7
+ - **Folder mode** (default): create a registry item from every file in a folder (default: current directory).
8
+ - **Git mode** (`--git`): create a registry item from files changed since the initial git commit.
12
9
 
13
- The reason we are recreating the initial commit is so that the resulting JSON output is only the changes to the project after ShadCN was added, and not the entire project history.
10
+ ### Install & build
14
11
 
15
- You can then take the resulting JSON ouput and host it on a URL, then use that with the ShadCN CLI to generate a new project or add to an existing project.
12
+ From the `differ` folder:
16
13
 
17
14
  ```bash
18
- npx shadcn@latest init http://your-json-output-url
15
+ npm install -g components-differ
19
16
  ```
20
17
 
21
- You can use the `--src-dir` flag if you want to use the `src` directory in your project.
18
+ After building, you can run the CLI with `node` or via the `components-differ` bin (if linked/installed).
22
19
 
23
- Or you can add the JSON output to an existing project:
20
+ ### Default: folder mode (current directory)
21
+
22
+ Run with no arguments to generate a registry item from the **current folder** (and all files it imports):
24
23
 
25
24
  ```bash
26
- npx shadcn@latest add http://your-json-output-url
25
+ # from your project root – includes whole project and resolves imports
26
+ components-differ > block.json
27
+
28
+ # optional name
29
+ components-differ -n my-block > block.json
27
30
  ```
28
31
 
29
- # Why is this useful?
32
+ Or target a specific folder:
33
+
34
+ ```bash
35
+ components-differ --folder src/app/dashboard -n dashboard-block > block.json
36
+ components-differ --folder src/app/dashboard > block.json
37
+ ```
38
+
39
+ ### Git mode (changed files only)
40
+
41
+ Use `--git` when you want a registry item only from files changed since the initial commit:
42
+
43
+ ```bash
44
+ # initialize a clean git history first
45
+ components-differ --init
46
+
47
+ # after making changes, generate from changed files only
48
+ components-differ --git -n my-block > block.json
49
+ ```
50
+
51
+ ### Add the block locally
52
+
53
+ ```bash
54
+ npx shadcn add ./block.json
55
+ ```
56
+
57
+ ### Options summary
58
+
59
+ - **`--init`**: initialize a clean git repo and first commit (for use with `--git` later).
60
+ - **`-n, --name <name>`**: registry item name; defaults to current (or folder) name.
61
+ - **`-f, --folder <folder>`**: folder to include (default: current directory). Ignored if `--git` is set.
62
+ - **`--git`**: use git mode (only files changed since initial commit).
30
63
 
31
- This allows library maintainers or SaaS services to create a one step installer for their library or service into an existing project, or to bootstrap a new project with the library or service.
@@ -0,0 +1,130 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ const WHITELISTED_COMPONENTS = [
4
+ "accordion",
5
+ "alert",
6
+ "alert-dialog",
7
+ "aspect-ratio",
8
+ "avatar",
9
+ "badge",
10
+ "breadcrumb",
11
+ "button",
12
+ "button-group",
13
+ "calendar",
14
+ "card",
15
+ "carousel",
16
+ "chart",
17
+ "checkbox",
18
+ "collapsible",
19
+ "combobox",
20
+ "command",
21
+ "context-menu",
22
+ "data-table",
23
+ "date-picker",
24
+ "dialog",
25
+ "drawer",
26
+ "dropdown-menu",
27
+ "empty",
28
+ "field",
29
+ "form",
30
+ "hover-card",
31
+ "input",
32
+ "input-group",
33
+ "input-otp",
34
+ "item",
35
+ "kbd",
36
+ "label",
37
+ "menubar",
38
+ "native-select",
39
+ "navigation-menu",
40
+ "pagination",
41
+ "popover",
42
+ "progress",
43
+ "radio-group",
44
+ "resizable",
45
+ "scroll-area",
46
+ "select",
47
+ "separator",
48
+ "sheet",
49
+ "sidebar",
50
+ "skeleton",
51
+ "slider",
52
+ "sonner",
53
+ "spinner",
54
+ "switch",
55
+ "table",
56
+ "tabs",
57
+ "textarea",
58
+ "toast",
59
+ "toggle",
60
+ "toggle-group",
61
+ "tooltip",
62
+ "typography",
63
+ ];
64
+ function findComponentsJson(startDir) {
65
+ let currentDir = startDir;
66
+ while (true) {
67
+ const manifestPath = path.join(currentDir, "components.json");
68
+ if (fs.existsSync(manifestPath)) {
69
+ return manifestPath;
70
+ }
71
+ const parentDir = path.dirname(currentDir);
72
+ if (parentDir === currentDir) {
73
+ break;
74
+ }
75
+ currentDir = parentDir;
76
+ }
77
+ return null;
78
+ }
79
+ export function findComponentFiles(config, originalFiles) {
80
+ const registryDependencies = [];
81
+ const compDir = config.ui.replace("@/", config.isSrcDir ? "src/" : "");
82
+ const registriesConfig = config.registries ?? {};
83
+ const registryNamespaces = Object.keys(registriesConfig);
84
+ const defaultNamespace = registryNamespaces.find((name) => name === "@shadcn") ??
85
+ registryNamespaces[0] ??
86
+ null;
87
+ for (const { path: filePath } of originalFiles) {
88
+ if (filePath.startsWith(compDir)) {
89
+ const fileExtension = path.extname(filePath);
90
+ const fileName = path.basename(filePath, fileExtension);
91
+ if ((fileExtension === ".tsx" || fileExtension === ".jsx") &&
92
+ WHITELISTED_COMPONENTS.includes(fileName)) {
93
+ const baseName = path.basename(filePath, fileExtension);
94
+ const dependencyName = defaultNamespace
95
+ ? `${defaultNamespace}/${baseName}`
96
+ : baseName;
97
+ registryDependencies.push(dependencyName);
98
+ }
99
+ }
100
+ }
101
+ return registryDependencies;
102
+ }
103
+ export function readComponentsManifest(dir) {
104
+ const manifestPath = findComponentsJson(dir);
105
+ if (!manifestPath) {
106
+ console.error("Components manifest (components.json) not found");
107
+ process.exit(1);
108
+ }
109
+ const json = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
110
+ return {
111
+ ...json.aliases,
112
+ registries: json.registries ?? {},
113
+ };
114
+ }
115
+ export function getAliasedPaths(config) {
116
+ return [
117
+ config.components.replace("@/", ""),
118
+ config.utils.replace("@/", ""),
119
+ config.ui.replace("@/", ""),
120
+ config.lib.replace("@/", ""),
121
+ config.hooks.replace("@/", ""),
122
+ ];
123
+ }
124
+ export function isBuiltinComponent(config, filePath) {
125
+ if (filePath.startsWith(config.ui.replace("@/", ""))) {
126
+ const component = path.basename(filePath, path.extname(filePath));
127
+ return WHITELISTED_COMPONENTS.includes(component);
128
+ }
129
+ return false;
130
+ }
@@ -0,0 +1,77 @@
1
+ import { findComponentFiles, getAliasedPaths, isBuiltinComponent, } from "./components.js";
2
+ import { parseFilePath } from "./parse-file-path.js";
3
+ import { extractImportedPackages } from "./extract-imports.js";
4
+ function addFile(output, config, inSrcDir, relativeFilePath, content) {
5
+ if (!isBuiltinComponent(config, relativeFilePath)) {
6
+ output.files.push(parseFilePath(inSrcDir, config, `./${relativeFilePath}`, content));
7
+ }
8
+ }
9
+ function addDependencies(output, _initialPackageContents, currentPackageContents, usedPackages) {
10
+ const currentPackageJson = JSON.parse(currentPackageContents);
11
+ const currentDependencies = currentPackageJson.dependencies ?? {};
12
+ const currentDevDependencies = currentPackageJson.devDependencies ?? {};
13
+ const shadcnNamespaces = new Set(output.registryDependencies
14
+ .map((dep) => dep.split("/")[0])
15
+ .filter((ns) => ns === "@shadcn"));
16
+ const shouldKeepDep = (dep) => {
17
+ if (!usedPackages.has(dep))
18
+ return false;
19
+ if (!shadcnNamespaces.size)
20
+ return true;
21
+ if (dep === "shadcn/ui")
22
+ return false;
23
+ for (const ns of shadcnNamespaces) {
24
+ if (dep === ns || dep === `${ns}/ui`) {
25
+ return false;
26
+ }
27
+ }
28
+ return true;
29
+ };
30
+ // Only include packages that are actually imported in the registry item files
31
+ // (and exist in package.json so we know dep vs devDep). No other deps.
32
+ output.dependencies = Object.keys(currentDependencies).filter(shouldKeepDep);
33
+ output.devDependencies = Object.keys(currentDevDependencies).filter(shouldKeepDep);
34
+ }
35
+ function scanWithSrcDir(output, config, alteredFiles) {
36
+ for (const { path, content } of alteredFiles) {
37
+ if (path.startsWith("src/")) {
38
+ addFile(output, config, true, path.replace("src/", ""), content);
39
+ }
40
+ else {
41
+ addFile(output, config, false, path, content);
42
+ }
43
+ }
44
+ }
45
+ function isInAppDir(filePath) {
46
+ return filePath.startsWith("app/");
47
+ }
48
+ function scanWithoutSrcDir(output, config, alteredFiles) {
49
+ const aliasedPaths = getAliasedPaths(config);
50
+ for (const { path, content } of alteredFiles) {
51
+ const inSrcDir = aliasedPaths.includes(path) || isInAppDir(path);
52
+ addFile(output, config, inSrcDir, path, content);
53
+ }
54
+ }
55
+ export function createDiff({ name, config, alteredFiles, specificFiles, currentFiles, currentPackageJson, }) {
56
+ const output = {
57
+ name,
58
+ type: "registry:block",
59
+ dependencies: [],
60
+ devDependencies: [],
61
+ registryDependencies: [],
62
+ files: [],
63
+ tailwind: {},
64
+ cssVars: {},
65
+ meta: {},
66
+ };
67
+ if (config.isSrcDir) {
68
+ scanWithSrcDir(output, config, alteredFiles);
69
+ }
70
+ else {
71
+ scanWithoutSrcDir(output, config, alteredFiles);
72
+ }
73
+ output.registryDependencies = findComponentFiles(config, currentFiles);
74
+ const usedPackages = extractImportedPackages(alteredFiles);
75
+ addDependencies(output, specificFiles["./package.json"], currentPackageJson, usedPackages);
76
+ return output;
77
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Extract npm package names from import/require statements in source code.
3
+ * Used to include only dependencies that are actually used by the registry item files.
4
+ */
5
+ /**
6
+ * Returns the root package name for a specifier. Handles scoped packages and subpaths.
7
+ * - "react" -> "react"
8
+ * - "lodash/merge" -> "lodash"
9
+ * - "@scope/pkg" -> "@scope/pkg"
10
+ * - "@scope/pkg/sub" -> "@scope/pkg"
11
+ */
12
+ function specifierToPackageName(specifier) {
13
+ const s = specifier.trim();
14
+ if (!s)
15
+ return null;
16
+ // Node / bundler builtins
17
+ if (s.startsWith("node:") || s === "node")
18
+ return null;
19
+ // Relative imports – not packages
20
+ if (s.startsWith(".") || s.startsWith("/"))
21
+ return null;
22
+ // Common path aliases (not package names)
23
+ if (s.startsWith("@/") || s.startsWith("~/") || s.startsWith("#"))
24
+ return null;
25
+ // Scoped package: @scope/pkg or @scope/pkg/subpath
26
+ if (s.startsWith("@")) {
27
+ const firstSlash = s.indexOf("/");
28
+ if (firstSlash === -1)
29
+ return s;
30
+ const secondSlash = s.indexOf("/", firstSlash + 1);
31
+ return secondSlash === -1 ? s : s.slice(0, secondSlash);
32
+ }
33
+ // Normal package or subpath
34
+ const slash = s.indexOf("/");
35
+ return slash === -1 ? s : s.slice(0, slash);
36
+ }
37
+ const RE_IMPORT = /(?:import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?|import\s*)['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
38
+ /**
39
+ * Collect all package names that are imported or required in the given file content.
40
+ */
41
+ function extractFromContent(content) {
42
+ const packages = new Set();
43
+ let m;
44
+ RE_IMPORT.lastIndex = 0;
45
+ while ((m = RE_IMPORT.exec(content)) !== null) {
46
+ const specifier = (m[1] ?? m[2] ?? m[3]) ?? "";
47
+ const pkg = specifierToPackageName(specifier);
48
+ if (pkg)
49
+ packages.add(pkg);
50
+ }
51
+ return packages;
52
+ }
53
+ /**
54
+ * Collect all package names imported by any of the given files (by their content).
55
+ */
56
+ export function extractImportedPackages(files) {
57
+ const all = new Set();
58
+ for (const { content } of files) {
59
+ for (const pkg of extractFromContent(content)) {
60
+ all.add(pkg);
61
+ }
62
+ }
63
+ return all;
64
+ }
65
+ /** True if the import specifier refers to a project file (relative or path alias), not npm. */
66
+ export function isPathSpecifier(specifier) {
67
+ const s = specifier.trim();
68
+ if (!s || s.startsWith("node:"))
69
+ return false;
70
+ if (s.startsWith(".") || s.startsWith("/"))
71
+ return true;
72
+ if (s.startsWith("@/") || s.startsWith("~/") || s.startsWith("#"))
73
+ return true;
74
+ return false;
75
+ }
76
+ /**
77
+ * Extract all import/require specifiers that are project paths (relative or alias)
78
+ * from file content. Used to pull in imported files into the registry.
79
+ */
80
+ export function extractPathSpecifiers(content) {
81
+ const out = [];
82
+ let m;
83
+ RE_IMPORT.lastIndex = 0;
84
+ while ((m = RE_IMPORT.exec(content)) !== null) {
85
+ const specifier = (m[1] ?? m[2] ?? m[3]) ?? "";
86
+ if (isPathSpecifier(specifier))
87
+ out.push(specifier);
88
+ }
89
+ return out;
90
+ }
package/dist/git.js ADDED
@@ -0,0 +1,117 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import ignore from "ignore";
5
+ const INITIAL_DIR = "_initial";
6
+ const EXCLUDE_DIRS = [
7
+ "node_modules",
8
+ "dist",
9
+ "fonts",
10
+ "build",
11
+ "public",
12
+ "static",
13
+ ".next",
14
+ ".git",
15
+ INITIAL_DIR,
16
+ ];
17
+ const EXCLUDE_FILES = [
18
+ ".DS_Store",
19
+ "next-env.d.ts",
20
+ "package-lock.json",
21
+ "yarn.lock",
22
+ "pnpm-lock.yaml",
23
+ "bun.lockb",
24
+ "package.json",
25
+ "tailwind.config.ts",
26
+ "tailwind.config.js",
27
+ "components.json",
28
+ "favicon.ico",
29
+ ];
30
+ function cloneInitialCommit() {
31
+ deleteInitialDir();
32
+ try {
33
+ const initialCommit = execSync("git rev-list --max-parents=0 HEAD")
34
+ .toString()
35
+ .trim();
36
+ execSync(`git worktree add -f ${INITIAL_DIR} ${initialCommit}`, {
37
+ stdio: "ignore",
38
+ });
39
+ }
40
+ catch (error) {
41
+ console.error("Error cloning initial commit:", error.message);
42
+ process.exit(1);
43
+ }
44
+ }
45
+ function deleteInitialDir() {
46
+ if (fs.existsSync(INITIAL_DIR)) {
47
+ fs.rmSync(INITIAL_DIR, { recursive: true, force: true });
48
+ try {
49
+ execSync("git worktree prune", { stdio: "ignore" });
50
+ }
51
+ catch (error) {
52
+ console.error("Error pruning git worktree:", error.message);
53
+ }
54
+ }
55
+ }
56
+ function checkIfFileIsChanged(relativeFilePath) {
57
+ const initialFilePath = path.join(INITIAL_DIR, relativeFilePath);
58
+ const fullPath = path.join(process.cwd(), relativeFilePath);
59
+ if (!fs.existsSync(initialFilePath)) {
60
+ return true;
61
+ }
62
+ const currentContent = fs.readFileSync(fullPath, "utf-8");
63
+ const initialContent = fs.readFileSync(initialFilePath, "utf-8");
64
+ return currentContent !== initialContent;
65
+ }
66
+ export function scanForFiles(startDir, checkFile = false) {
67
+ const foundFiles = [];
68
+ let ignorer = () => false;
69
+ const gitignorePath = path.join(startDir, ".gitignore");
70
+ if (fs.existsSync(gitignorePath)) {
71
+ const gitIgnore = ignore().add(fs.readFileSync(gitignorePath).toString());
72
+ ignorer = (relativeFilePath) => gitIgnore.ignores(relativeFilePath);
73
+ }
74
+ function scanDirectory(dir, relativePath = "") {
75
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
76
+ for (const entry of entries) {
77
+ const fullPath = path.join(dir, entry.name);
78
+ const relativeFilePath = path.join(relativePath, entry.name);
79
+ if (entry.isDirectory()) {
80
+ if (!EXCLUDE_DIRS.includes(entry.name)) {
81
+ scanDirectory(path.join(dir, entry.name), relativeFilePath);
82
+ }
83
+ }
84
+ else if (!checkFile ||
85
+ (checkFile && checkIfFileIsChanged(relativeFilePath))) {
86
+ if (!EXCLUDE_FILES.includes(entry.name) &&
87
+ !ignorer(relativeFilePath)) {
88
+ foundFiles.push({
89
+ path: relativeFilePath,
90
+ content: fs.readFileSync(fullPath, "utf-8"),
91
+ });
92
+ }
93
+ }
94
+ }
95
+ }
96
+ scanDirectory(startDir);
97
+ return foundFiles.sort((a, b) => a.path.localeCompare(b.path));
98
+ }
99
+ export function scanForAlteredFiles(specificFilesToReturn = []) {
100
+ cloneInitialCommit();
101
+ const alteredFiles = scanForFiles(process.cwd(), true);
102
+ const specificFiles = specificFilesToReturn.reduce((out, file) => {
103
+ const fullPath = path.join(process.cwd(), INITIAL_DIR, file);
104
+ out[file] = fs.existsSync(fullPath)
105
+ ? fs.readFileSync(fullPath, "utf-8")
106
+ : "";
107
+ return out;
108
+ }, {});
109
+ deleteInitialDir();
110
+ return {
111
+ alteredFiles,
112
+ specificFiles,
113
+ };
114
+ }
115
+ export function hasSrcDir(dir) {
116
+ return fs.existsSync(path.join(dir, "src"));
117
+ }
package/dist/index.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { execSync } from "node:child_process";
5
+ import { program } from "commander";
6
+ import { scanForAlteredFiles, scanForFiles, hasSrcDir, } from "./git.js";
7
+ import { readComponentsManifest } from "./components.js";
8
+ import { createDiff } from "./create-diff.js";
9
+ import { expandIncludedFiles } from "./resolve-imports.js";
10
+ function runCommand(command) {
11
+ try {
12
+ execSync(command, { stdio: "inherit" });
13
+ }
14
+ catch (error) {
15
+ console.error(`Failed to execute command: ${command}`);
16
+ process.exit(1);
17
+ }
18
+ }
19
+ function ensureGitignore() {
20
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
21
+ if (!fs.existsSync(gitignorePath)) {
22
+ const content = `
23
+ /node_modules
24
+ /.pnp
25
+ .pnp.*
26
+ .yarn/*
27
+ !.yarn/patches
28
+ !.yarn/plugins
29
+ !.yarn/releases
30
+ !.yarn/versions
31
+
32
+ # testing
33
+ /coverage
34
+
35
+ # next.js
36
+ /.next/
37
+ /out/
38
+
39
+ # production
40
+ /build
41
+
42
+ # misc
43
+ .DS_Store
44
+ *.pem
45
+
46
+ # debug
47
+ npm-debug.log*
48
+ yarn-debug.log*
49
+ yarn-error.log*
50
+
51
+ # env files (can opt-in for committing if needed)
52
+ .env*
53
+
54
+ # vercel
55
+ .vercel
56
+
57
+ # typescript
58
+ *.tsbuildinfo
59
+ next-env.d.ts
60
+ `;
61
+ fs.writeFileSync(gitignorePath, content, "utf8");
62
+ }
63
+ }
64
+ function main() {
65
+ program
66
+ .option("-n, --name <name>")
67
+ .option("--init")
68
+ .option("-f, --folder <folder>", "folder to include (default: current directory)")
69
+ .option("--git", "use git mode: only files changed since initial commit");
70
+ program.parse();
71
+ const options = program.opts();
72
+ if (options.init) {
73
+ // Initialize a clean git repository for the current component
74
+ if (process.platform === "win32") {
75
+ runCommand('rmdir /s /q .git && git init && git add . && git commit -m "Initial commit"');
76
+ }
77
+ else {
78
+ runCommand('rm -fr .git && git init && git add . && git commit -m "Initial commit"');
79
+ }
80
+ ensureGitignore();
81
+ return;
82
+ }
83
+ const useFolderMode = !options.git;
84
+ const folderOpt = options.folder ?? (useFolderMode ? "." : undefined);
85
+ const baseName = folderOpt != null
86
+ ? path.basename(path.resolve(process.cwd(), folderOpt))
87
+ : path.basename(process.cwd());
88
+ const name = options.name || baseName;
89
+ let alteredFiles;
90
+ let specificFiles;
91
+ if (useFolderMode && folderOpt != null) {
92
+ const folderPath = path.resolve(process.cwd(), folderOpt);
93
+ if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
94
+ console.error(`Folder path is not a directory: ${folderOpt}`);
95
+ process.exit(1);
96
+ }
97
+ const folderPrefix = path
98
+ .relative(process.cwd(), folderPath)
99
+ .replace(/\\/g, "/")
100
+ .replace(/^\.\//, "");
101
+ const allFiles = scanForFiles(process.cwd());
102
+ alteredFiles = allFiles.filter(({ path: filePath }) => {
103
+ const normalized = filePath.replace(/\\/g, "/");
104
+ return (normalized === folderPrefix ||
105
+ normalized.startsWith(`${folderPrefix}/`));
106
+ });
107
+ specificFiles = {
108
+ "./package.json": "{}",
109
+ };
110
+ }
111
+ else {
112
+ const result = scanForAlteredFiles(["./package.json"]);
113
+ alteredFiles = result.alteredFiles;
114
+ specificFiles = result.specificFiles;
115
+ }
116
+ const currentFiles = scanForFiles(process.cwd());
117
+ const currentPackageJson = fs.readFileSync("./package.json", "utf-8");
118
+ const config = readComponentsManifest(process.cwd());
119
+ config.isSrcDir = hasSrcDir(process.cwd());
120
+ // Recursively add any project file imported by the included files so the
121
+ // registry item is self-contained (no "component not found").
122
+ alteredFiles = expandIncludedFiles(alteredFiles, currentFiles, config);
123
+ const output = createDiff({
124
+ name,
125
+ config,
126
+ alteredFiles,
127
+ currentFiles,
128
+ specificFiles,
129
+ currentPackageJson,
130
+ });
131
+ // Emit a complete registry:block JSON object suitable for local file support
132
+ // (e.g. `npx shadcn add ./block.json`)
133
+ // eslint-disable-next-line no-console
134
+ console.log(JSON.stringify(output, null, 2));
135
+ }
136
+ main();