@carefully-built/cli 0.1.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/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @carefully-built/cli
2
+
3
+ CLI for adding Carefully Built components to apps as editable source code.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bunx @carefully-built/cli list
9
+ bunx @carefully-built/cli add button
10
+ ```
11
+
12
+ Use `--overwrite` to replace existing files:
13
+
14
+ ```bash
15
+ bunx @carefully-built/cli add button --overwrite
16
+ ```
17
+
18
+ The first registry entry copies:
19
+
20
+ - `components/ui/button.tsx`
21
+ - `lib/utils.ts`
22
+
23
+ Install the printed dependencies in the target app if they are not already present.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ //#region src/index.d.ts
3
+ declare function runCli(argv?: string[]): Promise<void>;
4
+ //#endregion
5
+ export { runCli };
6
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;iBAWsB,MAAA,mBAAsC"}
package/dist/index.mjs ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ import { constants, readFileSync } from "node:fs";
3
+ import { access, copyFile, mkdir } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ //#region src/registry.ts
8
+ const registryRoot = join(join(dirname(fileURLToPath(import.meta.url)), ".."), "registry");
9
+ const componentNames = ["button"];
10
+ function listRegistryComponents() {
11
+ return [...componentNames];
12
+ }
13
+ function getRegistryComponent(componentName) {
14
+ if (!componentNames.includes(componentName)) return;
15
+ const manifestPath = join(registryRoot, "ui", componentName, "manifest.json");
16
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
17
+ return {
18
+ ...manifest,
19
+ files: manifest.files.map((file) => ({
20
+ ...file,
21
+ source: join(registryRoot, "ui", componentName, file.source)
22
+ }))
23
+ };
24
+ }
25
+
26
+ //#endregion
27
+ //#region src/add.ts
28
+ async function addComponent({ componentName, cwd, overwrite }) {
29
+ const component = getRegistryComponent(componentName);
30
+ if (!component) throw new Error(`Unknown component "${componentName}"`);
31
+ const created = [];
32
+ const overwritten = [];
33
+ const skipped = [];
34
+ for (const file of component.files) {
35
+ const targetPath = join(cwd, file.target);
36
+ const exists = await fileExists(targetPath);
37
+ if (exists && !overwrite) {
38
+ skipped.push(file.target);
39
+ continue;
40
+ }
41
+ await mkdir(dirname(targetPath), { recursive: true });
42
+ await copyFile(file.source, targetPath);
43
+ if (exists) overwritten.push(file.target);
44
+ else created.push(file.target);
45
+ }
46
+ return {
47
+ componentName: component.name,
48
+ created,
49
+ overwritten,
50
+ skipped,
51
+ dependencies: component.dependencies,
52
+ peerDependencies: component.peerDependencies
53
+ };
54
+ }
55
+ async function fileExists(path) {
56
+ try {
57
+ await access(path, constants.F_OK);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/index.ts
66
+ async function runCli(argv = process.argv.slice(2)) {
67
+ const args = parseArgs(argv);
68
+ if (args.help || !args.command) {
69
+ printHelp();
70
+ return;
71
+ }
72
+ if (args.command === "list") {
73
+ for (const componentName of listRegistryComponents()) console.log(componentName);
74
+ return;
75
+ }
76
+ if (args.command === "add") {
77
+ if (!args.componentName) throw new Error("Missing component name. Example: carefully-built add button");
78
+ printAddResult(await addComponent({
79
+ componentName: args.componentName,
80
+ cwd: process.cwd(),
81
+ overwrite: args.overwrite
82
+ }));
83
+ return;
84
+ }
85
+ throw new Error(`Unknown command "${args.command}"`);
86
+ }
87
+ function parseArgs(argv) {
88
+ return {
89
+ command: argv[0],
90
+ componentName: argv[1]?.startsWith("-") ? void 0 : argv[1],
91
+ overwrite: argv.includes("--overwrite"),
92
+ help: argv.includes("--help") || argv.includes("-h")
93
+ };
94
+ }
95
+ function printHelp() {
96
+ console.log(`carefully-built
97
+
98
+ Usage:
99
+ carefully-built list
100
+ carefully-built add <component> [--overwrite]
101
+
102
+ Components:
103
+ ${listRegistryComponents().join(", ")}
104
+ `);
105
+ }
106
+ function printAddResult(result) {
107
+ for (const file of result.created) console.log(`created ${file}`);
108
+ for (const file of result.overwritten) console.log(`overwrote ${file}`);
109
+ for (const file of result.skipped) console.log(`skipped ${file}`);
110
+ if (result.dependencies.length > 0) console.log(`dependencies: ${result.dependencies.join(", ")}`);
111
+ if (result.peerDependencies.length > 0) console.log(`peer dependencies: ${result.peerDependencies.join(", ")}`);
112
+ }
113
+ runCli().catch((error) => {
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ console.error(message);
116
+ process.exitCode = 1;
117
+ });
118
+
119
+ //#endregion
120
+ export { runCli };
121
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["created: string[]","overwritten: string[]","skipped: string[]"],"sources":["../src/registry.ts","../src/add.ts","../src/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface RegistryFile {\n readonly source: string;\n readonly target: string;\n}\n\nexport interface RegistryComponent {\n readonly name: string;\n readonly description: string;\n readonly dependencies: readonly string[];\n readonly peerDependencies: readonly string[];\n readonly files: readonly RegistryFile[];\n}\n\nconst packageRoot = join(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst registryRoot = join(packageRoot, \"registry\");\nconst componentNames = [\"button\"] as const;\n\nexport function listRegistryComponents(): string[] {\n return [...componentNames];\n}\n\nexport function getRegistryComponent(\n componentName: string,\n): RegistryComponent | undefined {\n if (!componentNames.includes(componentName as (typeof componentNames)[number])) {\n return undefined;\n }\n\n const manifestPath = join(registryRoot, \"ui\", componentName, \"manifest.json\");\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as Omit<\n RegistryComponent,\n \"files\"\n > & {\n files: readonly RegistryFile[];\n };\n\n return {\n ...manifest,\n files: manifest.files.map((file) => ({\n ...file,\n source: join(registryRoot, \"ui\", componentName, file.source),\n })),\n };\n}\n","import { constants } from \"node:fs\";\nimport { access, copyFile, mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { getRegistryComponent } from \"./registry\";\n\nexport interface AddComponentOptions {\n readonly componentName: string;\n readonly cwd: string;\n readonly overwrite: boolean;\n}\n\nexport interface AddComponentResult {\n readonly componentName: string;\n readonly created: string[];\n readonly overwritten: string[];\n readonly skipped: string[];\n readonly dependencies: readonly string[];\n readonly peerDependencies: readonly string[];\n}\n\nexport async function addComponent({\n componentName,\n cwd,\n overwrite,\n}: AddComponentOptions): Promise<AddComponentResult> {\n const component = getRegistryComponent(componentName);\n\n if (!component) {\n throw new Error(`Unknown component \"${componentName}\"`);\n }\n\n const created: string[] = [];\n const overwritten: string[] = [];\n const skipped: string[] = [];\n\n for (const file of component.files) {\n const targetPath = join(cwd, file.target);\n const exists = await fileExists(targetPath);\n\n if (exists && !overwrite) {\n skipped.push(file.target);\n continue;\n }\n\n await mkdir(dirname(targetPath), { recursive: true });\n await copyFile(file.source, targetPath);\n\n if (exists) {\n overwritten.push(file.target);\n } else {\n created.push(file.target);\n }\n }\n\n return {\n componentName: component.name,\n created,\n overwritten,\n skipped,\n dependencies: component.dependencies,\n peerDependencies: component.peerDependencies,\n };\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n","#!/usr/bin/env node\nimport { addComponent } from \"./add\";\nimport { listRegistryComponents } from \"./registry\";\n\ninterface ParsedArgs {\n readonly command?: string;\n readonly componentName?: string;\n readonly overwrite: boolean;\n readonly help: boolean;\n}\n\nexport async function runCli(argv = process.argv.slice(2)): Promise<void> {\n const args = parseArgs(argv);\n\n if (args.help || !args.command) {\n printHelp();\n return;\n }\n\n if (args.command === \"list\") {\n for (const componentName of listRegistryComponents()) {\n console.log(componentName);\n }\n return;\n }\n\n if (args.command === \"add\") {\n if (!args.componentName) {\n throw new Error(\"Missing component name. Example: carefully-built add button\");\n }\n\n const result = await addComponent({\n componentName: args.componentName,\n cwd: process.cwd(),\n overwrite: args.overwrite,\n });\n\n printAddResult(result);\n return;\n }\n\n throw new Error(`Unknown command \"${args.command}\"`);\n}\n\nfunction parseArgs(argv: readonly string[]): ParsedArgs {\n return {\n command: argv[0],\n componentName: argv[1]?.startsWith(\"-\") ? undefined : argv[1],\n overwrite: argv.includes(\"--overwrite\"),\n help: argv.includes(\"--help\") || argv.includes(\"-h\"),\n };\n}\n\nfunction printHelp(): void {\n console.log(`carefully-built\n\nUsage:\n carefully-built list\n carefully-built add <component> [--overwrite]\n\nComponents:\n ${listRegistryComponents().join(\", \")}\n`);\n}\n\nfunction printAddResult(result: Awaited<ReturnType<typeof addComponent>>): void {\n for (const file of result.created) {\n console.log(`created ${file}`);\n }\n for (const file of result.overwritten) {\n console.log(`overwrote ${file}`);\n }\n for (const file of result.skipped) {\n console.log(`skipped ${file}`);\n }\n\n if (result.dependencies.length > 0) {\n console.log(`dependencies: ${result.dependencies.join(\", \")}`);\n }\n if (result.peerDependencies.length > 0) {\n console.log(`peer dependencies: ${result.peerDependencies.join(\", \")}`);\n }\n}\n\nrunCli().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@carefully-built/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for adding Carefully Built components to apps.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Alessandro Dodi",
8
+ "homepage": "https://github.com/AlessandroDodi/carefully-built-saas-kit#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/AlessandroDodi/carefully-built-saas-kit.git",
12
+ "directory": "packages/cli"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/AlessandroDodi/carefully-built-saas-kit/issues"
16
+ },
17
+ "keywords": [
18
+ "react",
19
+ "components",
20
+ "cli",
21
+ "shadcn",
22
+ "saas"
23
+ ],
24
+ "bin": {
25
+ "carefully-built": "dist/index.mjs"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "registry",
30
+ "README.md"
31
+ ],
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.mts",
35
+ "import": "./dist/index.mjs",
36
+ "default": "./dist/index.mjs"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "scripts": {
41
+ "build": "tsdown src/index.ts --format esm --dts",
42
+ "prepublishOnly": "bun run typecheck && bun run test && bun run build",
43
+ "test": "bun test",
44
+ "typecheck": "tsc --noEmit"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "devDependencies": {
50
+ "@types/bun": "^1.3.5"
51
+ }
52
+ }
@@ -0,0 +1,89 @@
1
+ import * as React from "react";
2
+ import { cva } from "class-variance-authority";
3
+ import { Slot } from "radix-ui";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground hover:brightness-90 [a]:hover:bg-primary/80",
14
+ outline:
15
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
16
+ secondary:
17
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
18
+ ghost:
19
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
20
+ destructive:
21
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default:
26
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
27
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
28
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
29
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
30
+ icon: "size-8",
31
+ "icon-xs":
32
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
33
+ "icon-sm":
34
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
35
+ "icon-lg": "size-9",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ variant: "default",
40
+ size: "default",
41
+ },
42
+ },
43
+ );
44
+
45
+ type ButtonVariant =
46
+ | "default"
47
+ | "outline"
48
+ | "secondary"
49
+ | "ghost"
50
+ | "destructive"
51
+ | "link";
52
+ type ButtonSize =
53
+ | "default"
54
+ | "xs"
55
+ | "sm"
56
+ | "lg"
57
+ | "icon"
58
+ | "icon-xs"
59
+ | "icon-sm"
60
+ | "icon-lg";
61
+
62
+ interface ButtonProps extends React.ComponentProps<"button"> {
63
+ readonly asChild?: boolean;
64
+ readonly size?: ButtonSize;
65
+ readonly variant?: ButtonVariant;
66
+ }
67
+
68
+ function Button({
69
+ className,
70
+ variant = "default",
71
+ size = "default",
72
+ asChild = false,
73
+ ...props
74
+ }: ButtonProps) {
75
+ const Comp = asChild ? Slot.Root : "button";
76
+
77
+ return (
78
+ <Comp
79
+ data-slot="button"
80
+ data-variant={variant}
81
+ data-size={size}
82
+ className={cn(buttonVariants({ variant, size, className }))}
83
+ {...props}
84
+ />
85
+ );
86
+ }
87
+
88
+ export { Button, buttonVariants };
89
+ export type { ButtonProps, ButtonSize, ButtonVariant };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "button",
3
+ "description": "Shared button primitive with Carefully Built variants and sizes.",
4
+ "dependencies": ["class-variance-authority", "clsx", "tailwind-merge"],
5
+ "peerDependencies": ["react", "radix-ui"],
6
+ "files": [
7
+ {
8
+ "source": "button.tsx",
9
+ "target": "components/ui/button.tsx"
10
+ },
11
+ {
12
+ "source": "cn.ts",
13
+ "target": "lib/utils.ts"
14
+ }
15
+ ]
16
+ }