@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 +70 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +297 -0
- package/package.json +32 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|