demo-dev2d-cli 1.0.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/bin/index.js ADDED
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require("commander");
4
+ const fs = require("fs-extra");
5
+ const path = require("path");
6
+ const babel = require("@babel/core");
7
+ const presetTypescript = require("@babel/preset-typescript");
8
+ const { execSync } = require("child_process");
9
+ const registry = require("../registry.json");
10
+
11
+ const program = new Command();
12
+
13
+ /* -----------------------------
14
+ INQUIRER (FIXED SAFE WRAPPER)
15
+ ------------------------------*/
16
+ const prompt = async (questions) => {
17
+ const inquirer = await import("inquirer");
18
+ return inquirer.default.prompt(questions);
19
+ };
20
+
21
+ /* =============================
22
+ ADD COMMAND
23
+ ============================= */
24
+ program
25
+ .command("add <name>")
26
+ .description("Add components or utilities")
27
+ .action(async (name) => {
28
+ try {
29
+ const cwd = process.cwd();
30
+ console.log("\nšŸš€ Starting installation...\n");
31
+
32
+ const isTsProject = fs.existsSync(path.join(cwd, "tsconfig.json"));
33
+
34
+ let templatePath = "";
35
+ let outputDir = "";
36
+ let extension = "tsx";
37
+
38
+ /* ---------------- COMPONENTS ---------------- */
39
+ if (!name.startsWith("utils/")) {
40
+ templatePath = path.join(__dirname, `../templates/${name}.tsx`);
41
+ outputDir = path.join(cwd, "src/components/pejay-ui");
42
+ extension = "tsx";
43
+ } else {
44
+
45
+ /* ---------------- UTILITIES ---------------- */
46
+ const utilName = name.split("/")[1];
47
+ templatePath = path.join(__dirname, `../utils/${utilName}.ts`);
48
+ outputDir = path.join(cwd, "src/utils/pejay-ui");
49
+ extension = "ts";
50
+ }
51
+
52
+ if (!fs.existsSync(templatePath)) {
53
+ console.log(`āŒ "${name}" not found`);
54
+ return;
55
+ }
56
+
57
+ const code = await fs.readFile(templatePath, "utf8");
58
+
59
+ let finalCode = code;
60
+ let outputExt = extension;
61
+
62
+ /* ---------------- TRANSFORM ---------------- */
63
+ if (!isTsProject) {
64
+ const transformed = babel.transformSync(code, {
65
+ presets: [presetTypescript],
66
+ filename: `file.${extension}`,
67
+ });
68
+
69
+ if (!transformed?.code) {
70
+ throw new Error("Babel transform failed");
71
+ }
72
+
73
+ finalCode = transformed.code;
74
+ outputExt = extension === "tsx" ? "jsx" : "js";
75
+ }
76
+
77
+ await fs.ensureDir(outputDir);
78
+
79
+ const itemName = name.includes("/") ? name.split("/")[1] : name;
80
+ const outputPath = path.join(outputDir, `${itemName}.${outputExt}`);
81
+
82
+ await fs.writeFile(outputPath, finalCode);
83
+ console.log(`āœ… Created ${itemName}.${outputExt}`);
84
+
85
+ /* ---------------- DEPENDENCIES ---------------- */
86
+ const itemRegistry = registry[name];
87
+
88
+ if (itemRegistry?.dependencies?.length) {
89
+ for (const dependency of itemRegistry.dependencies) {
90
+ const depName = dependency.includes("/")
91
+ ? dependency.split("/")[1]
92
+ : dependency;
93
+
94
+ const depPath = path.join(__dirname, `../utils/${depName}.ts`);
95
+
96
+ if (!fs.existsSync(depPath)) continue;
97
+
98
+ let depCode = await fs.readFile(depPath, "utf8");
99
+
100
+ if (!isTsProject) {
101
+ const transformed = babel.transformSync(depCode, {
102
+ presets: [presetTypescript],
103
+ filename: `${depName}.ts`,
104
+ });
105
+
106
+ depCode = transformed.code;
107
+ }
108
+
109
+ const utilDir = path.join(cwd, "src/utils/pejay-ui");
110
+ await fs.ensureDir(utilDir);
111
+
112
+ const ext = isTsProject ? "ts" : "js";
113
+ const outPath = path.join(utilDir, `${depName}.${ext}`);
114
+
115
+ if (!fs.existsSync(outPath)) {
116
+ await fs.writeFile(outPath, depCode);
117
+ console.log(`šŸ“¦ Added dependency ${depName}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ /* ---------------- STATE SAVE ---------------- */
123
+ const statePath = path.join(cwd, ".pejay-ui.json");
124
+
125
+ let state = {};
126
+ if (fs.existsSync(statePath)) {
127
+ state = JSON.parse(await fs.readFile(statePath, "utf8"));
128
+ }
129
+
130
+ state.installed = state.installed || {};
131
+
132
+ state.installed[name] = {
133
+ files: [`src/components/pejay-ui/${itemName}.${outputExt}`],
134
+ uses:
135
+ itemRegistry?.dependencies?.map((d) =>
136
+ d.includes("/") ? d.split("/")[1] : d,
137
+ ) || [],
138
+ };
139
+
140
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2));
141
+
142
+ console.log(`\nšŸŽ‰ ${name} installed successfully\n`);
143
+ } catch (err) {
144
+ console.error("\nāŒ Add failed\n", err);
145
+ }
146
+ });
147
+
148
+ /* =============================
149
+ REMOVE COMMAND (SAFE VERSION)
150
+ ============================= */
151
+ program
152
+ .command("remove <name>")
153
+ .description("Remove components or utilities safely")
154
+ .action(async (name) => {
155
+ try {
156
+ const cwd = process.cwd();
157
+ console.log("\n🧹 Starting removal...\n");
158
+
159
+ const itemRegistry = registry[name];
160
+
161
+ if (!itemRegistry) {
162
+ console.log(`āŒ "${name}" not found in registry`);
163
+ return;
164
+ }
165
+
166
+ const isTsProject = fs.existsSync(path.join(cwd, "tsconfig.json"));
167
+ const itemName = name.includes("/") ? name.split("/")[1] : name;
168
+
169
+ const componentDir = path.join(cwd, "src/components/pejay-ui");
170
+ const utilsDir = path.join(cwd, "src/utils/pejay-ui");
171
+
172
+ let removedAny = false;
173
+
174
+ /* ---------------- REMOVE COMPONENT ---------------- */
175
+ const exts = isTsProject ? ["tsx", "ts"] : ["jsx", "js"];
176
+
177
+ for (const ext of exts) {
178
+ const file = path.join(componentDir, `${itemName}.${ext}`);
179
+
180
+ if (fs.existsSync(file)) {
181
+ await fs.remove(file);
182
+ console.log(`šŸ—‘ļø Removed ${file}`);
183
+ removedAny = true;
184
+ }
185
+ }
186
+
187
+ /* ---------------- LOAD STATE ---------------- */
188
+ const statePath = path.join(cwd, ".pejay-ui.json");
189
+
190
+ let state = {};
191
+ if (fs.existsSync(statePath)) {
192
+ state = JSON.parse(await fs.readFile(statePath, "utf8"));
193
+ }
194
+
195
+ state.installed = state.installed || {};
196
+
197
+ /* ---------------- BUILD USAGE MAP ---------------- */
198
+ const usageMap = new Map();
199
+
200
+ for (const comp of Object.values(state.installed)) {
201
+ for (const u of comp.uses || []) {
202
+ usageMap.set(u, (usageMap.get(u) || 0) + 1);
203
+ }
204
+ }
205
+
206
+ /* ---------------- SAFE UTILITY REMOVE ---------------- */
207
+ if (itemRegistry.dependencies?.length) {
208
+ for (const dep of itemRegistry.dependencies) {
209
+ const utilName = dep.includes("/") ? dep.split("/")[1] : dep;
210
+
211
+ const usage = usageMap.get(utilName) || 0;
212
+
213
+ if (usage > 1) {
214
+ console.log(`ā›” Skipping ${utilName} (used by other components)`);
215
+ continue;
216
+ }
217
+
218
+ const { confirm } = await prompt([
219
+ {
220
+ type: "confirm",
221
+ name: "confirm",
222
+ message: `Remove utility "${utilName}"?`,
223
+ default: false,
224
+ },
225
+ ]);
226
+
227
+ if (!confirm) continue;
228
+
229
+ for (const ext of exts) {
230
+ const file = path.join(utilsDir, `${utilName}.${ext}`);
231
+
232
+ if (fs.existsSync(file)) {
233
+ await fs.remove(file);
234
+ console.log(`šŸ—‘ļø Removed ${file}`);
235
+ removedAny = true;
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ /* ---------------- REMOVE PACKAGES ---------------- */
242
+ // Only collect packages from deps that NO other component still uses
243
+ const pkgs = [];
244
+
245
+ for (const dep of itemRegistry.dependencies || []) {
246
+ const utilName = dep.includes("/") ? dep.split("/")[1] : dep;
247
+ const usage = usageMap.get(utilName) || 0;
248
+
249
+ // usage > 1 means another component still needs this dep → skip its packages
250
+ if (usage > 1) {
251
+ console.log(`ā›” Keeping packages for ${utilName} (used by other components)`);
252
+ continue;
253
+ }
254
+
255
+ const reg = registry[dep];
256
+ if (reg?.packages) pkgs.push(...reg.packages);
257
+ }
258
+
259
+ if (pkgs.length) {
260
+ const unique = [...new Set(pkgs)];
261
+
262
+ const { ok } = await prompt([
263
+ {
264
+ type: "confirm",
265
+ name: "ok",
266
+ message: `Uninstall packages: ${unique.join(", ")}?`,
267
+ default: false,
268
+ },
269
+ ]);
270
+
271
+ if (ok) {
272
+ execSync(`npm uninstall ${unique.join(" ")}`, {
273
+ cwd,
274
+ stdio: "inherit",
275
+ });
276
+ }
277
+ }
278
+
279
+ /* ---------------- CLEAN STATE ---------------- */
280
+ if (state.installed[name]) {
281
+ delete state.installed[name];
282
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2));
283
+ }
284
+
285
+ console.log(`\nšŸŽ‰ ${name} removed successfully\n`);
286
+ } catch (err) {
287
+ console.error("\nāŒ Remove failed\n", err);
288
+ }
289
+ });
290
+
291
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "demo-dev2d-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI to install reusable React components like shadcn/ui",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "demo-dev2d-cli": "./bin/index.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "templates",
12
+ "utils",
13
+ "registry.json"
14
+ ],
15
+ "scripts": {
16
+ "test": "echo \"Error: no test specified\" && exit 1"
17
+ },
18
+ "keywords": [],
19
+ "author": "demo-auth",
20
+ "license": "ISC",
21
+ "dependencies": {
22
+ "@babel/core": "^7.29.0",
23
+ "@babel/preset-react": "^7.28.5",
24
+ "@babel/preset-typescript": "^7.28.5",
25
+ "@types/react": "^19.2.14",
26
+ "clsx": "^2.1.1",
27
+ "commander": "^14.0.3",
28
+ "fs-extra": "^11.3.5",
29
+ "inquirer": "^13.4.3",
30
+ "react": "^19.2.6",
31
+ "tailwind-merge": "^3.6.0"
32
+ }
33
+ }
package/registry.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "input": {
3
+ "dependencies": ["utils/cn"]
4
+ },
5
+
6
+ "utils/cn": {
7
+ "packages": ["clsx", "tailwind-merge"]
8
+ }
9
+ }
@@ -0,0 +1,213 @@
1
+ import React, { useState } from "react";
2
+ import { cn } from "../../utils/pejay-ui/cn";
3
+
4
+ interface InputProps extends Omit<
5
+ React.InputHTMLAttributes<HTMLInputElement>,
6
+ "prefix"
7
+ > {
8
+ label?: string;
9
+ description?: string;
10
+ error?: string;
11
+ icon?: React.ReactNode;
12
+ rightIcon?: React.ReactNode;
13
+ prefix?: React.ReactNode;
14
+ suffix?: React.ReactNode;
15
+ onRightIconClick?: (e: React.MouseEvent) => void;
16
+ variant?: "rounded" | "curved" | "square" | "floating";
17
+ labelDirection?:
18
+ | "label-left"
19
+ | "label-left-top"
20
+ | "label-left-bottom"
21
+ | "label-right"
22
+ | "label-right-top"
23
+ | "label-right-bottom"
24
+ | "label-top"
25
+ | "label-top-right"
26
+ | "label-top-center";
27
+ labelWidth?: string;
28
+ labelAlign?: "left" | "center" | "right";
29
+ labelGap?: string;
30
+ }
31
+
32
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
33
+ (
34
+ {
35
+ label,
36
+ description,
37
+ error,
38
+ icon,
39
+ rightIcon,
40
+ prefix,
41
+ suffix,
42
+ onRightIconClick,
43
+ variant = "rounded",
44
+ labelDirection = "label-top",
45
+ labelWidth = "w-32",
46
+ labelAlign,
47
+ labelGap = "gap-1.5",
48
+ className,
49
+ onFocus,
50
+ onBlur,
51
+ ...props
52
+ },
53
+ ref,
54
+ ) => {
55
+ const [isFocused, setIsFocused] = useState(false);
56
+
57
+ const isFloating = variant === "floating";
58
+ const hasValue = props.value !== undefined && props.value !== "";
59
+ const isActive = isFocused || hasValue;
60
+
61
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
62
+ setIsFocused(true);
63
+ onFocus?.(e);
64
+ };
65
+
66
+ const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
67
+ setIsFocused(false);
68
+ onBlur?.(e);
69
+ };
70
+
71
+ const isSideLabel =
72
+ labelDirection.startsWith("label-left") ||
73
+ labelDirection.startsWith("label-right");
74
+ const alignment =
75
+ labelAlign ||
76
+ (labelDirection.includes("-left")
77
+ ? "left"
78
+ : labelDirection.includes("-right")
79
+ ? "right"
80
+ : labelDirection.includes("-center")
81
+ ? "center"
82
+ : "left");
83
+
84
+ const radiusClass =
85
+ variant === "square"
86
+ ? "rounded-none"
87
+ : variant === "curved"
88
+ ? "rounded-lg"
89
+ : "rounded-xl";
90
+
91
+ return (
92
+ <div
93
+ className={cn(
94
+ "flex w-full",
95
+ labelGap,
96
+ isSideLabel ? "flex-row" : "flex-col",
97
+ className,
98
+ )}
99
+ >
100
+ {/* Standard Label (Non-Floating) */}
101
+ {label && !isFloating && (
102
+ <div
103
+ className={cn(
104
+ "flex flex-col",
105
+ isSideLabel ? "shrink-0" : "w-full",
106
+ labelDirection.endsWith("-top") && isSideLabel && "mt-1.5",
107
+ )}
108
+ >
109
+ <div
110
+ className={cn(
111
+ isSideLabel ? labelWidth : "w-full",
112
+ "flex flex-col",
113
+ alignment === "left" && "items-start text-left",
114
+ alignment === "right" && "items-end text-right",
115
+ alignment === "center" && "items-center text-center",
116
+ )}
117
+ >
118
+ <span className="text-sm font-semibold tracking-tight text-black dark:text-white uppercase">
119
+ {label}
120
+ </span>
121
+ {description && (
122
+ <span className="text-[11px] text-gray-400 font-medium mt-0.5">
123
+ {description}
124
+ </span>
125
+ )}
126
+ </div>
127
+ </div>
128
+ )}
129
+
130
+ <div className="flex-1 relative group">
131
+ {/* Floating Label */}
132
+ {label && isFloating && (
133
+ <span
134
+ className={cn(
135
+ "absolute transition-all duration-200 pointer-events-none font-bold uppercase tracking-tight z-10 block truncate",
136
+ isActive
137
+ ? "-top-2.5 left-3 right-auto text-[10px] bg-white dark:bg-black px-1.5 text-sky-500 max-w-[calc(100%-1.5rem)]"
138
+ : cn(
139
+ "top-1/2 -translate-y-1/2 text-[13px] text-gray-400",
140
+ icon ? "left-11 right-8" : "left-4 right-4",
141
+ ),
142
+ )}
143
+ >
144
+ {label}
145
+ </span>
146
+ )}
147
+
148
+ <div className="relative flex items-center">
149
+ {icon && (
150
+ <div
151
+ className={cn(
152
+ "absolute left-4 text-gray-400 transition-colors",
153
+ isFocused ? "text-black dark:text-white" : "",
154
+ )}
155
+ >
156
+ {icon}
157
+ </div>
158
+ )}
159
+ <input
160
+ ref={ref}
161
+ {...props}
162
+ onFocus={handleFocus}
163
+ onBlur={handleBlur}
164
+ className={cn(
165
+ "w-full h-11 bg-white dark:bg-black border-2 text-[13px] font-semibold text-black dark:text-white transition-all duration-200 outline-none",
166
+ radiusClass,
167
+ icon || prefix ? "pl-11" : "px-4",
168
+ rightIcon || suffix ? "pr-11" : icon || prefix ? "" : "px-4",
169
+ isFocused
170
+ ? "border-sky-500 ring-4 ring-sky-500/10"
171
+ : "border-gray-100 dark:border-gray-800 hover:border-gray-300 dark:hover:border-gray-600",
172
+ error ? "border-red-500 ring-4 ring-red-500/10" : "",
173
+ isFloating &&
174
+ "placeholder:opacity-0 focus:placeholder:opacity-100",
175
+ )}
176
+ />
177
+ {prefix && (
178
+ <div className="absolute left-4 text-gray-400 font-semibold pointer-events-none">
179
+ {prefix}
180
+ </div>
181
+ )}
182
+ {suffix && (
183
+ <div className="absolute right-4 text-gray-400 font-semibold pointer-events-none">
184
+ {suffix}
185
+ </div>
186
+ )}
187
+ {rightIcon && (
188
+ <div
189
+ onClick={onRightIconClick}
190
+ className={cn(
191
+ "absolute right-4 text-gray-400 transition-colors",
192
+ onRightIconClick
193
+ ? "cursor-pointer hover:text-black dark:hover:text-white"
194
+ : "",
195
+ isFocused ? "text-black dark:text-white" : "",
196
+ )}
197
+ >
198
+ {rightIcon}
199
+ </div>
200
+ )}
201
+ </div>
202
+ {error && (
203
+ <span className="text-[10px] font-bold text-red-500 mt-1.5 ml-1 block animate-in fade-in slide-in-from-top-1">
204
+ {error}
205
+ </span>
206
+ )}
207
+ </div>
208
+ </div>
209
+ );
210
+ },
211
+ );
212
+
213
+ Input.displayName = "Input";
package/utils/cn.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }