@reviewlico/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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/index.js +414 -0
- package/dist/index.js.map +1 -0
- package/dist/registry/registry/plain/ReviewCard.tsx +36 -0
- package/dist/registry/registry/plain/ReviewForm.tsx +133 -0
- package/dist/registry/registry/plain/ReviewList.tsx +105 -0
- package/dist/registry/registry/plain/StarRating.tsx +62 -0
- package/dist/registry/registry/plain/review-components.css +492 -0
- package/dist/registry/registry/tailwind/ReviewCard.tsx +36 -0
- package/dist/registry/registry/tailwind/ReviewForm.tsx +199 -0
- package/dist/registry/registry/tailwind/ReviewList.tsx +108 -0
- package/dist/registry/registry/tailwind/StarRating.tsx +61 -0
- package/dist/registry/shared/api.ts +130 -0
- package/dist/registry/shared/hooks/useReviews.ts +65 -0
- package/dist/registry/shared/hooks/useSubmitReview.ts +36 -0
- package/dist/registry/shared/types.ts +56 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reviewlico
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# `@reviewlico/cli`
|
|
2
|
+
|
|
3
|
+
Add Reviewlico review components directly into your app as editable source files.
|
|
4
|
+
|
|
5
|
+
Reviewlico website and dashboard: [https://reviewlico.vercel.app](https://reviewlico.vercel.app)
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Use the CLI without installing it globally:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @reviewlico/cli add ReviewForm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This copies the selected component into your project and creates `reviewlico.config.json` on first run if needed.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Configure output dir + preferred style
|
|
21
|
+
npx @reviewlico/cli init
|
|
22
|
+
|
|
23
|
+
# Add a single component
|
|
24
|
+
npx @reviewlico/cli add ReviewForm
|
|
25
|
+
|
|
26
|
+
# Use the Tailwind variant
|
|
27
|
+
npx @reviewlico/cli add ReviewList --styles tailwind
|
|
28
|
+
|
|
29
|
+
# Prompt before overwriting files
|
|
30
|
+
npx @reviewlico/cli add ReviewForm --confirm
|
|
31
|
+
|
|
32
|
+
# Leave existing files untouched
|
|
33
|
+
npx @reviewlico/cli add ReviewForm --skip-existing
|
|
34
|
+
|
|
35
|
+
# See all available components
|
|
36
|
+
npx @reviewlico/cli list
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Available Components
|
|
40
|
+
|
|
41
|
+
- `ReviewForm`
|
|
42
|
+
- `ReviewList`
|
|
43
|
+
|
|
44
|
+
The CLI copies source files into your app so you can edit them freely. It does not add a runtime UI dependency.
|
|
45
|
+
|
|
46
|
+
## Supported Frameworks
|
|
47
|
+
|
|
48
|
+
- Next.js
|
|
49
|
+
- Vite
|
|
50
|
+
|
|
51
|
+
The CLI detects your framework and whether Tailwind is configured to preselect a sensible default style on first run.
|
|
52
|
+
|
|
53
|
+
## Configuration and Env Vars
|
|
54
|
+
|
|
55
|
+
After generation, import the copied files directly from your own codebase:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { ReviewForm } from './components/reviews/ReviewForm';
|
|
59
|
+
|
|
60
|
+
<ReviewForm config={{ externalProductId: 'prod-001' }} />
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Set your API URL and embed key in your app's env file so you do not hardcode them in JSX:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# .env (Vite)
|
|
67
|
+
VITE_REVIEWLICO_API_URL=https://api.example.com
|
|
68
|
+
VITE_REVIEWLICO_API_KEY=rk_live_...
|
|
69
|
+
|
|
70
|
+
# .env.local (Next.js)
|
|
71
|
+
NEXT_PUBLIC_REVIEWLICO_API_URL=https://api.example.com
|
|
72
|
+
NEXT_PUBLIC_REVIEWLICO_API_KEY=rk_live_...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The API key is a client-side embed key, so it will be visible in the browser bundle. Keep any secret keys server-side.
|
|
76
|
+
|
|
77
|
+
## Overwrite Behavior
|
|
78
|
+
|
|
79
|
+
`reviewlico add` overwrites existing files by default.
|
|
80
|
+
|
|
81
|
+
- Use `--confirm` to approve each overwrite interactively
|
|
82
|
+
- Use `--skip-existing` to keep existing files unchanged
|
|
83
|
+
|
|
84
|
+
## Dashboard
|
|
85
|
+
|
|
86
|
+
Manage your Reviewlico setup in the dashboard:
|
|
87
|
+
|
|
88
|
+
- Website and dashboard: [https://reviewlico.vercel.app](https://reviewlico.vercel.app)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import pc5 from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/commands/add.ts
|
|
8
|
+
import pc3 from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/lib/registry.ts
|
|
11
|
+
var REGISTRY = {
|
|
12
|
+
ReviewForm: {
|
|
13
|
+
description: "Review submission form with star rating, text, name, and email fields",
|
|
14
|
+
variantFiles: ["ReviewForm.tsx", "StarRating.tsx"],
|
|
15
|
+
sharedFiles: ["types.ts", "api.ts", "hooks/useSubmitReview.ts"],
|
|
16
|
+
cssFile: "review-components.css"
|
|
17
|
+
},
|
|
18
|
+
ReviewList: {
|
|
19
|
+
description: "Paginated list of published reviews with optional inline submission form",
|
|
20
|
+
variantFiles: ["ReviewList.tsx", "ReviewForm.tsx", "ReviewCard.tsx", "StarRating.tsx"],
|
|
21
|
+
sharedFiles: ["types.ts", "api.ts", "hooks/useReviews.ts", "hooks/useSubmitReview.ts"],
|
|
22
|
+
cssFile: "review-components.css"
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/lib/config.ts
|
|
27
|
+
import { readFile, writeFile } from "fs/promises";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
var CONFIG_FILE = "reviewlico.config.json";
|
|
30
|
+
var ConfigError = class extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "ConfigError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
async function readConfig(cwd) {
|
|
37
|
+
const filePath = join(cwd, CONFIG_FILE);
|
|
38
|
+
let raw;
|
|
39
|
+
try {
|
|
40
|
+
raw = await readFile(filePath, "utf-8");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.code === "ENOENT") return null;
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
throw new ConfigError(
|
|
49
|
+
`${CONFIG_FILE} contains invalid JSON. Fix it or delete it to re-initialize.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function writeConfig(cwd, config) {
|
|
54
|
+
await writeFile(join(cwd, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/lib/copy.ts
|
|
58
|
+
import { copyFile, mkdir, readdir, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
59
|
+
import { existsSync } from "fs";
|
|
60
|
+
import { dirname, isAbsolute, join as join2, relative, resolve } from "path";
|
|
61
|
+
import { fileURLToPath } from "url";
|
|
62
|
+
import prompts from "prompts";
|
|
63
|
+
import pc from "picocolors";
|
|
64
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
65
|
+
var REGISTRY_ROOT = join2(dirname(__filename), "registry");
|
|
66
|
+
function isSafeRegistryPath(file) {
|
|
67
|
+
if (isAbsolute(file)) return false;
|
|
68
|
+
return !file.split(/[\\/]/).includes("..");
|
|
69
|
+
}
|
|
70
|
+
async function copyComponent(entry, style, outputDir, cwd, overwritePolicy = "overwrite") {
|
|
71
|
+
const resolvedOutput = resolve(cwd, outputDir);
|
|
72
|
+
const rel = relative(resolve(cwd), resolvedOutput);
|
|
73
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
74
|
+
throw new ConfigError(
|
|
75
|
+
`outputDir "${outputDir}" must be within the project root. Update reviewlico.config.json.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const allFiles = [...entry.variantFiles, ...entry.sharedFiles];
|
|
79
|
+
if (entry.cssFile) allFiles.push(entry.cssFile);
|
|
80
|
+
for (const file of allFiles) {
|
|
81
|
+
if (!isSafeRegistryPath(file)) {
|
|
82
|
+
throw new ConfigError(`Invalid registry file path: "${file}"`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const copied = [];
|
|
86
|
+
const overwritten = [];
|
|
87
|
+
const skipped = [];
|
|
88
|
+
const filePairs = [];
|
|
89
|
+
for (const file of entry.variantFiles) {
|
|
90
|
+
filePairs.push({
|
|
91
|
+
src: join2(REGISTRY_ROOT, "registry", style, file),
|
|
92
|
+
dest: join2(resolvedOutput, file),
|
|
93
|
+
transformSharedImports: file.endsWith(".ts") || file.endsWith(".tsx")
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const file of entry.sharedFiles) {
|
|
97
|
+
filePairs.push({
|
|
98
|
+
src: join2(REGISTRY_ROOT, "shared", file),
|
|
99
|
+
dest: join2(resolvedOutput, file)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (style === "plain" && entry.cssFile) {
|
|
103
|
+
filePairs.push({
|
|
104
|
+
src: join2(REGISTRY_ROOT, "registry", "plain", entry.cssFile),
|
|
105
|
+
dest: join2(resolvedOutput, entry.cssFile)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
for (const { src, dest, transformSharedImports } of filePairs) {
|
|
109
|
+
const fileName = relative(resolvedOutput, dest);
|
|
110
|
+
const exists = existsSync(dest);
|
|
111
|
+
if (exists) {
|
|
112
|
+
if (overwritePolicy === "skip") {
|
|
113
|
+
console.log(` ${pc.dim("skip")} ${fileName}`);
|
|
114
|
+
skipped.push(fileName);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (overwritePolicy === "prompt") {
|
|
118
|
+
const { overwrite } = await prompts({
|
|
119
|
+
type: "confirm",
|
|
120
|
+
name: "overwrite",
|
|
121
|
+
message: `${pc.yellow(fileName)} already exists. Overwrite?`,
|
|
122
|
+
initial: false
|
|
123
|
+
});
|
|
124
|
+
if (!overwrite) {
|
|
125
|
+
console.log(` ${pc.dim("skip")} ${fileName}`);
|
|
126
|
+
skipped.push(fileName);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
132
|
+
if (transformSharedImports) {
|
|
133
|
+
const contents = await readFile2(src, "utf-8");
|
|
134
|
+
const updated = contents.replace(/\.\.\/\.\.\/shared\//g, "./");
|
|
135
|
+
await writeFile2(dest, updated, "utf-8");
|
|
136
|
+
} else {
|
|
137
|
+
await copyFile(src, dest);
|
|
138
|
+
}
|
|
139
|
+
if (exists) {
|
|
140
|
+
console.log(` ${pc.yellow("overwrite")} ${fileName}`);
|
|
141
|
+
overwritten.push(fileName);
|
|
142
|
+
} else {
|
|
143
|
+
console.log(` ${pc.green("copy")} ${fileName}`);
|
|
144
|
+
copied.push(fileName);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { copied, overwritten, skipped };
|
|
148
|
+
}
|
|
149
|
+
async function registryExists() {
|
|
150
|
+
try {
|
|
151
|
+
await readdir(REGISTRY_ROOT);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/commands/init.ts
|
|
159
|
+
import prompts2 from "prompts";
|
|
160
|
+
import pc2 from "picocolors";
|
|
161
|
+
|
|
162
|
+
// src/lib/detect.ts
|
|
163
|
+
import { existsSync as existsSync2 } from "fs";
|
|
164
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
165
|
+
import { join as join3 } from "path";
|
|
166
|
+
async function readPackageJson(cwd) {
|
|
167
|
+
try {
|
|
168
|
+
const raw = await readFile3(join3(cwd, "package.json"), "utf-8");
|
|
169
|
+
return JSON.parse(raw);
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function hasDependency(pkg, name) {
|
|
175
|
+
if (!pkg) return false;
|
|
176
|
+
return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
|
|
177
|
+
}
|
|
178
|
+
function detectFramework(pkg) {
|
|
179
|
+
if (hasDependency(pkg, "next")) return "next";
|
|
180
|
+
if (hasDependency(pkg, "vite") || hasDependency(pkg, "@vitejs/plugin-react")) return "vite";
|
|
181
|
+
if (hasDependency(pkg, "react-scripts")) return "cra";
|
|
182
|
+
return "unknown";
|
|
183
|
+
}
|
|
184
|
+
function detectTailwind(cwd, pkg) {
|
|
185
|
+
if (hasDependency(pkg, "tailwindcss")) return true;
|
|
186
|
+
const configFiles = [
|
|
187
|
+
"tailwind.config.js",
|
|
188
|
+
"tailwind.config.cjs",
|
|
189
|
+
"tailwind.config.mjs",
|
|
190
|
+
"tailwind.config.ts"
|
|
191
|
+
];
|
|
192
|
+
return configFiles.some((file) => existsSync2(join3(cwd, file)));
|
|
193
|
+
}
|
|
194
|
+
async function detectProject(cwd) {
|
|
195
|
+
const pkg = await readPackageJson(cwd);
|
|
196
|
+
return {
|
|
197
|
+
framework: detectFramework(pkg),
|
|
198
|
+
hasTailwind: detectTailwind(cwd, pkg)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/commands/init.ts
|
|
203
|
+
async function runInit(cwd) {
|
|
204
|
+
const existing = await readConfig(cwd);
|
|
205
|
+
if (existing) {
|
|
206
|
+
console.log(`${pc2.green("\u2713")} Found existing ${CONFIG_FILE}`);
|
|
207
|
+
return existing;
|
|
208
|
+
}
|
|
209
|
+
console.log(`
|
|
210
|
+
${pc2.bold("reviewlico")} \u2014 let's get you set up
|
|
211
|
+
`);
|
|
212
|
+
const detection = await detectProject(cwd);
|
|
213
|
+
const frameworkLabel = detection.framework === "next" ? "Next.js" : detection.framework === "vite" ? "Vite" : detection.framework === "cra" ? "Create React App" : "Unknown framework";
|
|
214
|
+
console.log(
|
|
215
|
+
pc2.dim(`Detected ${frameworkLabel}; Tailwind: ${detection.hasTailwind ? "yes" : "no"}`)
|
|
216
|
+
);
|
|
217
|
+
const answers = await prompts2(
|
|
218
|
+
[
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
name: "outputDir",
|
|
222
|
+
message: "Where should components be copied?",
|
|
223
|
+
initial: "src/components/reviews",
|
|
224
|
+
validate: (v) => v.trim().length > 0 ? true : "Path cannot be empty"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: "select",
|
|
228
|
+
name: "style",
|
|
229
|
+
message: "Which styling variant do you want?",
|
|
230
|
+
choices: [
|
|
231
|
+
{ title: "Plain CSS (BEM classes, --rc-* tokens)", value: "plain" },
|
|
232
|
+
{ title: "Tailwind CSS (utility classes)", value: "tailwind" }
|
|
233
|
+
],
|
|
234
|
+
initial: detection.hasTailwind ? 1 : 0
|
|
235
|
+
}
|
|
236
|
+
],
|
|
237
|
+
{
|
|
238
|
+
onCancel: () => {
|
|
239
|
+
console.log("\nSetup cancelled.");
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
const config = {
|
|
245
|
+
outputDir: answers.outputDir.trim().replace(/\/$/, ""),
|
|
246
|
+
style: answers.style
|
|
247
|
+
};
|
|
248
|
+
await writeConfig(cwd, config);
|
|
249
|
+
console.log(`
|
|
250
|
+
${pc2.green("\u2713")} Created ${CONFIG_FILE}
|
|
251
|
+
`);
|
|
252
|
+
return config;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/commands/add.ts
|
|
256
|
+
async function runAdd(componentName, styleFlag, options) {
|
|
257
|
+
const cwd = process.cwd();
|
|
258
|
+
const entry = REGISTRY[componentName];
|
|
259
|
+
if (!entry) {
|
|
260
|
+
const names = Object.keys(REGISTRY).join(", ");
|
|
261
|
+
console.error(`${pc3.red("error")} Unknown component "${componentName}". Available: ${names}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
if (!await registryExists()) {
|
|
265
|
+
console.error(
|
|
266
|
+
`${pc3.red("error")} Component registry not found. Make sure @reviewlico/review-components is present.`
|
|
267
|
+
);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
let config = await readConfig(cwd);
|
|
271
|
+
if (!config) {
|
|
272
|
+
config = await runInit(cwd);
|
|
273
|
+
}
|
|
274
|
+
const style = styleFlag ?? config.style;
|
|
275
|
+
if (style !== "plain" && style !== "tailwind") {
|
|
276
|
+
console.error(`${pc3.red("error")} Invalid style "${style}". Use "plain" or "tailwind".`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
if (styleFlag && styleFlag !== config.style) {
|
|
280
|
+
config.style = style;
|
|
281
|
+
await writeConfig(cwd, config);
|
|
282
|
+
console.log(`${pc3.dim(`Updated style to "${style}" in reviewlico.config.json`)}`);
|
|
283
|
+
}
|
|
284
|
+
if (options.confirm && options.skipExisting) {
|
|
285
|
+
console.error(`${pc3.red("error")} Use only one of --confirm or --skip-existing.`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const overwritePolicy = options.confirm ? "prompt" : options.skipExisting ? "skip" : "overwrite";
|
|
289
|
+
const detection = await detectProject(cwd);
|
|
290
|
+
const frameworkLabel = detection.framework === "next" ? "Next.js" : detection.framework === "vite" ? "Vite" : detection.framework === "cra" ? "Create React App" : "Unknown";
|
|
291
|
+
const overwriteLabel = overwritePolicy === "prompt" ? "prompt" : overwritePolicy === "skip" ? "skip" : "overwrite";
|
|
292
|
+
console.log(
|
|
293
|
+
`
|
|
294
|
+
Adding ${pc3.cyan(componentName)} (${pc3.bold(style)}) \u2192 ${pc3.dim(config.outputDir)}
|
|
295
|
+
`
|
|
296
|
+
);
|
|
297
|
+
console.log(pc3.dim(`Framework: ${frameworkLabel}`));
|
|
298
|
+
console.log(pc3.dim(`Tailwind detected: ${detection.hasTailwind ? "yes" : "no"}`));
|
|
299
|
+
console.log(pc3.dim(`Overwrite policy: ${overwriteLabel}`));
|
|
300
|
+
const { copied, overwritten, skipped } = await copyComponent(
|
|
301
|
+
entry,
|
|
302
|
+
style,
|
|
303
|
+
config.outputDir,
|
|
304
|
+
cwd,
|
|
305
|
+
overwritePolicy
|
|
306
|
+
);
|
|
307
|
+
console.log("");
|
|
308
|
+
const addedCount = copied.length;
|
|
309
|
+
const overwrittenCount = overwritten.length;
|
|
310
|
+
const skippedCount = skipped.length;
|
|
311
|
+
if (addedCount > 0) {
|
|
312
|
+
console.log(`${pc3.green("\u2713")} ${addedCount} file${addedCount !== 1 ? "s" : ""} added`);
|
|
313
|
+
}
|
|
314
|
+
if (overwrittenCount > 0) {
|
|
315
|
+
console.log(
|
|
316
|
+
`${pc3.yellow("\u2713")} ${overwrittenCount} file${overwrittenCount !== 1 ? "s" : ""} overwritten`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (skippedCount > 0) {
|
|
320
|
+
console.log(
|
|
321
|
+
`${pc3.dim(`${skippedCount} file${skippedCount !== 1 ? "s" : ""} skipped (already exist)`)}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (style === "plain" && entry.cssFile) {
|
|
325
|
+
console.log(
|
|
326
|
+
`
|
|
327
|
+
${pc3.yellow("Reminder:")} Import the CSS file once in your app:
|
|
328
|
+
${pc3.dim(`import './${config.outputDir}/${entry.cssFile}';`)}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (style === "tailwind" && !detection.hasTailwind) {
|
|
332
|
+
console.log(
|
|
333
|
+
`
|
|
334
|
+
${pc3.yellow("Warning:")} Tailwind was not detected in this project. The Tailwind variant requires Tailwind CSS to be configured.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (detection.framework === "cra") {
|
|
338
|
+
console.log(`
|
|
339
|
+
${pc3.yellow("Warning:")} Create React App is not supported.`);
|
|
340
|
+
}
|
|
341
|
+
console.log(
|
|
342
|
+
`
|
|
343
|
+
Next steps:
|
|
344
|
+
${pc3.dim(`import { ${componentName} } from './${config.outputDir}/${componentName}';`)}
|
|
345
|
+
${pc3.dim(`<${componentName} config={{ externalProductId: 'prod-001' }} />`)}
|
|
346
|
+
`
|
|
347
|
+
);
|
|
348
|
+
if (detection.framework === "next") {
|
|
349
|
+
console.log(
|
|
350
|
+
`${pc3.bold("Env vars (Next.js):")}
|
|
351
|
+
${pc3.dim("NEXT_PUBLIC_REVIEWLICO_API_URL=https://api.example.com")}
|
|
352
|
+
${pc3.dim("NEXT_PUBLIC_REVIEWLICO_API_KEY=rk_live_...")}
|
|
353
|
+
`
|
|
354
|
+
);
|
|
355
|
+
} else if (detection.framework === "vite") {
|
|
356
|
+
console.log(
|
|
357
|
+
`${pc3.bold("Env vars (Vite):")}
|
|
358
|
+
${pc3.dim("VITE_REVIEWLICO_API_URL=https://api.example.com")}
|
|
359
|
+
${pc3.dim("VITE_REVIEWLICO_API_KEY=rk_live_...")}
|
|
360
|
+
`
|
|
361
|
+
);
|
|
362
|
+
} else {
|
|
363
|
+
console.log(
|
|
364
|
+
`${pc3.bold("Env vars:")}
|
|
365
|
+
${pc3.dim("VITE_REVIEWLICO_API_URL=https://api.example.com")}
|
|
366
|
+
${pc3.dim("VITE_REVIEWLICO_API_KEY=rk_live_...")}
|
|
367
|
+
${pc3.dim("NEXT_PUBLIC_REVIEWLICO_API_URL=https://api.example.com")}
|
|
368
|
+
${pc3.dim("NEXT_PUBLIC_REVIEWLICO_API_KEY=rk_live_...")}
|
|
369
|
+
`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/commands/list.ts
|
|
375
|
+
import pc4 from "picocolors";
|
|
376
|
+
function runList() {
|
|
377
|
+
console.log(`
|
|
378
|
+
${pc4.bold("Available components:")}
|
|
379
|
+
`);
|
|
380
|
+
for (const [name, entry] of Object.entries(REGISTRY)) {
|
|
381
|
+
console.log(` ${pc4.cyan(name)}`);
|
|
382
|
+
console.log(` ${pc4.dim(entry.description)}
|
|
383
|
+
`);
|
|
384
|
+
}
|
|
385
|
+
console.log(`Add a component: ${pc4.bold("npx @reviewlico/cli add <component>")}`);
|
|
386
|
+
console.log(`With Tailwind: ${pc4.bold("npx @reviewlico/cli add <component> --styles tailwind")}
|
|
387
|
+
`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/index.ts
|
|
391
|
+
var program = new Command();
|
|
392
|
+
program.name("reviewlico").description("Add reviewlico components to your project").version("0.1.0");
|
|
393
|
+
program.command("init").description("Configure reviewlico for this project").action(async () => {
|
|
394
|
+
await runInit(process.cwd());
|
|
395
|
+
});
|
|
396
|
+
program.command("add <component>").description("Add a component to your project").option("--styles <variant>", "Styling variant: plain or tailwind").option("--confirm", "Prompt before overwriting existing files").option("--skip-existing", "Skip files that already exist").action(
|
|
397
|
+
async (component, options) => {
|
|
398
|
+
await runAdd(component, options.styles, {
|
|
399
|
+
confirm: options.confirm,
|
|
400
|
+
skipExisting: options.skipExisting
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
program.command("list").description("List available components").action(() => {
|
|
405
|
+
runList();
|
|
406
|
+
});
|
|
407
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
408
|
+
if (err instanceof ConfigError) {
|
|
409
|
+
console.error(`${pc5.red("error")} ${err.message}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
throw err;
|
|
413
|
+
});
|
|
414
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/add.ts","../src/lib/registry.ts","../src/lib/config.ts","../src/lib/copy.ts","../src/commands/init.ts","../src/lib/detect.ts","../src/commands/list.ts"],"sourcesContent":["import { Command } from 'commander';\nimport pc from 'picocolors';\nimport { runAdd } from './commands/add.js';\nimport { runInit } from './commands/init.js';\nimport { runList } from './commands/list.js';\nimport { ConfigError } from './lib/config.js';\n\ndeclare const __CLI_VERSION__: string;\n\nconst program = new Command();\n\nprogram\n .name('reviewlico')\n .description('Add reviewlico components to your project')\n .version(__CLI_VERSION__);\n\nprogram\n .command('init')\n .description('Configure reviewlico for this project')\n .action(async () => {\n await runInit(process.cwd());\n });\n\nprogram\n .command('add <component>')\n .description('Add a component to your project')\n .option('--styles <variant>', 'Styling variant: plain or tailwind')\n .option('--confirm', 'Prompt before overwriting existing files')\n .option('--skip-existing', 'Skip files that already exist')\n .action(\n async (\n component: string,\n options: { styles?: string; confirm?: boolean; skipExisting?: boolean },\n ) => {\n await runAdd(component, options.styles, {\n confirm: options.confirm,\n skipExisting: options.skipExisting,\n });\n },\n );\n\nprogram\n .command('list')\n .description('List available components')\n .action(() => {\n runList();\n });\n\nprogram.parseAsync(process.argv).catch((err: unknown) => {\n if (err instanceof ConfigError) {\n console.error(`${pc.red('error')} ${err.message}`);\n process.exit(1);\n }\n throw err;\n});\n","import pc from 'picocolors';\nimport { REGISTRY, type Style } from '../lib/registry.js';\nimport { readConfig, writeConfig } from '../lib/config.js';\nimport { copyComponent, registryExists, type OverwritePolicy } from '../lib/copy.js';\nimport { runInit } from './init.js';\nimport { detectProject } from '../lib/detect.js';\n\nexport async function runAdd(\n componentName: string,\n styleFlag: string | undefined,\n options: { confirm?: boolean; skipExisting?: boolean },\n) {\n const cwd = process.cwd();\n\n // Validate component name\n const entry = REGISTRY[componentName];\n if (!entry) {\n const names = Object.keys(REGISTRY).join(', ');\n console.error(`${pc.red('error')} Unknown component \"${componentName}\". Available: ${names}`);\n process.exit(1);\n }\n\n // Ensure registry source exists (dev environment check)\n if (!(await registryExists())) {\n console.error(\n `${pc.red('error')} Component registry not found. Make sure @reviewlico/review-components is present.`,\n );\n process.exit(1);\n }\n\n // Load or create config\n let config = await readConfig(cwd);\n if (!config) {\n config = await runInit(cwd);\n }\n\n // Apply --styles override\n const style: Style = (styleFlag as Style | undefined) ?? config.style;\n if (style !== 'plain' && style !== 'tailwind') {\n console.error(`${pc.red('error')} Invalid style \"${style}\". Use \"plain\" or \"tailwind\".`);\n process.exit(1);\n }\n\n // Persist style change if flag was provided\n if (styleFlag && styleFlag !== config.style) {\n config.style = style;\n await writeConfig(cwd, config);\n console.log(`${pc.dim(`Updated style to \"${style}\" in reviewlico.config.json`)}`);\n }\n\n if (options.confirm && options.skipExisting) {\n console.error(`${pc.red('error')} Use only one of --confirm or --skip-existing.`);\n process.exit(1);\n }\n\n const overwritePolicy: OverwritePolicy = options.confirm\n ? 'prompt'\n : options.skipExisting\n ? 'skip'\n : 'overwrite';\n\n const detection = await detectProject(cwd);\n const frameworkLabel =\n detection.framework === 'next'\n ? 'Next.js'\n : detection.framework === 'vite'\n ? 'Vite'\n : detection.framework === 'cra'\n ? 'Create React App'\n : 'Unknown';\n const overwriteLabel =\n overwritePolicy === 'prompt' ? 'prompt' : overwritePolicy === 'skip' ? 'skip' : 'overwrite';\n\n console.log(\n `\\nAdding ${pc.cyan(componentName)} (${pc.bold(style)}) → ${pc.dim(config.outputDir)}\\n`,\n );\n console.log(pc.dim(`Framework: ${frameworkLabel}`));\n console.log(pc.dim(`Tailwind detected: ${detection.hasTailwind ? 'yes' : 'no'}`));\n console.log(pc.dim(`Overwrite policy: ${overwriteLabel}`));\n\n const { copied, overwritten, skipped } = await copyComponent(\n entry,\n style,\n config.outputDir,\n cwd,\n overwritePolicy,\n );\n\n console.log('');\n\n const addedCount = copied.length;\n const overwrittenCount = overwritten.length;\n const skippedCount = skipped.length;\n if (addedCount > 0) {\n console.log(`${pc.green('✓')} ${addedCount} file${addedCount !== 1 ? 's' : ''} added`);\n }\n if (overwrittenCount > 0) {\n console.log(\n `${pc.yellow('✓')} ${overwrittenCount} file${\n overwrittenCount !== 1 ? 's' : ''\n } overwritten`,\n );\n }\n if (skippedCount > 0) {\n console.log(\n `${pc.dim(`${skippedCount} file${skippedCount !== 1 ? 's' : ''} skipped (already exist)`)}`,\n );\n }\n\n if (style === 'plain' && entry.cssFile) {\n console.log(\n `\\n${pc.yellow('Reminder:')} Import the CSS file once in your app:\\n` +\n ` ${pc.dim(`import './${config.outputDir}/${entry.cssFile}';`)}`,\n );\n }\n\n if (style === 'tailwind' && !detection.hasTailwind) {\n console.log(\n `\\n${pc.yellow('Warning:')} Tailwind was not detected in this project. ` +\n `The Tailwind variant requires Tailwind CSS to be configured.`,\n );\n }\n\n if (detection.framework === 'cra') {\n console.log(`\\n${pc.yellow('Warning:')} Create React App is not supported.`);\n }\n\n console.log(\n `\\nNext steps:\\n` +\n ` ${pc.dim(`import { ${componentName} } from './${config.outputDir}/${componentName}';`)}\\n` +\n ` ${pc.dim(`<${componentName} config={{ externalProductId: 'prod-001' }} />`)}\\n`,\n );\n\n if (detection.framework === 'next') {\n console.log(\n `${pc.bold('Env vars (Next.js):')}\\n` +\n ` ${pc.dim('NEXT_PUBLIC_REVIEWLICO_API_URL=https://api.example.com')}\\n` +\n ` ${pc.dim('NEXT_PUBLIC_REVIEWLICO_API_KEY=rk_live_...')}\\n`,\n );\n } else if (detection.framework === 'vite') {\n console.log(\n `${pc.bold('Env vars (Vite):')}\\n` +\n ` ${pc.dim('VITE_REVIEWLICO_API_URL=https://api.example.com')}\\n` +\n ` ${pc.dim('VITE_REVIEWLICO_API_KEY=rk_live_...')}\\n`,\n );\n } else {\n console.log(\n `${pc.bold('Env vars:')}\\n` +\n ` ${pc.dim('VITE_REVIEWLICO_API_URL=https://api.example.com')}\\n` +\n ` ${pc.dim('VITE_REVIEWLICO_API_KEY=rk_live_...')}\\n` +\n ` ${pc.dim('NEXT_PUBLIC_REVIEWLICO_API_URL=https://api.example.com')}\\n` +\n ` ${pc.dim('NEXT_PUBLIC_REVIEWLICO_API_KEY=rk_live_...')}\\n`,\n );\n }\n}\n","export type Style = 'plain' | 'tailwind';\n\nexport interface RegistryEntry {\n description: string;\n /** Files sourced from registry/<style>/ */\n variantFiles: string[];\n /** Files sourced from shared/ — same regardless of style */\n sharedFiles: string[];\n /** CSS file name inside registry/plain/ — only copied when style === 'plain' */\n cssFile?: string;\n}\n\nexport const REGISTRY: Record<string, RegistryEntry> = {\n ReviewForm: {\n description: 'Review submission form with star rating, text, name, and email fields',\n variantFiles: ['ReviewForm.tsx', 'StarRating.tsx'],\n sharedFiles: ['types.ts', 'api.ts', 'hooks/useSubmitReview.ts'],\n cssFile: 'review-components.css',\n },\n ReviewList: {\n description: 'Paginated list of published reviews with optional inline submission form',\n variantFiles: ['ReviewList.tsx', 'ReviewForm.tsx', 'ReviewCard.tsx', 'StarRating.tsx'],\n sharedFiles: ['types.ts', 'api.ts', 'hooks/useReviews.ts', 'hooks/useSubmitReview.ts'],\n cssFile: 'review-components.css',\n },\n};\n\nexport function getComponentNames(): string[] {\n return Object.keys(REGISTRY);\n}\n","import { readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { Style } from './registry.js';\n\nexport const CONFIG_FILE = 'reviewlico.config.json';\n\nexport interface ReviewlicoConfig {\n outputDir: string;\n style: Style;\n}\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\nexport async function readConfig(cwd: string): Promise<ReviewlicoConfig | null> {\n const filePath = join(cwd, CONFIG_FILE);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null; // normal first-run\n throw err; // permission error or other unexpected failure — rethrow\n }\n try {\n return JSON.parse(raw) as ReviewlicoConfig;\n } catch {\n throw new ConfigError(\n `${CONFIG_FILE} contains invalid JSON. Fix it or delete it to re-initialize.`,\n );\n }\n}\n\nexport async function writeConfig(cwd: string, config: ReviewlicoConfig): Promise<void> {\n await writeFile(join(cwd, CONFIG_FILE), JSON.stringify(config, null, 2) + '\\n', 'utf-8');\n}\n","import { copyFile, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport prompts from 'prompts';\nimport pc from 'picocolors';\nimport type { RegistryEntry, Style } from './registry.js';\nimport { ConfigError } from './config.js';\n\n// dist/index.js lives in packages/cli/dist/\n// dist/registry/ is the bundled copy of packages/review-components/ (copied by tsup onSuccess)\nconst __filename = fileURLToPath(import.meta.url);\nconst REGISTRY_ROOT = join(dirname(__filename), 'registry');\n\nexport interface CopyResult {\n copied: string[];\n overwritten: string[];\n skipped: string[];\n}\n\nexport type OverwritePolicy = 'overwrite' | 'prompt' | 'skip';\n\nfunction isSafeRegistryPath(file: string): boolean {\n if (isAbsolute(file)) return false;\n return !file.split(/[\\\\/]/).includes('..');\n}\n\nexport async function copyComponent(\n entry: RegistryEntry,\n style: Style,\n outputDir: string,\n cwd: string,\n overwritePolicy: OverwritePolicy = 'overwrite',\n): Promise<CopyResult> {\n // Guard: outputDir must stay within the project root (cwd)\n const resolvedOutput = resolve(cwd, outputDir);\n const rel = relative(resolve(cwd), resolvedOutput);\n if (rel.startsWith('..') || isAbsolute(rel)) {\n throw new ConfigError(\n `outputDir \"${outputDir}\" must be within the project root. Update reviewlico.config.json.`,\n );\n }\n\n // Guard: registry file paths must not escape the registry directory\n const allFiles = [...entry.variantFiles, ...entry.sharedFiles];\n if (entry.cssFile) allFiles.push(entry.cssFile);\n for (const file of allFiles) {\n if (!isSafeRegistryPath(file)) {\n throw new ConfigError(`Invalid registry file path: \"${file}\"`);\n }\n }\n\n const copied: string[] = [];\n const overwritten: string[] = [];\n const skipped: string[] = [];\n\n // Build list of { src, dest } pairs\n const filePairs: Array<{ src: string; dest: string; transformSharedImports?: boolean }> = [];\n\n for (const file of entry.variantFiles) {\n filePairs.push({\n src: join(REGISTRY_ROOT, 'registry', style, file),\n dest: join(resolvedOutput, file),\n transformSharedImports: file.endsWith('.ts') || file.endsWith('.tsx'),\n });\n }\n\n for (const file of entry.sharedFiles) {\n filePairs.push({\n src: join(REGISTRY_ROOT, 'shared', file),\n dest: join(resolvedOutput, file),\n });\n }\n\n if (style === 'plain' && entry.cssFile) {\n filePairs.push({\n src: join(REGISTRY_ROOT, 'registry', 'plain', entry.cssFile),\n dest: join(resolvedOutput, entry.cssFile),\n });\n }\n\n for (const { src, dest, transformSharedImports } of filePairs) {\n const fileName = relative(resolvedOutput, dest);\n\n const exists = existsSync(dest);\n if (exists) {\n if (overwritePolicy === 'skip') {\n console.log(` ${pc.dim('skip')} ${fileName}`);\n skipped.push(fileName);\n continue;\n }\n if (overwritePolicy === 'prompt') {\n const { overwrite } = await prompts({\n type: 'confirm',\n name: 'overwrite',\n message: `${pc.yellow(fileName)} already exists. Overwrite?`,\n initial: false,\n });\n\n if (!overwrite) {\n console.log(` ${pc.dim('skip')} ${fileName}`);\n skipped.push(fileName);\n continue;\n }\n }\n }\n\n await mkdir(dirname(dest), { recursive: true });\n if (transformSharedImports) {\n const contents = await readFile(src, 'utf-8');\n const updated = contents.replace(/\\.\\.\\/\\.\\.\\/shared\\//g, './');\n await writeFile(dest, updated, 'utf-8');\n } else {\n await copyFile(src, dest);\n }\n if (exists) {\n console.log(` ${pc.yellow('overwrite')} ${fileName}`);\n overwritten.push(fileName);\n } else {\n console.log(` ${pc.green('copy')} ${fileName}`);\n copied.push(fileName);\n }\n }\n\n return { copied, overwritten, skipped };\n}\n\nexport async function registryExists(): Promise<boolean> {\n try {\n await readdir(REGISTRY_ROOT);\n return true;\n } catch {\n return false;\n }\n}\n","import prompts from 'prompts';\nimport pc from 'picocolors';\nimport { writeConfig, readConfig, CONFIG_FILE } from '../lib/config.js';\nimport type { Style } from '../lib/registry.js';\nimport type { ReviewlicoConfig } from '../lib/config.js';\nimport { detectProject } from '../lib/detect.js';\n\nexport async function runInit(cwd: string): Promise<ReviewlicoConfig> {\n const existing = await readConfig(cwd);\n if (existing) {\n console.log(`${pc.green('✓')} Found existing ${CONFIG_FILE}`);\n return existing;\n }\n\n console.log(`\\n${pc.bold('reviewlico')} — let's get you set up\\n`);\n\n const detection = await detectProject(cwd);\n const frameworkLabel =\n detection.framework === 'next'\n ? 'Next.js'\n : detection.framework === 'vite'\n ? 'Vite'\n : detection.framework === 'cra'\n ? 'Create React App'\n : 'Unknown framework';\n console.log(\n pc.dim(`Detected ${frameworkLabel}; Tailwind: ${detection.hasTailwind ? 'yes' : 'no'}`),\n );\n\n const answers = await prompts(\n [\n {\n type: 'text',\n name: 'outputDir',\n message: 'Where should components be copied?',\n initial: 'src/components/reviews',\n validate: (v: string) => (v.trim().length > 0 ? true : 'Path cannot be empty'),\n },\n {\n type: 'select',\n name: 'style',\n message: 'Which styling variant do you want?',\n choices: [\n { title: 'Plain CSS (BEM classes, --rc-* tokens)', value: 'plain' },\n { title: 'Tailwind CSS (utility classes)', value: 'tailwind' },\n ],\n initial: detection.hasTailwind ? 1 : 0,\n },\n ],\n {\n onCancel: () => {\n console.log('\\nSetup cancelled.');\n process.exit(0);\n },\n },\n );\n\n const config: ReviewlicoConfig = {\n outputDir: (answers.outputDir as string).trim().replace(/\\/$/, ''),\n style: answers.style as Style,\n };\n\n await writeConfig(cwd, config);\n console.log(`\\n${pc.green('✓')} Created ${CONFIG_FILE}\\n`);\n\n return config;\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport type Framework = 'next' | 'vite' | 'cra' | 'unknown';\n\nexport interface ProjectDetection {\n framework: Framework;\n hasTailwind: boolean;\n}\n\ninterface PackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\nasync function readPackageJson(cwd: string): Promise<PackageJson | null> {\n try {\n const raw = await readFile(join(cwd, 'package.json'), 'utf-8');\n return JSON.parse(raw) as PackageJson;\n } catch {\n return null;\n }\n}\n\nfunction hasDependency(pkg: PackageJson | null, name: string): boolean {\n if (!pkg) return false;\n return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);\n}\n\nfunction detectFramework(pkg: PackageJson | null): Framework {\n if (hasDependency(pkg, 'next')) return 'next';\n if (hasDependency(pkg, 'vite') || hasDependency(pkg, '@vitejs/plugin-react')) return 'vite';\n if (hasDependency(pkg, 'react-scripts')) return 'cra';\n return 'unknown';\n}\n\nfunction detectTailwind(cwd: string, pkg: PackageJson | null): boolean {\n if (hasDependency(pkg, 'tailwindcss')) return true;\n const configFiles = [\n 'tailwind.config.js',\n 'tailwind.config.cjs',\n 'tailwind.config.mjs',\n 'tailwind.config.ts',\n ];\n return configFiles.some((file) => existsSync(join(cwd, file)));\n}\n\nexport async function detectProject(cwd: string): Promise<ProjectDetection> {\n const pkg = await readPackageJson(cwd);\n return {\n framework: detectFramework(pkg),\n hasTailwind: detectTailwind(cwd, pkg),\n };\n}\n","import pc from 'picocolors';\nimport { REGISTRY } from '../lib/registry.js';\n\nexport function runList() {\n console.log(`\\n${pc.bold('Available components:')}\\n`);\n for (const [name, entry] of Object.entries(REGISTRY)) {\n console.log(` ${pc.cyan(name)}`);\n console.log(` ${pc.dim(entry.description)}\\n`);\n }\n console.log(`Add a component: ${pc.bold('npx @reviewlico/cli add <component>')}`);\n console.log(`With Tailwind: ${pc.bold('npx @reviewlico/cli add <component> --styles tailwind')}\\n`);\n}\n"],"mappings":"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","names":["pc","pc","readFile","writeFile","join","join","readFile","writeFile","prompts","pc","existsSync","readFile","join","pc","prompts","pc","pc","pc","pc"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PublicReview } from '../../shared/types';
|
|
2
|
+
import { StarRating } from './StarRating';
|
|
3
|
+
|
|
4
|
+
function formatRelative(dateStr: string | null | undefined): string {
|
|
5
|
+
if (!dateStr) return '';
|
|
6
|
+
const timestamp = new Date(dateStr).getTime();
|
|
7
|
+
if (!Number.isFinite(timestamp)) return '';
|
|
8
|
+
const diff = Date.now() - timestamp;
|
|
9
|
+
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
|
10
|
+
const minutes = Math.round(diff / 60_000);
|
|
11
|
+
if (Math.abs(minutes) < 60) return rtf.format(-minutes, 'minute');
|
|
12
|
+
const hours = Math.round(diff / 3_600_000);
|
|
13
|
+
if (Math.abs(hours) < 24) return rtf.format(-hours, 'hour');
|
|
14
|
+
const days = Math.round(diff / 86_400_000);
|
|
15
|
+
if (Math.abs(days) < 30) return rtf.format(-days, 'day');
|
|
16
|
+
const months = Math.round(diff / 2_592_000_000);
|
|
17
|
+
if (Math.abs(months) < 12) return rtf.format(-months, 'month');
|
|
18
|
+
return rtf.format(-Math.round(diff / 31_536_000_000), 'year');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ReviewCardProps {
|
|
22
|
+
review: PublicReview;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ReviewCard({ review }: ReviewCardProps) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="rc-card rc-root">
|
|
28
|
+
<div className="rc-card__header">
|
|
29
|
+
<StarRating value={review.rating} size="sm" />
|
|
30
|
+
<span className="rc-card__date">{formatRelative(review.createdAt)}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<span className="rc-card__reviewer">{review.reviewerName}</span>
|
|
33
|
+
<p className="rc-card__text">{review.text}</p>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|