lorre-blocks 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.
Files changed (3) hide show
  1. package/README.md +53 -0
  2. package/dist/index.js +351 -0
  3. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # lorre-blocks
2
+
3
+ CLI for [lorre-blocks](https://github.com/zukazine/lorre-blocks) — a shadcn-style
4
+ component library. Instead of installing a package and importing from `node_modules`,
5
+ this CLI **copies component source directly into your project**, so you own and can
6
+ edit every line.
7
+
8
+ ## Usage
9
+
10
+ In your React + Tailwind project:
11
+
12
+ ```bash
13
+ # 1. One-time setup — writes components.json
14
+ npx lorre-blocks init
15
+
16
+ # 2. Add components — copies their source in and installs their dependencies
17
+ npx lorre-blocks add button
18
+ npx lorre-blocks add input
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ### `init`
24
+ Creates a `components.json` in your project describing the registry URL and your
25
+ import aliases.
26
+
27
+ | Option | Description |
28
+ | --- | --- |
29
+ | `-r, --registry <url>` | Registry base URL to pull components from |
30
+ | `-y, --yes` | Accept defaults, skip prompts |
31
+ | `-c, --cwd <path>` | Run in a different directory |
32
+
33
+ ### `add [components...]`
34
+ Fetches each component from the registry, installs its npm dependencies with your
35
+ project's package manager, resolves any component dependencies, rewrites import
36
+ aliases to match your `components.json`, and writes the files into your project.
37
+
38
+ | Option | Description |
39
+ | --- | --- |
40
+ | `-o, --overwrite` | Overwrite existing files without asking |
41
+ | `-y, --yes` | Skip confirmation prompts |
42
+ | `-c, --cwd <path>` | Run in a different directory |
43
+
44
+ ## How it works
45
+
46
+ Components are described in a JSON **registry** served as static files. The CLI reads
47
+ `components.json`, fetches `<registry>/r/<name>.json`, resolves dependencies, and writes
48
+ the source into the directories your aliases point at (detected from your `tsconfig.json`
49
+ `paths`).
50
+
51
+ ## License
52
+
53
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { promises as fs2 } from "fs";
8
+ import path2 from "path";
9
+ import * as p from "@clack/prompts";
10
+ import color from "picocolors";
11
+
12
+ // src/utils/config.ts
13
+ import { promises as fs } from "fs";
14
+ import path from "path";
15
+ var CONFIG_FILE = "components.json";
16
+ var DEFAULT_REGISTRY = "http://localhost:3000";
17
+ var DEFAULT_ALIASES = {
18
+ components: "@/components",
19
+ ui: "@/components/ui",
20
+ lib: "@/lib",
21
+ hooks: "@/hooks",
22
+ utils: "@/lib/utils"
23
+ };
24
+ function configPath(cwd) {
25
+ return path.join(cwd, CONFIG_FILE);
26
+ }
27
+ async function configExists(cwd) {
28
+ try {
29
+ await fs.access(configPath(cwd));
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+ async function readConfig(cwd) {
36
+ try {
37
+ const raw = await fs.readFile(configPath(cwd), "utf8");
38
+ return JSON.parse(raw);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ async function writeConfig(cwd, config) {
44
+ await fs.writeFile(
45
+ configPath(cwd),
46
+ JSON.stringify(config, null, 2) + "\n",
47
+ "utf8"
48
+ );
49
+ }
50
+
51
+ // src/commands/init.ts
52
+ async function runInit(options) {
53
+ const { cwd } = options;
54
+ p.intro(color.bgCyan(color.black(" lorre-blocks init ")));
55
+ if (await configExists(cwd)) {
56
+ const overwrite = options.yes ? true : await p.confirm({
57
+ message: "components.json already exists. Overwrite it?",
58
+ initialValue: false
59
+ });
60
+ if (p.isCancel(overwrite) || !overwrite) {
61
+ p.cancel("Init aborted; existing components.json kept.");
62
+ return;
63
+ }
64
+ }
65
+ const tsx = await fileExists(path2.join(cwd, "tsconfig.json"));
66
+ const registry = options.registry ?? (options.yes ? DEFAULT_REGISTRY : await promptText(
67
+ "Registry URL",
68
+ DEFAULT_REGISTRY
69
+ ));
70
+ const config = {
71
+ $schema: "https://lorre-blocks.dev/schema.json",
72
+ registry,
73
+ tsx,
74
+ aliases: { ...DEFAULT_ALIASES }
75
+ };
76
+ await writeConfig(cwd, config);
77
+ p.outro(
78
+ `${color.green("\u2714")} Wrote components.json. Now run ${color.cyan(
79
+ "lorre-blocks add button"
80
+ )}.`
81
+ );
82
+ }
83
+ async function promptText(message, initial) {
84
+ const value = await p.text({ message, placeholder: initial, defaultValue: initial });
85
+ if (p.isCancel(value)) {
86
+ p.cancel("Init cancelled.");
87
+ process.exit(0);
88
+ }
89
+ return value || initial;
90
+ }
91
+ async function fileExists(f) {
92
+ try {
93
+ await fs2.access(f);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ // src/commands/add.ts
101
+ import { promises as fs5 } from "fs";
102
+ import path5 from "path";
103
+ import * as p2 from "@clack/prompts";
104
+ import color2 from "picocolors";
105
+
106
+ // src/utils/registry.ts
107
+ async function fetchRegistryItem(registry, name) {
108
+ const base = registry.replace(/\/$/, "");
109
+ const url = `${base}/r/${name}.json`;
110
+ let res;
111
+ try {
112
+ res = await fetch(url);
113
+ } catch (err) {
114
+ throw new Error(
115
+ `Could not reach the registry at ${url}. Is it running / correct?
116
+ ${err.message}`
117
+ );
118
+ }
119
+ if (res.status === 404) {
120
+ throw new Error(`Component "${name}" was not found in the registry (${url}).`);
121
+ }
122
+ if (!res.ok) {
123
+ throw new Error(`Failed to fetch ${url} (HTTP ${res.status}).`);
124
+ }
125
+ return await res.json();
126
+ }
127
+ async function resolveTree(registry, names) {
128
+ const resolved = /* @__PURE__ */ new Map();
129
+ async function visit(name) {
130
+ if (resolved.has(name)) return;
131
+ const item = await fetchRegistryItem(registry, name);
132
+ for (const dep of item.registryDependencies ?? []) {
133
+ await visit(dep);
134
+ }
135
+ if (!resolved.has(name)) resolved.set(name, item);
136
+ }
137
+ for (const name of names) {
138
+ await visit(name);
139
+ }
140
+ return [...resolved.values()];
141
+ }
142
+
143
+ // src/utils/package-manager.ts
144
+ import { promises as fs3 } from "fs";
145
+ import path3 from "path";
146
+ import { spawn } from "child_process";
147
+ async function detectPackageManager(cwd) {
148
+ const has = async (f) => fs3.access(path3.join(cwd, f)).then(() => true).catch(() => false);
149
+ if (await has("pnpm-lock.yaml")) return "pnpm";
150
+ if (await has("yarn.lock")) return "yarn";
151
+ if (await has("bun.lockb")) return "bun";
152
+ return "npm";
153
+ }
154
+ function installArgs(pm, deps) {
155
+ switch (pm) {
156
+ case "pnpm":
157
+ return ["add", ...deps];
158
+ case "yarn":
159
+ return ["add", ...deps];
160
+ case "bun":
161
+ return ["add", ...deps];
162
+ case "npm":
163
+ return ["install", ...deps];
164
+ }
165
+ }
166
+ async function installDependencies(cwd, pm, deps) {
167
+ if (deps.length === 0) return;
168
+ const args = installArgs(pm, deps);
169
+ await new Promise((resolve, reject) => {
170
+ const child = spawn(pm, args, {
171
+ cwd,
172
+ stdio: "inherit",
173
+ // On Windows, package managers are .cmd shims that need a shell.
174
+ shell: process.platform === "win32"
175
+ });
176
+ child.on("error", reject);
177
+ child.on("close", (code) => {
178
+ if (code === 0) resolve();
179
+ else reject(new Error(`${pm} ${args.join(" ")} exited with code ${code}`));
180
+ });
181
+ });
182
+ }
183
+
184
+ // src/utils/paths.ts
185
+ import { promises as fs4 } from "fs";
186
+ import path4 from "path";
187
+ async function resolveBaseDir(cwd) {
188
+ for (const file of ["tsconfig.json", "jsconfig.json"]) {
189
+ try {
190
+ const raw = await fs4.readFile(path4.join(cwd, file), "utf8");
191
+ const json = JSON.parse(stripJsonComments(raw));
192
+ const paths = json?.compilerOptions?.paths;
193
+ const baseUrl = json?.compilerOptions?.baseUrl ?? ".";
194
+ const target = paths?.["@/*"]?.[0];
195
+ if (target) {
196
+ const dir = target.replace(/\/\*$/, "");
197
+ return path4.resolve(cwd, baseUrl, dir);
198
+ }
199
+ } catch {
200
+ }
201
+ }
202
+ try {
203
+ const srcStat = await fs4.stat(path4.join(cwd, "src"));
204
+ if (srcStat.isDirectory()) return path4.join(cwd, "src");
205
+ } catch {
206
+ }
207
+ return cwd;
208
+ }
209
+ function aliasToDir(alias, baseDir) {
210
+ const rel = alias.replace(/^@\//, "");
211
+ return path4.join(baseDir, rel);
212
+ }
213
+ function targetDirForType(type, aliases, baseDir) {
214
+ switch (type) {
215
+ case "registry:ui":
216
+ return aliasToDir(aliases.ui, baseDir);
217
+ case "registry:hook":
218
+ return aliasToDir(aliases.hooks, baseDir);
219
+ case "registry:lib":
220
+ return aliasToDir(aliases.lib, baseDir);
221
+ }
222
+ }
223
+ function rewriteImports(content, config) {
224
+ const { aliases } = config;
225
+ const replacements = [
226
+ ["@/lib/utils", aliases.utils],
227
+ ["@/components/ui", aliases.ui],
228
+ ["@/components", aliases.components],
229
+ ["@/hooks", aliases.hooks],
230
+ ["@/lib", aliases.lib]
231
+ ];
232
+ let result = content;
233
+ for (const [from, to] of replacements) {
234
+ if (from === to) continue;
235
+ const pattern = new RegExp(
236
+ `(["'\`])${escapeRegExp(from)}(?=["'\`/])`,
237
+ "g"
238
+ );
239
+ result = result.replace(pattern, `$1${to}`);
240
+ }
241
+ return result;
242
+ }
243
+ function escapeRegExp(s) {
244
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
245
+ }
246
+ function stripJsonComments(input) {
247
+ return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/,(\s*[}\]])/g, "$1");
248
+ }
249
+
250
+ // src/commands/add.ts
251
+ async function runAdd(options) {
252
+ const { cwd, components } = options;
253
+ p2.intro(color2.bgCyan(color2.black(" lorre-blocks add ")));
254
+ const config = await readConfig(cwd);
255
+ if (!config) {
256
+ p2.cancel(
257
+ `No components.json found. Run ${color2.cyan("lorre-blocks init")} first.`
258
+ );
259
+ process.exit(1);
260
+ }
261
+ if (components.length === 0) {
262
+ p2.cancel("Specify at least one component, e.g. `lorre-blocks add button`.");
263
+ process.exit(1);
264
+ }
265
+ const spinner2 = p2.spinner();
266
+ spinner2.start("Resolving components from the registry");
267
+ let tree;
268
+ try {
269
+ tree = await resolveTree(config.registry, components);
270
+ } catch (err) {
271
+ spinner2.stop("Failed to resolve components.");
272
+ p2.cancel(err.message);
273
+ process.exit(1);
274
+ }
275
+ spinner2.stop(
276
+ `Resolved ${tree.length} item(s): ${tree.map((t) => t.name).join(", ")}`
277
+ );
278
+ const npmDeps = [...new Set(tree.flatMap((t) => t.dependencies ?? []))];
279
+ if (npmDeps.length > 0) {
280
+ const pm = await detectPackageManager(cwd);
281
+ const depSpinner = p2.spinner();
282
+ depSpinner.start(`Installing ${npmDeps.length} dependency(ies) with ${pm}`);
283
+ try {
284
+ await installDependencies(cwd, pm, npmDeps);
285
+ depSpinner.stop(`Installed: ${npmDeps.join(", ")}`);
286
+ } catch (err) {
287
+ depSpinner.stop("Dependency install failed.");
288
+ p2.cancel(err.message);
289
+ process.exit(1);
290
+ }
291
+ }
292
+ const baseDir = await resolveBaseDir(cwd);
293
+ const written = [];
294
+ for (const item of tree) {
295
+ for (const file of item.files) {
296
+ const dir = targetDirForType(file.type, config.aliases, baseDir);
297
+ const fileName = path5.basename(file.path);
298
+ const dest = path5.join(dir, fileName);
299
+ if (await fileExists2(dest)) {
300
+ const shouldOverwrite = options.overwrite || options.yes || await p2.confirm({
301
+ message: `${path5.relative(cwd, dest)} exists. Overwrite?`,
302
+ initialValue: false
303
+ });
304
+ if (p2.isCancel(shouldOverwrite) || !shouldOverwrite) {
305
+ p2.log.warn(`Skipped ${path5.relative(cwd, dest)}`);
306
+ continue;
307
+ }
308
+ }
309
+ await fs5.mkdir(dir, { recursive: true });
310
+ const content = rewriteImports(file.content, config);
311
+ await fs5.writeFile(dest, content, "utf8");
312
+ written.push(path5.relative(cwd, dest));
313
+ }
314
+ }
315
+ if (written.length > 0) {
316
+ p2.log.success(`Added:
317
+ ${written.map((f) => ` ${color2.green(f)}`).join("\n")}`);
318
+ }
319
+ p2.outro(`${color2.green("\u2714")} Done.`);
320
+ }
321
+ async function fileExists2(f) {
322
+ try {
323
+ await fs5.access(f);
324
+ return true;
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ // src/index.ts
331
+ var program = new Command();
332
+ program.name("lorre-blocks").description("Add lorre-blocks components to your project by copying their source in.").version("0.1.0");
333
+ program.command("init").description("Create a components.json config in your project.").option("-c, --cwd <path>", "working directory", process.cwd()).option("-r, --registry <url>", "registry base URL").option("-y, --yes", "skip prompts and accept defaults", false).action(async (opts) => {
334
+ await runInit({
335
+ cwd: opts.cwd,
336
+ registry: opts.registry,
337
+ yes: opts.yes
338
+ });
339
+ });
340
+ program.command("add").description("Add one or more components to your project.").argument("[components...]", "component names, e.g. button").option("-c, --cwd <path>", "working directory", process.cwd()).option("-y, --yes", "skip confirmation prompts", false).option("-o, --overwrite", "overwrite existing files without asking", false).action(async (components, opts) => {
341
+ await runAdd({
342
+ cwd: opts.cwd,
343
+ components,
344
+ yes: opts.yes,
345
+ overwrite: opts.overwrite
346
+ });
347
+ });
348
+ program.parseAsync(process.argv).catch((err) => {
349
+ console.error(err);
350
+ process.exit(1);
351
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "lorre-blocks",
3
+ "version": "0.1.0",
4
+ "description": "Add lorre-blocks components to your project by copying their source in.",
5
+ "type": "module",
6
+ "bin": {
7
+ "lorre-blocks": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch"
15
+ },
16
+ "keywords": [
17
+ "cli",
18
+ "components",
19
+ "react",
20
+ "tailwind",
21
+ "shadcn"
22
+ ],
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/zukazine/lorre-blocks.git",
27
+ "directory": "packages/cli"
28
+ },
29
+ "homepage": "https://github.com/zukazine/lorre-blocks#readme",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@clack/prompts": "^0.9.1",
35
+ "commander": "^13.0.0",
36
+ "picocolors": "^1.1.1"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.10.5",
40
+ "tsup": "^8.3.5",
41
+ "typescript": "^5.7.2"
42
+ }
43
+ }