cooterlabs 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,253 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { execSync } from "child_process";
7
+ import prompts from "prompts";
8
+ import chalk from "chalk";
9
+ import prettier from "prettier";
10
+ import ora from "ora";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const cwd = process.cwd();
15
+
16
+ const REGISTRY = {
17
+ button: {
18
+ name: "Button",
19
+ dependencies: [
20
+ "lucide-react",
21
+ "clsx",
22
+ "tailwind-merge",
23
+ "@radix-ui/react-slot",
24
+ ],
25
+ dev_dependencies: [],
26
+ },
27
+ input: {
28
+ name: "Input",
29
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
30
+ dev_dependencies: [],
31
+ },
32
+ };
33
+
34
+ const templatesDir = path.join(__dirname, "../templates");
35
+
36
+ const detectEnvironment = () => {
37
+ const env = {
38
+ framework: "react", // Next, Vite, React
39
+ typescript: false,
40
+ tailwind: false,
41
+ pkgCommand: "npm",
42
+ isAppDir: false,
43
+ componentsDir: "src/components/ui",
44
+ };
45
+
46
+ const pkgPath = path.join(cwd, "package.json");
47
+ if (fs.existsSync(pkgPath)) {
48
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
49
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
50
+
51
+ if (deps.next) env.framework = "next";
52
+ else if (deps.vite) env.framework = "vite";
53
+ else if (deps["react-scripts"]) env.framework = "react";
54
+
55
+ if (deps.typescript || fs.existsSync(path.join(cwd, "tsconfig.json"))) {
56
+ env.typescript = true;
57
+ }
58
+
59
+ if (
60
+ deps.tailwindcss ||
61
+ fs.existsSync(path.join(cwd, "tailwind.config.js")) ||
62
+ fs.existsSync(path.join(cwd, "tailwind.config.ts")) ||
63
+ fs.existsSync(path.join(cwd, "tailwind.config.mjs"))
64
+ ) {
65
+ env.tailwind = true;
66
+ }
67
+ }
68
+
69
+ // Package Manager
70
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) env.pkgCommand = "pnpm";
71
+ else if (fs.existsSync(path.join(cwd, "yarn.lock"))) env.pkgCommand = "yarn";
72
+ else if (fs.existsSync(path.join(cwd, "bun.lockb"))) env.pkgCommand = "bun";
73
+
74
+ // Directories
75
+ if (env.framework === "next") {
76
+ const hasSrc = fs.existsSync(path.join(cwd, "src"));
77
+ env.isAppDir = fs.existsSync(path.join(cwd, hasSrc ? "src/app" : "app"));
78
+ env.componentsDir = hasSrc ? "src/components/ui" : "components/ui";
79
+ } else if (env.framework === "vite" || env.framework === "react") {
80
+ env.componentsDir = "src/components/ui";
81
+ } else {
82
+ // default
83
+ env.componentsDir = "components/ui";
84
+ }
85
+
86
+ return env;
87
+ };
88
+
89
+ const installDependencies = (dependencies, pkgCommand) => {
90
+ if (!dependencies || dependencies.length === 0) return;
91
+ const cmd = pkgCommand === "npm" ? "npm install" : `${pkgCommand} add`;
92
+ execSync(`${cmd} ${dependencies.join(" ")}`, {
93
+ cwd,
94
+ stdio: "ignore",
95
+ });
96
+ };
97
+
98
+ const formatFile = async (content, filePath) => {
99
+ try {
100
+ const options = (await prettier.resolveConfig(cwd)) || {};
101
+ options.filepath = filePath;
102
+ return await prettier.format(content, options);
103
+ } catch (error) {
104
+ // Return original content if prettier fails
105
+ return content;
106
+ }
107
+ };
108
+
109
+ const main = async () => {
110
+ console.log(chalk.bold.blue("šŸš€ CooterLabs UI Library CLI"));
111
+
112
+ const env = detectEnvironment();
113
+ console.log(chalk.gray("--- Environment ---"));
114
+ console.log(`šŸ“¦ Framework: ${chalk.green(env.framework)}`);
115
+ console.log(
116
+ `šŸ“˜ TypeScript: ${env.typescript ? chalk.green("Yes") : chalk.yellow("No")}`,
117
+ );
118
+ console.log(
119
+ `šŸŽØ Tailwind: ${env.tailwind ? chalk.green("Yes") : chalk.yellow("No")}`,
120
+ );
121
+ console.log(`šŸ› ļø Pkg Manager: ${chalk.green(env.pkgCommand)}\n`);
122
+
123
+ if (!env.tailwind) {
124
+ console.log(
125
+ chalk.yellow(
126
+ "āš ļø Warning: Tailwind CSS is not detected. Components may not style properly.",
127
+ ),
128
+ );
129
+ }
130
+
131
+ const args = process.argv.slice(2);
132
+ let command = args[0];
133
+ let componentName = args[1];
134
+
135
+ if (!command || command !== "add") {
136
+ // If command isn't provided or is entirely missing, treat the first arg as the component name
137
+ command = "add";
138
+ componentName = args[0] || "";
139
+ }
140
+
141
+ let availableComponents = Object.keys(REGISTRY);
142
+ if (fs.existsSync(templatesDir)) {
143
+ const dirs = fs
144
+ .readdirSync(templatesDir)
145
+ .filter((f) => fs.statSync(path.join(templatesDir, f)).isDirectory());
146
+ if (dirs.length > 0) availableComponents = dirs;
147
+ }
148
+
149
+ if (command === "add") {
150
+ if (!componentName) {
151
+ const response = await prompts({
152
+ type: "select",
153
+ name: "component",
154
+ message: "Which component would you like to add?",
155
+ choices: availableComponents.map((c) => ({ title: c, value: c })),
156
+ });
157
+ componentName = response.component;
158
+ }
159
+
160
+ if (!componentName) {
161
+ console.log(chalk.red("āŒ Operation cancelled."));
162
+ process.exit(1);
163
+ }
164
+
165
+ if (!availableComponents.includes(componentName)) {
166
+ console.log(chalk.red(`āŒ Component '${componentName}' not found.`));
167
+ process.exit(1);
168
+ }
169
+
170
+ const templatePath = path.join(templatesDir, componentName);
171
+
172
+ // Create target dir
173
+ const targetDir = path.join(cwd, env.componentsDir);
174
+ if (!fs.existsSync(targetDir)) {
175
+ fs.mkdirSync(targetDir, { recursive: true });
176
+ }
177
+
178
+ // Determine target path
179
+ const extension = env.typescript ? ".tsx" : ".jsx";
180
+ const componentFile = `${componentName}${extension}`;
181
+ const targetPath = path.join(targetDir, componentFile);
182
+
183
+ const spinner = ora(`Installing ${componentName}...`).start();
184
+
185
+ try {
186
+ let sourceContent = "";
187
+ if (fs.existsSync(templatePath)) {
188
+ const files = fs.readdirSync(templatePath);
189
+ const srcFile = files.find(
190
+ (f) => f.endsWith(".tsx") || f.endsWith(".jsx"),
191
+ );
192
+ if (srcFile) {
193
+ sourceContent = fs.readFileSync(
194
+ path.join(templatePath, srcFile),
195
+ "utf-8",
196
+ );
197
+ } else {
198
+ sourceContent = fs.readFileSync(
199
+ path.join(templatePath, files[0]),
200
+ "utf-8",
201
+ );
202
+ }
203
+ } else {
204
+ // Fallback or generic content simply to show it works
205
+ sourceContent = `import React from 'react';\n\nexport const ${REGISTRY[componentName]?.name || "Component"} = () => { return <div>${componentName}</div>; };\n`;
206
+ }
207
+
208
+ // Hacky TS -> JS stripping if they don't use TypeScript
209
+ if (!env.typescript) {
210
+ // Simple type stripping if needed, but omitted for stability.
211
+ // It's recommended to publish compiled JS if targeting non-TS,
212
+ // or let the user fix up the .jsx file.
213
+ }
214
+
215
+ // Format code
216
+ let formattedContent = sourceContent;
217
+ try {
218
+ formattedContent = await formatFile(sourceContent, targetPath);
219
+ } catch (err) {
220
+ console.log(
221
+ chalk.yellow("āš ļø Formatting failed. Using original formatting."),
222
+ );
223
+ }
224
+
225
+ // Write file
226
+ fs.writeFileSync(targetPath, formattedContent, "utf-8");
227
+
228
+ spinner.succeed(`Created ${componentFile} in ${env.componentsDir}`);
229
+
230
+ // Install dependencies
231
+ const registryEntry = REGISTRY[componentName];
232
+ if (registryEntry && registryEntry.dependencies?.length > 0) {
233
+ const depSpinner = ora(
234
+ `Installing dependencies: ${registryEntry.dependencies.join(", ")}...`,
235
+ ).start();
236
+ installDependencies(registryEntry.dependencies, env.pkgCommand);
237
+ depSpinner.succeed(`Installed dependencies.`);
238
+ }
239
+
240
+ console.log(chalk.green(`\nāœ… ${componentName} added successfully!`));
241
+ } catch (err) {
242
+ spinner.fail(`Failed to add ${componentName}`);
243
+ console.error(chalk.red(err.message));
244
+ }
245
+ } else {
246
+ console.log(chalk.red(`āŒ Unknown command: ${command}`));
247
+ }
248
+ };
249
+
250
+ main().catch((err) => {
251
+ console.error(chalk.red(err.message));
252
+ process.exit(1);
253
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "cooterlabs",
3
+ "version": "1.0.0",
4
+ "description": "Component library CLI for CooterLabs apps",
5
+ "files": [
6
+ "bin",
7
+ "templates"
8
+ ],
9
+ "main": "index.js",
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "bin": {
14
+ "cooterlabs": "./bin/index.js"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "type": "module",
20
+ "dependencies": {
21
+ "chalk": "^5.6.2",
22
+ "ora": "^9.3.0",
23
+ "prettier": "^3.8.1",
24
+ "prompts": "^2.4.2"
25
+ }
26
+ }
@@ -0,0 +1,32 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { clsx, type ClassValue } from "clsx"
4
+ import { twMerge } from "tailwind-merge"
5
+
6
+ export function cn(...inputs: ClassValue[]) {
7
+ return twMerge(clsx(inputs))
8
+ }
9
+
10
+ export interface ButtonProps
11
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
12
+ asChild?: boolean
13
+ }
14
+
15
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
16
+ ({ className, asChild = false, ...props }, ref) => {
17
+ const Comp = asChild ? Slot : "button"
18
+ return (
19
+ <Comp
20
+ className={cn(
21
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2",
22
+ className
23
+ )}
24
+ ref={ref}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+ )
30
+ Button.displayName = "Button"
31
+
32
+ export { Button }
@@ -0,0 +1,29 @@
1
+ import * as React from "react"
2
+ import { clsx, type ClassValue } from "clsx"
3
+ import { twMerge } from "tailwind-merge"
4
+
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs))
7
+ }
8
+
9
+ export interface InputProps
10
+ extends React.InputHTMLAttributes<HTMLInputElement> { }
11
+
12
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
13
+ ({ className, type, ...props }, ref) => {
14
+ return (
15
+ <input
16
+ type={type}
17
+ className={cn(
18
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
19
+ className
20
+ )}
21
+ ref={ref}
22
+ {...props}
23
+ />
24
+ )
25
+ }
26
+ )
27
+ Input.displayName = "Input"
28
+
29
+ export { Input }