@the-grove/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,70 @@
1
+ # @the-grove/cli
2
+
3
+ CLI tool for the-grove component library.
4
+
5
+ ## Usage
6
+
7
+ ### Add components
8
+ ```bash
9
+ npx the-grove add async-button
10
+ ```
11
+
12
+ ### List components
13
+ ```bash
14
+ npx the-grove list
15
+ ```
16
+
17
+ ### Contribute components
18
+ ```bash
19
+ npx the-grove contribute ./components/my-component.tsx
20
+ ```
21
+
22
+ ## Commands
23
+
24
+ ### add
25
+
26
+ Add components to your project.
27
+
28
+ ```bash
29
+ npx the-grove add <component-name> [options]
30
+
31
+ Options:
32
+ -p, --path <path> Custom installation path
33
+ -y, --yes Skip confirmation prompts
34
+ ```
35
+
36
+ ### list
37
+
38
+ List all available components.
39
+
40
+ ```bash
41
+ npx the-grove list [options]
42
+
43
+ Options:
44
+ -c, --category <category> Filter by category
45
+ -t, --tag <tag> Filter by tag
46
+ ```
47
+
48
+ ### contribute
49
+
50
+ Contribute components back to the-grove.
51
+
52
+ ```bash
53
+ npx the-grove contribute <file...>
54
+ ```
55
+
56
+ Requires GITHUB_TOKEN environment variable or will prompt for token.
57
+
58
+ ## Development
59
+
60
+ ```bash
61
+ # Build
62
+ npm run build
63
+
64
+ # Watch mode
65
+ npm run dev
66
+
67
+ # Test locally
68
+ npm link
69
+ the-grove --help
70
+ ```
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/add.ts
7
+ import { execa } from "execa";
8
+ import ora from "ora";
9
+ import chalk from "chalk";
10
+ import prompts from "prompts";
11
+ import fs from "fs-extra";
12
+ var REGISTRY_BASE_URL = "https://raw.githubusercontent.com/matthewnaples/the-grove/main/packages/registry/registry";
13
+ async function add(components, options) {
14
+ if (!components || components.length === 0) {
15
+ console.log(chalk.yellow("No components specified."));
16
+ console.log("Usage: npx the-grove add <component-name>");
17
+ console.log("\nRun `npx the-grove list` to see available components.");
18
+ return;
19
+ }
20
+ const spinner = ora();
21
+ try {
22
+ const stack = await detectProjectStack();
23
+ for (const componentName of components) {
24
+ spinner.start(`Adding ${componentName}...`);
25
+ const registryUrl = await findComponentRegistry(componentName);
26
+ if (!registryUrl) {
27
+ spinner.fail(`Component '${componentName}' not found`);
28
+ continue;
29
+ }
30
+ const registryEntry = await fetchRegistryEntry(registryUrl);
31
+ const missingDeps = await checkDependencies(registryEntry, stack);
32
+ if (missingDeps.length > 0 && !options.yes) {
33
+ spinner.stop();
34
+ const { continue: shouldContinue } = await prompts({
35
+ type: "confirm",
36
+ name: "continue",
37
+ message: `${componentName} requires: ${missingDeps.join(", ")}. Continue?`,
38
+ initial: true
39
+ });
40
+ if (!shouldContinue) {
41
+ console.log(chalk.gray(`Skipped ${componentName}`));
42
+ continue;
43
+ }
44
+ spinner.start(`Adding ${componentName}...`);
45
+ }
46
+ const shadcnUrl = registryUrl;
47
+ const args = ["shadcn@latest", "add", shadcnUrl];
48
+ if (options.path) {
49
+ args.push("--path", options.path);
50
+ }
51
+ if (options.yes) {
52
+ args.push("--yes");
53
+ }
54
+ await execa("npx", args, { stdio: "inherit" });
55
+ spinner.succeed(`Added ${componentName}`);
56
+ }
57
+ console.log(chalk.green("\n\u2705 All components added successfully!"));
58
+ } catch (error) {
59
+ spinner.fail("Failed to add component");
60
+ console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
61
+ process.exit(1);
62
+ }
63
+ }
64
+ async function detectProjectStack() {
65
+ try {
66
+ const packageJson = await fs.readJson("./package.json");
67
+ const deps = {
68
+ ...packageJson.dependencies,
69
+ ...packageJson.devDependencies
70
+ };
71
+ return {
72
+ hasConvex: "convex" in deps,
73
+ hasClerk: "@clerk/nextjs" in deps || "@clerk/clerk-react" in deps,
74
+ hasNextAuth: "next-auth" in deps
75
+ };
76
+ } catch {
77
+ return {
78
+ hasConvex: false,
79
+ hasClerk: false,
80
+ hasNextAuth: false
81
+ };
82
+ }
83
+ }
84
+ async function findComponentRegistry(componentName) {
85
+ const categories = ["core", "convex", "clerk", "convex-clerk"];
86
+ for (const category of categories) {
87
+ const url = `${REGISTRY_BASE_URL}/${category}/${componentName}.json`;
88
+ try {
89
+ const response = await fetch(url);
90
+ if (response.ok) {
91
+ return url;
92
+ }
93
+ } catch {
94
+ continue;
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ async function fetchRegistryEntry(url) {
100
+ const response = await fetch(url);
101
+ if (!response.ok) {
102
+ throw new Error(`Failed to fetch registry: ${url}`);
103
+ }
104
+ return response.json();
105
+ }
106
+ async function checkDependencies(registryEntry, stack) {
107
+ const missing = [];
108
+ const deps = registryEntry.dependencies || [];
109
+ for (const dep of deps) {
110
+ if (dep === "convex" && !stack.hasConvex) {
111
+ missing.push("convex");
112
+ }
113
+ if ((dep === "@clerk/nextjs" || dep === "@clerk/clerk-react") && !stack.hasClerk) {
114
+ missing.push("clerk");
115
+ }
116
+ }
117
+ return missing;
118
+ }
119
+
120
+ // src/commands/contribute.ts
121
+ import { Octokit } from "@octokit/rest";
122
+ import fs2 from "fs-extra";
123
+ import path from "path";
124
+ import prompts2 from "prompts";
125
+ import ora2 from "ora";
126
+ import chalk2 from "chalk";
127
+ var TEMPLATE_REPO = {
128
+ owner: "matthewnaples",
129
+ repo: "the-grove",
130
+ branch: "main"
131
+ };
132
+ async function contribute(filePaths) {
133
+ const spinner = ora2();
134
+ try {
135
+ const files = [];
136
+ for (const fp of filePaths) {
137
+ if (!await fs2.pathExists(fp)) {
138
+ throw new Error(`File not found: ${fp}`);
139
+ }
140
+ files.push({
141
+ localPath: fp,
142
+ content: await fs2.readFile(fp, "utf-8"),
143
+ componentName: path.basename(fp, path.extname(fp))
144
+ });
145
+ }
146
+ const answers = await prompts2([
147
+ {
148
+ type: "select",
149
+ name: "changeType",
150
+ message: "What kind of change is this?",
151
+ choices: [
152
+ { title: "New component", value: "feat" },
153
+ { title: "Bug fix", value: "fix" },
154
+ { title: "Enhancement", value: "enhance" }
155
+ ]
156
+ },
157
+ {
158
+ type: "text",
159
+ name: "description",
160
+ message: "Describe your changes:",
161
+ initial: files.length === 1 ? `Update ${files[0].componentName}` : `Update ${files.length} components`
162
+ }
163
+ ]);
164
+ if (!answers.changeType || !answers.description) {
165
+ console.log(chalk2.gray("Cancelled."));
166
+ return;
167
+ }
168
+ const token = process.env.GITHUB_TOKEN || await promptForToken();
169
+ const octokit = new Octokit({ auth: token });
170
+ spinner.start("Creating branch...");
171
+ const { data: refData } = await octokit.git.getRef({
172
+ ...TEMPLATE_REPO,
173
+ ref: `heads/${TEMPLATE_REPO.branch}`
174
+ });
175
+ const mainSha = refData.object.sha;
176
+ const branchName = `${answers.changeType}/${files[0].componentName}-${Date.now()}`;
177
+ await octokit.git.createRef({
178
+ ...TEMPLATE_REPO,
179
+ ref: `refs/heads/${branchName}`,
180
+ sha: mainSha
181
+ });
182
+ spinner.succeed(`Created branch: ${branchName}`);
183
+ for (const file of files) {
184
+ spinner.start(`Uploading ${file.localPath}...`);
185
+ const templatePath = mapLocalPathToTemplate(file.localPath);
186
+ let existingFileSha;
187
+ try {
188
+ const { data: existingFile } = await octokit.repos.getContent({
189
+ ...TEMPLATE_REPO,
190
+ path: templatePath,
191
+ ref: branchName
192
+ });
193
+ if ("sha" in existingFile) {
194
+ existingFileSha = existingFile.sha;
195
+ }
196
+ } catch {
197
+ }
198
+ await octokit.repos.createOrUpdateFileContents({
199
+ ...TEMPLATE_REPO,
200
+ path: templatePath,
201
+ message: `${answers.changeType}: ${file.componentName}`,
202
+ content: Buffer.from(file.content).toString("base64"),
203
+ branch: branchName,
204
+ sha: existingFileSha
205
+ });
206
+ spinner.succeed(`${existingFileSha ? "Updated" : "Added"} ${templatePath}`);
207
+ }
208
+ spinner.start("Creating pull request...");
209
+ const { data: pr } = await octokit.pulls.create({
210
+ ...TEMPLATE_REPO,
211
+ title: answers.description,
212
+ head: branchName,
213
+ base: TEMPLATE_REPO.branch,
214
+ body: generatePRBody(files, answers)
215
+ });
216
+ spinner.succeed("Pull request created!");
217
+ console.log(chalk2.green("\n\u2705 Contribution submitted successfully!"));
218
+ console.log(chalk2.blue(`\u{1F517} ${pr.html_url}`));
219
+ console.log(chalk2.gray("\nReview and merge when ready."));
220
+ } catch (error) {
221
+ spinner.fail("Failed to contribute");
222
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
223
+ process.exit(1);
224
+ }
225
+ }
226
+ function mapLocalPathToTemplate(localPath) {
227
+ const normalized = localPath.replace(/^\.\//, "").replace(/components\/ui\//, "components/").replace(/components\//, "");
228
+ const name = path.basename(normalized, path.extname(normalized));
229
+ return `packages/components/src/core/${name}/index.tsx`;
230
+ }
231
+ async function promptForToken() {
232
+ const { token } = await prompts2({
233
+ type: "password",
234
+ name: "token",
235
+ message: "Enter your GitHub personal access token:",
236
+ validate: (value) => value.length > 0 || "Token is required"
237
+ });
238
+ return token;
239
+ }
240
+ function generatePRBody(files, answers) {
241
+ return `## ${answers.description}
242
+
243
+ ### Changes
244
+ ${files.map((f) => `- ${f.localPath}`).join("\n")}
245
+
246
+ ### Type
247
+ \`${answers.changeType}\`
248
+
249
+ ---
250
+ *Contributed via \`npx the-grove contribute\`*`;
251
+ }
252
+
253
+ // src/commands/list.ts
254
+ import chalk3 from "chalk";
255
+ var REGISTRY_BASE_URL2 = "https://raw.githubusercontent.com/matthewnaples/the-grove/main/packages/registry/registry";
256
+ async function list(options) {
257
+ console.log(chalk3.bold("\n\u{1F4E6} Available Components\n"));
258
+ const categories = options.category ? [options.category] : ["core", "convex", "clerk", "convex-clerk"];
259
+ for (const category of categories) {
260
+ console.log(chalk3.bold.blue(`
261
+ ${category.toUpperCase()}:`));
262
+ try {
263
+ const components = await fetchCategoryComponents(category);
264
+ for (const component of components) {
265
+ if (options.tag && !component.tags?.includes(options.tag)) {
266
+ continue;
267
+ }
268
+ const deps = component.dependencies?.length ? chalk3.gray(` (requires: ${component.dependencies.join(", ")})`) : "";
269
+ console.log(` ${chalk3.green(component.name)}${deps}`);
270
+ if (component.description) {
271
+ console.log(` ${chalk3.gray(component.description)}`);
272
+ }
273
+ }
274
+ } catch (error) {
275
+ console.log(chalk3.gray(` No components found`));
276
+ }
277
+ }
278
+ console.log(chalk3.gray("\nRun `npx the-grove add <component>` to install\n"));
279
+ }
280
+ async function fetchCategoryComponents(category) {
281
+ const indexUrl = `${REGISTRY_BASE_URL2}/${category}/index.json`;
282
+ try {
283
+ const response = await fetch(indexUrl);
284
+ if (!response.ok) return [];
285
+ return await response.json();
286
+ } catch {
287
+ return [];
288
+ }
289
+ }
290
+
291
+ // src/index.ts
292
+ var program = new Command();
293
+ program.name("the-grove").description("Component library CLI for managing UI components").version("0.1.0");
294
+ program.command("add").description("Add component(s) to your project").argument("[components...]", "Component names to add").option("-p, --path <path>", "Custom installation path").option("-y, --yes", "Skip confirmation prompts").action(add);
295
+ program.command("contribute").description("Contribute component(s) back to the template repository").argument("<files...>", "Component file paths to contribute").action(contribute);
296
+ program.command("list").description("List all available components").option("-c, --category <category>", "Filter by category (core, convex, clerk, convex-clerk)").option("-t, --tag <tag>", "Filter by tag").action(list);
297
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@the-grove/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "CLI for the-grove component library",
6
+ "bin": {
7
+ "the-grove": "./dist/index.js"
8
+ },
9
+ "files": ["dist"],
10
+ "scripts": {
11
+ "build": "tsup",
12
+ "dev": "tsup --watch",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "dependencies": {
16
+ "commander": "^11.0.0",
17
+ "prompts": "^2.4.2",
18
+ "execa": "^8.0.0",
19
+ "ora": "^7.0.0",
20
+ "chalk": "^5.3.0",
21
+ "fs-extra": "^11.2.0",
22
+ "@octokit/rest": "^20.0.0",
23
+ "node-fetch": "^3.3.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/prompts": "^2.4.0",
27
+ "@types/fs-extra": "^11.0.0",
28
+ "tsup": "^8.0.0",
29
+ "typescript": "^5.0.0"
30
+ },
31
+ "keywords": ["the-grove", "components", "cli", "nextjs", "react"]
32
+ }