@carefully-built/cli 0.1.0 → 0.1.1
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 +127 -7
- package/assets/hero.png +0 -0
- package/dist/index.mjs +63 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,23 +1,143 @@
|
|
|
1
1
|
# @carefully-built/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Bring Carefully Built SaaS components into a React app in the form that fits the job: import the maintained package when you want upgrades, or eject the source when a component needs to become part of the product.
|
|
6
|
+
|
|
7
|
+
The CLI is the editable-source side of the Carefully Built SaaS Kit. It copies registry components into your app using the same mental model as shadcn: the code lands in your repository, keeps normal imports such as `@/lib/utils`, and can be changed without waiting for a package release.
|
|
8
|
+
|
|
9
|
+
## Two Ways To Use The Kit
|
|
10
|
+
|
|
11
|
+
### 1. Managed Package Imports
|
|
12
|
+
|
|
13
|
+
Use package imports when the component should stay shared and receive fixes through npm updates.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @carefully-built/ui
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { Button, SmartTable, TableToolbar } from "@carefully-built/ui";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This is the best default for stable primitives, CRUD helpers, table controls, layout shells, and shared SaaS behavior that should remain consistent across apps.
|
|
24
|
+
|
|
25
|
+
### 2. Editable Source With The CLI
|
|
26
|
+
|
|
27
|
+
Use the CLI when a component needs to become local code.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bunx @carefully-built/cli add button
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The CLI reads common shadcn project conventions. If your app uses `components.json` aliases and `tsconfig.json` paths like `@/* -> ./src/*`, files are copied into `src/components/ui` and `src/lib`. Without those conventions, files are copied to root-level `components/ui` and `lib`.
|
|
34
|
+
|
|
35
|
+
## Install And Commands
|
|
36
|
+
|
|
37
|
+
You can run it without installing:
|
|
6
38
|
|
|
7
39
|
```bash
|
|
8
40
|
bunx @carefully-built/cli list
|
|
9
41
|
bunx @carefully-built/cli add button
|
|
10
42
|
```
|
|
11
43
|
|
|
12
|
-
|
|
44
|
+
Or with npm:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @carefully-built/cli list
|
|
48
|
+
npx @carefully-built/cli add button
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Replace existing local files only when you ask for it:
|
|
13
52
|
|
|
14
53
|
```bash
|
|
15
54
|
bunx @carefully-built/cli add button --overwrite
|
|
16
55
|
```
|
|
17
56
|
|
|
18
|
-
|
|
57
|
+
## What Gets Copied
|
|
58
|
+
|
|
59
|
+
The first registry entry is intentionally small:
|
|
60
|
+
|
|
61
|
+
```txt
|
|
62
|
+
button
|
|
63
|
+
├─ components/ui/button.tsx
|
|
64
|
+
└─ lib/utils.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
In a `src` app with shadcn aliases, that becomes:
|
|
68
|
+
|
|
69
|
+
```txt
|
|
70
|
+
button
|
|
71
|
+
├─ src/components/ui/button.tsx
|
|
72
|
+
└─ src/lib/utils.ts
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The copied `Button` uses:
|
|
19
76
|
|
|
20
|
-
- `
|
|
21
|
-
- `
|
|
77
|
+
- `class-variance-authority`
|
|
78
|
+
- `clsx`
|
|
79
|
+
- `tailwind-merge`
|
|
80
|
+
- `react`
|
|
81
|
+
- `radix-ui`
|
|
22
82
|
|
|
23
|
-
|
|
83
|
+
The CLI prints dependency hints after copying. Install anything your app does not already provide.
|
|
84
|
+
|
|
85
|
+
## When To Import vs Eject
|
|
86
|
+
|
|
87
|
+
Import from `@carefully-built/ui` when:
|
|
88
|
+
|
|
89
|
+
- you want package updates and bug fixes without owning the source
|
|
90
|
+
- the component is generic enough to stay shared
|
|
91
|
+
- app-specific behavior can be passed through props, `className`, slots, or `classes`
|
|
92
|
+
|
|
93
|
+
Use `@carefully-built/cli add` when:
|
|
94
|
+
|
|
95
|
+
- the component is about to become product-specific
|
|
96
|
+
- design or behavior needs local edits
|
|
97
|
+
- you want to inspect and own every line in the consuming app
|
|
98
|
+
- the component should follow the same source-control workflow as the rest of the app
|
|
99
|
+
|
|
100
|
+
## Current Registry
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bunx @carefully-built/cli list
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Available now:
|
|
107
|
+
|
|
108
|
+
- `button`
|
|
109
|
+
|
|
110
|
+
More registry entries should be added deliberately, starting with low-risk primitives and then moving toward composed SaaS surfaces such as tables, toolbars, forms, and shells.
|
|
111
|
+
|
|
112
|
+
## Project Assumptions
|
|
113
|
+
|
|
114
|
+
The generated components assume a modern React app with:
|
|
115
|
+
|
|
116
|
+
- React 18 or 19
|
|
117
|
+
- TypeScript
|
|
118
|
+
- Tailwind-compatible design tokens such as `bg-primary`, `text-primary-foreground`, `border`, `ring`, and `muted`
|
|
119
|
+
- an alias for `@/lib/utils`, or a project structure where the CLI can resolve that alias through `components.json` and `tsconfig.json`
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
From the monorepo root:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
bun install
|
|
127
|
+
bun run --cwd packages/cli test
|
|
128
|
+
bun run --cwd packages/cli typecheck
|
|
129
|
+
bun run --cwd packages/cli build
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Before publishing:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
cd packages/cli
|
|
136
|
+
npm publish --dry-run --access public
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Publish:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm publish --access public
|
|
143
|
+
```
|
package/assets/hero.png
ADDED
|
Binary file
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { constants, readFileSync } from "node:fs";
|
|
3
|
-
import { access, copyFile, mkdir } from "node:fs/promises";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { access, copyFile, mkdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, normalize } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
//#region src/registry.ts
|
|
@@ -31,17 +31,19 @@ async function addComponent({ componentName, cwd, overwrite }) {
|
|
|
31
31
|
const created = [];
|
|
32
32
|
const overwritten = [];
|
|
33
33
|
const skipped = [];
|
|
34
|
+
const projectConfig = await readProjectConfig(cwd);
|
|
34
35
|
for (const file of component.files) {
|
|
35
|
-
const
|
|
36
|
+
const target = resolveTargetPath(file.target, projectConfig);
|
|
37
|
+
const targetPath = join(cwd, target);
|
|
36
38
|
const exists = await fileExists(targetPath);
|
|
37
39
|
if (exists && !overwrite) {
|
|
38
|
-
skipped.push(
|
|
40
|
+
skipped.push(target);
|
|
39
41
|
continue;
|
|
40
42
|
}
|
|
41
43
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
42
44
|
await copyFile(file.source, targetPath);
|
|
43
|
-
if (exists) overwritten.push(
|
|
44
|
-
else created.push(
|
|
45
|
+
if (exists) overwritten.push(target);
|
|
46
|
+
else created.push(target);
|
|
45
47
|
}
|
|
46
48
|
return {
|
|
47
49
|
componentName: component.name,
|
|
@@ -60,6 +62,61 @@ async function fileExists(path) {
|
|
|
60
62
|
return false;
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
async function readProjectConfig(cwd) {
|
|
66
|
+
const [componentsJson, tsconfigJson] = await Promise.all([readJsonFile(join(cwd, "components.json")), readJsonFile(join(cwd, "tsconfig.json"))]);
|
|
67
|
+
return {
|
|
68
|
+
uiAlias: readString(componentsJson, ["aliases", "ui"]),
|
|
69
|
+
utilsAlias: readString(componentsJson, ["aliases", "utils"]),
|
|
70
|
+
...readPrimaryTsconfigAlias(tsconfigJson)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function resolveTargetPath(target, config) {
|
|
74
|
+
if (target === "lib/utils.ts" && config.utilsAlias) return resolveAliasPath(`${config.utilsAlias}.ts`, config);
|
|
75
|
+
if (target.startsWith("components/ui/") && config.uiAlias) {
|
|
76
|
+
const fileName = target.slice(14);
|
|
77
|
+
return resolveAliasPath(`${config.uiAlias}/${fileName}`, config);
|
|
78
|
+
}
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
81
|
+
function resolveAliasPath(path, config) {
|
|
82
|
+
if (config.aliasPrefix && config.aliasTarget && path.startsWith(config.aliasPrefix)) return cleanPath(path.replace(config.aliasPrefix, config.aliasTarget));
|
|
83
|
+
if (path.startsWith("@/")) return cleanPath(path.slice(2));
|
|
84
|
+
return cleanPath(path);
|
|
85
|
+
}
|
|
86
|
+
function readPrimaryTsconfigAlias(value) {
|
|
87
|
+
const aliasTarget = readArray(readRecord(readRecord(value, "compilerOptions"), "paths"), "@/*").find((entry) => typeof entry === "string");
|
|
88
|
+
if (typeof aliasTarget !== "string") return {};
|
|
89
|
+
return {
|
|
90
|
+
aliasPrefix: "@/",
|
|
91
|
+
aliasTarget: aliasTarget.replace(/\/\*$/, "/")
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function readJsonFile(path) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(stripJsonComments(await readFile(path, "utf8")));
|
|
97
|
+
} catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function stripJsonComments(source) {
|
|
102
|
+
return source.replace(/^\s*\/\/.*$/gm, "");
|
|
103
|
+
}
|
|
104
|
+
function readString(value, path) {
|
|
105
|
+
let current = value;
|
|
106
|
+
for (const key of path) current = readRecord(current, key);
|
|
107
|
+
return typeof current === "string" ? current : void 0;
|
|
108
|
+
}
|
|
109
|
+
function readRecord(value, key) {
|
|
110
|
+
if (!value || typeof value !== "object") return;
|
|
111
|
+
return value[key];
|
|
112
|
+
}
|
|
113
|
+
function readArray(value, key) {
|
|
114
|
+
const array = readRecord(value, key);
|
|
115
|
+
return Array.isArray(array) ? array : [];
|
|
116
|
+
}
|
|
117
|
+
function cleanPath(path) {
|
|
118
|
+
return normalize(path).replace(/^\.\//, "");
|
|
119
|
+
}
|
|
63
120
|
|
|
64
121
|
//#endregion
|
|
65
122
|
//#region src/index.ts
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
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, readFile } from \"node:fs/promises\";\nimport { dirname, join, normalize } 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 const projectConfig = await readProjectConfig(cwd);\n\n for (const file of component.files) {\n const target = resolveTargetPath(file.target, projectConfig);\n const targetPath = join(cwd, target);\n const exists = await fileExists(targetPath);\n\n if (exists && !overwrite) {\n skipped.push(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(target);\n } else {\n created.push(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\ninterface ProjectConfig {\n readonly uiAlias?: string;\n readonly utilsAlias?: string;\n readonly aliasPrefix?: string;\n readonly aliasTarget?: string;\n}\n\nasync function readProjectConfig(cwd: string): Promise<ProjectConfig> {\n const [componentsJson, tsconfigJson] = await Promise.all([\n readJsonFile(join(cwd, \"components.json\")),\n readJsonFile(join(cwd, \"tsconfig.json\")),\n ]);\n\n return {\n uiAlias: readString(componentsJson, [\"aliases\", \"ui\"]),\n utilsAlias: readString(componentsJson, [\"aliases\", \"utils\"]),\n ...readPrimaryTsconfigAlias(tsconfigJson),\n };\n}\n\nfunction resolveTargetPath(target: string, config: ProjectConfig): string {\n if (target === \"lib/utils.ts\" && config.utilsAlias) {\n return resolveAliasPath(`${config.utilsAlias}.ts`, config);\n }\n\n if (target.startsWith(\"components/ui/\") && config.uiAlias) {\n const fileName = target.slice(\"components/ui/\".length);\n return resolveAliasPath(`${config.uiAlias}/${fileName}`, config);\n }\n\n return target;\n}\n\nfunction resolveAliasPath(path: string, config: ProjectConfig): string {\n if (\n config.aliasPrefix &&\n config.aliasTarget &&\n path.startsWith(config.aliasPrefix)\n ) {\n return cleanPath(path.replace(config.aliasPrefix, config.aliasTarget));\n }\n\n if (path.startsWith(\"@/\")) {\n return cleanPath(path.slice(2));\n }\n\n return cleanPath(path);\n}\n\nfunction readPrimaryTsconfigAlias(value: unknown): Pick<\n ProjectConfig,\n \"aliasPrefix\" | \"aliasTarget\"\n> {\n const paths = readRecord(readRecord(value, \"compilerOptions\"), \"paths\");\n const aliasTargets = readArray(paths, \"@/*\");\n const aliasTarget = aliasTargets.find((entry) => typeof entry === \"string\");\n\n if (typeof aliasTarget !== \"string\") {\n return {};\n }\n\n return {\n aliasPrefix: \"@/\",\n aliasTarget: aliasTarget.replace(/\\/\\*$/, \"/\"),\n };\n}\n\nasync function readJsonFile(path: string): Promise<unknown> {\n try {\n return JSON.parse(stripJsonComments(await readFile(path, \"utf8\")));\n } catch {\n return undefined;\n }\n}\n\nfunction stripJsonComments(source: string): string {\n return source.replace(/^\\s*\\/\\/.*$/gm, \"\");\n}\n\nfunction readString(value: unknown, path: readonly string[]): string | undefined {\n let current = value;\n\n for (const key of path) {\n current = readRecord(current, key);\n }\n\n return typeof current === \"string\" ? current : undefined;\n}\n\nfunction readRecord(value: unknown, key: string): unknown {\n if (!value || typeof value !== \"object\") {\n return undefined;\n }\n\n return (value as Record<string, unknown>)[key];\n}\n\nfunction readArray(value: unknown, key: string): unknown[] {\n const array = readRecord(value, key);\n return Array.isArray(array) ? array : [];\n}\n\nfunction cleanPath(path: string): string {\n return normalize(path).replace(/^\\.\\//, \"\");\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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carefully-built/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Add Carefully Built SaaS components to apps as editable source, with package imports when you want managed upgrades.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Alessandro Dodi",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"carefully-built": "dist/index.mjs"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
+
"assets",
|
|
28
29
|
"dist",
|
|
29
30
|
"registry",
|
|
30
31
|
"README.md"
|