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 +253 -0
- package/package.json +26 -0
- package/templates/button/button.tsx +32 -0
- package/templates/input/input.tsx +29 -0
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 }
|