components-differ 1.2.2 → 1.2.4

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,106 +1,63 @@
1
- # components-differ
1
+ ## components-differ
2
2
 
3
- `components-differ` is a CLI that inspects a Shadcn-based project and emits a [registry-item](https://ui.shadcn.com/docs/registry/registry-item-json) compatible JSON bundle. The generated bundle captures only the delta between the project’s initial template commit and the current working tree, making it easy to distribute curated component packs or bootstrap new installations.
3
+ CLI to generate shadcn-compatible registry items (`registry:block`) from your project.
4
4
 
5
- ## Features
5
+ You can use it in two ways:
6
6
 
7
- - ⚡️ **Quick diffing** Scans the git history to compare the initial commit against the current state of the repository.
8
- - 🧠 **Alias aware** Understands `components.json` aliases and supports projects with or without a `src/` directory.
9
- - 📦 **Full registry support** – Outputs files using the latest Shadcn registry types (`registry:ui`, `registry:block`, `registry:page`, `registry:file`, `registry:theme`, etc.).
10
- - 🧩 **Automated dependency detection** – Detects new npm dependencies/devDependencies introduced since the initial commit.
11
- - 🛠️ **Build+publish ready** – Includes a reproducible build step, release helpers, and Vitest coverage.
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
- ## Installation
10
+ ### Install & build
14
11
 
15
- ```bash
16
- pnpm install components-differ
17
- # or
18
- npm install components-differ
19
- # or
20
- yarn add components-differ
21
- ```
22
-
23
- For one-off usage you can run the CLI with `npx`:
12
+ From the `differ` folder:
24
13
 
25
14
  ```bash
26
- npx components-differ
15
+ npm install -g components-differ
27
16
  ```
28
17
 
29
- ## Usage
18
+ After building, you can run the CLI with `node` or via the `components-differ` bin (if linked/installed).
30
19
 
31
- ### 1. Prepare your project
20
+ ### Default: folder mode (current directory)
32
21
 
33
- 1. Scaffold a new Next.js application.
34
- 2. Install [shadcn/ui](https://ui.shadcn.com/docs/components).
35
- 3. Commit the pristine template so that the first commit represents the baseline.
36
-
37
- You can use the built-in init helper to automate step 3:
22
+ Run with no arguments to generate a registry item from the **current folder** (and all files it imports):
38
23
 
39
24
  ```bash
40
- npx components-differ --init
41
- ```
25
+ # from your project root – includes whole project and resolves imports
26
+ components-differ > block.json
42
27
 
43
- This command wipes the current `.git` folder, initializes a new repository, creates an initial commit, and provisions a sensible `.gitignore`.
44
-
45
- ### 2. Make changes
46
-
47
- Add or modify components, hooks, utilities, env files, Tailwind styles, or any other project files.
28
+ # optional name
29
+ components-differ -n my-block > block.json
30
+ ```
48
31
 
49
- ### 3. Generate the registry diff
32
+ Or target a specific folder:
50
33
 
51
34
  ```bash
52
- npx components-differ
35
+ components-differ --folder src/app/dashboard -n dashboard-block > block.json
36
+ components-differ --folder src/app/dashboard > block.json
53
37
  ```
54
38
 
55
- The CLI prints a JSON payload describing:
56
-
57
- - File list with appropriate registry types and targets.
58
- - New registry dependencies detected in your component directory.
59
- - Added npm dependencies and devDependencies.
39
+ ### Git mode (changed files only)
60
40
 
61
- You can pipe this JSON to a file or publish it to a URL and consume it with the official Shadcn CLI:
41
+ Use `--git` when you want a registry item only from files changed since the initial commit:
62
42
 
63
43
  ```bash
64
- npx shadcn@latest init https://your-domain.com/registry.json
65
- npx shadcn@latest add https://your-domain.com/registry.json
66
- ```
67
-
68
- ## CLI Options
44
+ # initialize a clean git history first
45
+ components-differ --init
69
46
 
70
- | Flag | Description |
71
- | ---- | ----------- |
72
- | `--name <name>` | Overrides the generated registry item `name`. Defaults to the current folder name. |
73
- | `--init` | Re-initializes git and creates the baseline commit described above. |
47
+ # after making changes, generate from changed files only
48
+ components-differ --git -n my-block > block.json
49
+ ```
74
50
 
75
- ## Development
51
+ ### Add the block locally
76
52
 
77
53
  ```bash
78
- pnpm install # install dependencies
79
- pnpm test # run Vitest suite (snapshot + unit tests)
80
- pnpm run build # build the distributable into dist/
81
- pnpm run release # build, test, and publish (requires npm auth + clean git tree)
54
+ npx shadcn add ./block.json
82
55
  ```
83
56
 
84
- The project relies on git worktrees to obtain the baseline commit, so ensure your repository has at least one commit before running the CLI in development.
85
-
86
- ## How It Works
87
-
88
- 1. **Worktree clone** – `scanForAlteredFiles` creates a temporary git worktree of the initial commit.
89
- 2. **File classification** – `parseFilePath` maps files to Shadcn registry types (page, ui, block, theme, style, lib, hook, file, item) while honoring `components.json` aliases and `src/` directory layouts.
90
- 3. **Manifest parsing** – `readComponentsManifest` reads alias definitions and determines whether the project uses `src/`.
91
- 4. **Dependency diff** – `createDiff` compares the current `package.json` with the baseline to capture dependency changes.
92
- 5. **Output assembly** – Files, registry dependencies, metadata, Tailwind placeholders, and CSS variables are assembled into a registry item schema.
93
-
94
- ## Releasing
95
-
96
- 1. Update `package.json` version (use `pnpm node ./scripts/bump-version.mjs` for a patch bump).
97
- 2. Ensure the working tree is clean.
98
- 3. Run `pnpm run release` to build, test, and publish to npm.
99
-
100
- ## Contributing
101
-
102
- Pull requests are welcome! Please accompany feature changes with unit tests or snapshot updates as appropriate. If you’re unsure where files are classified or need new registry types, check `src/parse-file-path.mjs` and corresponding tests under `tests/tests`.
57
+ ### Options summary
103
58
 
104
- ## License
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).
105
63
 
106
- MIT © Components Host
@@ -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,75 @@
1
+ import { findComponentFiles, getAliasedPaths, } 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
+ output.files.push(parseFilePath(inSrcDir, config, `./${relativeFilePath}`, content));
6
+ }
7
+ function addDependencies(output, _initialPackageContents, currentPackageContents, usedPackages) {
8
+ const currentPackageJson = JSON.parse(currentPackageContents);
9
+ const currentDependencies = currentPackageJson.dependencies ?? {};
10
+ const currentDevDependencies = currentPackageJson.devDependencies ?? {};
11
+ const shadcnNamespaces = new Set(output.registryDependencies
12
+ .map((dep) => dep.split("/")[0])
13
+ .filter((ns) => ns === "@shadcn"));
14
+ const shouldKeepDep = (dep) => {
15
+ if (!usedPackages.has(dep))
16
+ return false;
17
+ if (!shadcnNamespaces.size)
18
+ return true;
19
+ if (dep === "shadcn/ui")
20
+ return false;
21
+ for (const ns of shadcnNamespaces) {
22
+ if (dep === ns || dep === `${ns}/ui`) {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ };
28
+ // Only include packages that are actually imported in the registry item files
29
+ // (and exist in package.json so we know dep vs devDep). No other deps.
30
+ output.dependencies = Object.keys(currentDependencies).filter(shouldKeepDep);
31
+ output.devDependencies = Object.keys(currentDevDependencies).filter(shouldKeepDep);
32
+ }
33
+ function scanWithSrcDir(output, config, alteredFiles) {
34
+ for (const { path, content } of alteredFiles) {
35
+ if (path.startsWith("src/")) {
36
+ addFile(output, config, true, path.replace("src/", ""), content);
37
+ }
38
+ else {
39
+ addFile(output, config, false, path, content);
40
+ }
41
+ }
42
+ }
43
+ function isInAppDir(filePath) {
44
+ return filePath.startsWith("app/");
45
+ }
46
+ function scanWithoutSrcDir(output, config, alteredFiles) {
47
+ const aliasedPaths = getAliasedPaths(config);
48
+ for (const { path, content } of alteredFiles) {
49
+ const inSrcDir = aliasedPaths.includes(path) || isInAppDir(path);
50
+ addFile(output, config, inSrcDir, path, content);
51
+ }
52
+ }
53
+ export function createDiff({ name, config, alteredFiles, specificFiles, currentFiles, currentPackageJson, }) {
54
+ const output = {
55
+ name,
56
+ type: "registry:block",
57
+ dependencies: [],
58
+ devDependencies: [],
59
+ registryDependencies: [],
60
+ files: [],
61
+ tailwind: {},
62
+ cssVars: {},
63
+ meta: {},
64
+ };
65
+ if (config.isSrcDir) {
66
+ scanWithSrcDir(output, config, alteredFiles);
67
+ }
68
+ else {
69
+ scanWithoutSrcDir(output, config, alteredFiles);
70
+ }
71
+ output.registryDependencies = findComponentFiles(config, currentFiles);
72
+ const usedPackages = extractImportedPackages(alteredFiles);
73
+ addDependencies(output, specificFiles["./package.json"], currentPackageJson, usedPackages);
74
+ return output;
75
+ }
@@ -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();