@ravox-ui/cli 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @ravox-ui/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Add [Ravox Kit](https://ui.ravoxlabs.com/en/ui) components, blocks and themes to your project. Works with React, Vue, Svelte and Angular.
4
+
5
+ ```bash
6
+ npx @ravox-ui/cli init # set up the project (config, cn() helper and the Ravox theme)
7
+ npx @ravox-ui/cli add button dialog # add components
8
+ npx @ravox-ui/cli add dashboard-01 # add a block (a full screen, React)
9
+ npx @ravox-ui/cli add --all # add every component
10
+ npx @ravox-ui/cli list # see everything available
11
+ ```
12
+
13
+ Also with `pnpm dlx @ravox-ui/cli …`, `yarn dlx @ravox-ui/cli …` and `bunx @ravox-ui/cli …`.
14
+ Installed globally (`npm i -g @ravox-ui/cli`), the command is just `ravox`.
15
+
16
+ ## How it works
17
+
18
+ The code is copied into your project, so it is yours to edit: there is no runtime package to keep updated.
19
+ Under the hood it uses the [shadcn](https://ui.shadcn.com) CLI with the Ravox registry
20
+ (`https://ui.ravoxlabs.com/r`), so it plays well with other shadcn components in the same project.
21
+
22
+ - **Framework:** detected from `package.json` (`--framework react|vue|svelte|angular` to choose).
23
+ React gets the shadcn items; Vue, Svelte and Angular get their own ports, with the same API.
24
+ - **Package manager:** shadcn runs with the one you used to call it (npx, pnpm, yarn or bun).
25
+ - **Names:** `button`, `Button` or `button.json` all work, and typos get a suggestion.
26
+
27
+ ## Options
28
+
29
+ | Option | |
30
+ | --- | --- |
31
+ | `-f, --framework <name>` | `react`, `vue`, `svelte` or `angular` |
32
+ | `-c, --cwd <dir>` | project folder (default: current folder) |
33
+ | `-o, --overwrite` | overwrite existing files |
34
+ | `-y, --yes` | skip confirmation prompts |
35
+ | `--dry-run` | print the shadcn command, change nothing |
36
+
37
+ Messages are in Portuguese when your system language is Portuguese (`LANG=pt_BR…`).
38
+
39
+ ## Links
40
+
41
+ - Components and docs: https://ui.ravoxlabs.com/en/ui
42
+ - Blocks: https://ui.ravoxlabs.com/en/ui/blocks
43
+ - Use it from your AI agent (MCP): https://ui.ravoxlabs.com/mcp
44
+
45
+ MIT © Ravox Labs
package/bin/ravox.mjs ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/cli.mjs";
3
+
4
+ main(process.argv.slice(2)).then(
5
+ (code) => process.exit(code ?? 0),
6
+ (error) => {
7
+ console.error(error instanceof Error ? error.message : error);
8
+ process.exit(1);
9
+ },
10
+ );
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@ravox-ui/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Add Ravox Kit components, blocks and themes to your project (React, Vue, Svelte and Angular).",
5
+ "type": "module",
6
+ "bin": {
7
+ "ravox": "bin/ravox.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "engines": {
18
+ "node": ">=18.17"
19
+ },
20
+ "scripts": {
21
+ "test": "node --test test/"
22
+ },
23
+ "keywords": [
24
+ "ravox",
25
+ "ui",
26
+ "components",
27
+ "shadcn",
28
+ "react",
29
+ "vue",
30
+ "svelte",
31
+ "angular",
32
+ "tailwind",
33
+ "cli"
34
+ ],
35
+ "homepage": "https://ui.ravoxlabs.com/en/ui",
36
+ "license": "MIT",
37
+ "author": "Ravox Labs"
38
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,133 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { messages } from "./messages.mjs";
5
+ import { detectFramework, FRAMEWORKS, hasComponentsJson, readPackageJson, runnerFor } from "./project.mjs";
6
+ import { availableNames, fetchIndex, itemUrl, resolveNames, setupUrls } from "./registry.mjs";
7
+ import { suggest } from "./suggest.mjs";
8
+
9
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
10
+
11
+ /** Parses `command [names...] [--flags]`. */
12
+ export function parseArgs(argv) {
13
+ const opts = { command: null, names: [], framework: null, cwd: process.cwd(), overwrite: false, yes: false, all: false, dryRun: false, help: false, version: false };
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const a = argv[i];
16
+ const value = () => {
17
+ const v = argv[++i];
18
+ if (v === undefined) throw new Error(`${a} needs a value`);
19
+ return v;
20
+ };
21
+ if (a === "-h" || a === "--help") opts.help = true;
22
+ else if (a === "-v" || a === "--version") opts.version = true;
23
+ else if (a === "-f" || a === "--framework") opts.framework = value().toLowerCase();
24
+ else if (a.startsWith("--framework=")) opts.framework = a.slice(12).toLowerCase();
25
+ else if (a === "-c" || a === "--cwd") opts.cwd = resolve(value());
26
+ else if (a === "-o" || a === "--overwrite") opts.overwrite = true;
27
+ else if (a === "-y" || a === "--yes") opts.yes = true;
28
+ else if (a === "-a" || a === "--all") opts.all = true;
29
+ else if (a === "--dry-run") opts.dryRun = true;
30
+ else if (!opts.command) opts.command = a;
31
+ else opts.names.push(a);
32
+ }
33
+ return opts;
34
+ }
35
+
36
+ /** Runs shadcn with the user's package manager, streaming its output (prompts included). */
37
+ function run(cmd, opts, t) {
38
+ if (opts.dryRun) {
39
+ console.log(`${t.dryRun} ${cmd.join(" ")}`);
40
+ return Promise.resolve(0);
41
+ }
42
+ return new Promise((done) => {
43
+ const child = spawn(cmd[0], cmd.slice(1), { cwd: opts.cwd, stdio: "inherit", shell: process.platform === "win32" });
44
+ child.on("close", (code) => done(code ?? 1));
45
+ child.on("error", (e) => {
46
+ console.error(e.message);
47
+ done(1);
48
+ });
49
+ });
50
+ }
51
+
52
+ function shadcn(args, opts) {
53
+ const flags = [...(opts.yes ? ["--yes"] : []), ...(opts.overwrite && args[0] === "add" ? ["--overwrite"] : []), "--cwd", opts.cwd];
54
+ return [...runnerFor(process.env.npm_config_user_agent), "shadcn@latest", ...args, ...flags];
55
+ }
56
+
57
+ function frameworkOf(opts, t) {
58
+ const fw = opts.framework ?? detectFramework(readPackageJson(opts.cwd));
59
+ if (!fw) throw new Error(t.noFramework);
60
+ if (!FRAMEWORKS.includes(fw)) throw new Error(t.badFramework(fw));
61
+ return fw;
62
+ }
63
+
64
+ async function init(opts, t) {
65
+ const fw = frameworkOf(opts, t);
66
+ if (fw === "react") {
67
+ console.log(t.initReact);
68
+ // A project without components.json gets shadcn init (config + cn()) and the theme in one go.
69
+ const args = hasComponentsJson(opts.cwd) ? ["add", ...setupUrls(fw)] : ["init", ...setupUrls(fw)];
70
+ return run(shadcn(args, opts), opts, t);
71
+ }
72
+ console.log(t.initOther(fw));
73
+ const code = await run(shadcn(["add", ...setupUrls(fw)], opts, t), opts, t);
74
+ if (code === 0) console.log(t.importTheme);
75
+ return code;
76
+ }
77
+
78
+ async function add(opts, t) {
79
+ const fw = frameworkOf(opts, t);
80
+ const index = await fetchIndex();
81
+ const available = availableNames(index, fw);
82
+ const names = opts.all ? index.components.map((c) => c.name).filter((n) => available.includes(n)) : opts.names;
83
+ if (!names.length) throw new Error(t.nothingToAdd);
84
+
85
+ const { known, unknown } = resolveNames(names, available, index.aliases);
86
+ if (unknown.length) {
87
+ const blocks = index.blocks.map((b) => b.name);
88
+ for (const u of unknown) console.error(fw !== "react" && blocks.includes(u.toLowerCase()) ? t.blocksReactOnly : t.unknown(u, suggest(u, available)));
89
+ return 1;
90
+ }
91
+
92
+ if (fw === "react" && !hasComponentsJson(opts.cwd)) {
93
+ console.log(t.needInit);
94
+ const code = await init(opts, t);
95
+ if (code !== 0) return code;
96
+ }
97
+ console.log(t.adding(known.length, fw));
98
+ // Non-React items are universal (fixed targets), so they also bring cn() and the theme the first time.
99
+ const urls = [...(fw === "react" ? [] : setupUrls(fw)), ...known.map((n) => itemUrl(n, fw))];
100
+ const code = await run(shadcn(["add", ...urls], opts, t), opts, t);
101
+ if (code === 0) console.log(t.done);
102
+ return code;
103
+ }
104
+
105
+ async function list(opts, t) {
106
+ const index = await fetchIndex();
107
+ const fw = opts.framework ?? detectFramework(readPackageJson(opts.cwd)) ?? "react";
108
+ const width = Math.max(...index.components.map((c) => c.name.length)) + 2;
109
+ console.log(`\n${t.components}`);
110
+ for (const c of index.components) console.log(` ${c.name.padEnd(width)}${c.title}`);
111
+ console.log(`\n${t.blocks}`);
112
+ for (const b of index.blocks) console.log(` ${b.name.padEnd(width)}${b.title}`);
113
+ console.log(`\n${t.available(fw, availableNames(index, fw).filter((n) => !index.blocks.some((b) => b.name === n)).length)}`);
114
+ return 0;
115
+ }
116
+
117
+ export async function main(argv) {
118
+ const t = messages();
119
+ const opts = parseArgs(argv);
120
+ if (opts.version) {
121
+ console.log(VERSION);
122
+ return 0;
123
+ }
124
+ if (opts.help || !opts.command) {
125
+ console.log(t.help);
126
+ return 0;
127
+ }
128
+ if (opts.command === "init") return init(opts, t);
129
+ if (opts.command === "add") return add(opts, t);
130
+ if (opts.command === "list" || opts.command === "ls") return list(opts, t);
131
+ console.error(t.unknownCommand(opts.command));
132
+ return 1;
133
+ }
@@ -0,0 +1,83 @@
1
+ // Terminal messages in English, or Portuguese when the system language is Portuguese (LANG=pt_BR...).
2
+
3
+ const en = {
4
+ help: `ravox: add Ravox Kit components to your project (React, Vue, Svelte and Angular)
5
+
6
+ Run it with npx @ravox-ui/cli <command> (or npm i -g @ravox-ui/cli, then just ravox <command>)
7
+
8
+ Usage
9
+ ravox init set up the project (config, cn() helper and the Ravox theme)
10
+ ravox add <name...> add components or blocks (ravox add button dialog login-01)
11
+ ravox add --all add every component
12
+ ravox list list components and blocks
13
+
14
+ Options
15
+ -f, --framework <name> react, vue, svelte or angular (detected from package.json)
16
+ -c, --cwd <dir> project folder (default: current folder)
17
+ -o, --overwrite overwrite existing files
18
+ -y, --yes skip confirmation prompts
19
+ --dry-run show what would run, change nothing
20
+ -v, --version print the version
21
+
22
+ Docs: https://ui.ravoxlabs.com/en/ui`,
23
+ noFramework: "Could not tell the framework from package.json. Pass --framework react|vue|svelte|angular.",
24
+ badFramework: (f) => `Unknown framework "${f}". Use react, vue, svelte or angular.`,
25
+ nothingToAdd: "Tell me what to add: npx @ravox-ui/cli add button (or add --all). See them all with: npx @ravox-ui/cli list",
26
+ unknown: (name, hint) => `"${name}" is not in the Ravox Kit.${hint ? ` Did you mean "${hint}"?` : ""}`,
27
+ blocksReactOnly: "Blocks are React only for now.",
28
+ adding: (n, fw) => `Adding ${n} ${n === 1 ? "item" : "items"} to your ${fw} project…`,
29
+ initReact: "Setting up the project and the Ravox theme…",
30
+ initOther: (fw) => `Adding the cn() helper and the Ravox theme to your ${fw} project…`,
31
+ importTheme: 'Import the theme once in your main CSS, after Tailwind: @import "./ravox-tema.css";',
32
+ needInit: "No components.json here yet: running ravox init first.",
33
+ done: "Done.",
34
+ dryRun: "Would run:",
35
+ components: "Components",
36
+ blocks: "Blocks (React)",
37
+ available: (fw, n) => `${n} components available for ${fw}.`,
38
+ unknownCommand: (c) => `Unknown command "${c}". Run npx @ravox-ui/cli --help.`,
39
+ };
40
+
41
+ const pt = {
42
+ help: `ravox: adicione componentes do Ravox Kit ao seu projeto (React, Vue, Svelte e Angular)
43
+
44
+ Rode com npx @ravox-ui/cli <comando> (ou npm i -g @ravox-ui/cli e depois só ravox <comando>)
45
+
46
+ Uso
47
+ ravox init prepara o projeto (configuração, função cn() e o tema do Ravox)
48
+ ravox add <nome...> adiciona componentes ou blocos (ravox add button dialog login-01)
49
+ ravox add --all adiciona todos os componentes
50
+ ravox list lista componentes e blocos
51
+
52
+ Opções
53
+ -f, --framework <nome> react, vue, svelte ou angular (detectado pelo package.json)
54
+ -c, --cwd <pasta> pasta do projeto (padrão: a pasta atual)
55
+ -o, --overwrite sobrescreve arquivos que já existem
56
+ -y, --yes pula as confirmações
57
+ --dry-run mostra o que rodaria, sem mudar nada
58
+ -v, --version mostra a versão
59
+
60
+ Documentação: https://ui.ravoxlabs.com/ui`,
61
+ noFramework: "Não deu para saber o framework pelo package.json. Passe --framework react|vue|svelte|angular.",
62
+ badFramework: (f) => `Framework "${f}" desconhecido. Use react, vue, svelte ou angular.`,
63
+ nothingToAdd: "Diga o que adicionar: npx @ravox-ui/cli add button (ou add --all). Veja todos com: npx @ravox-ui/cli list",
64
+ unknown: (name, hint) => `"${name}" não existe no Ravox Kit.${hint ? ` Você quis dizer "${hint}"?` : ""}`,
65
+ blocksReactOnly: "Por enquanto os blocos são só para React.",
66
+ adding: (n, fw) => `Adicionando ${n} ${n === 1 ? "item" : "itens"} ao seu projeto ${fw}…`,
67
+ initReact: "Preparando o projeto e o tema do Ravox…",
68
+ initOther: (fw) => `Adicionando a função cn() e o tema do Ravox ao seu projeto ${fw}…`,
69
+ importTheme: 'Importe o tema uma vez no CSS principal, depois do Tailwind: @import "./ravox-tema.css";',
70
+ needInit: "Ainda não há components.json aqui: rodando o ravox init antes.",
71
+ done: "Pronto.",
72
+ dryRun: "Rodaria:",
73
+ components: "Componentes",
74
+ blocks: "Blocos (React)",
75
+ available: (fw, n) => `${n} componentes disponíveis para ${fw}.`,
76
+ unknownCommand: (c) => `Comando "${c}" desconhecido. Rode npx @ravox-ui/cli --help.`,
77
+ };
78
+
79
+ /** Portuguese when the system says so, English otherwise. */
80
+ export function messages(env = process.env) {
81
+ const lang = (env.RAVOX_LANG ?? env.LC_ALL ?? env.LC_MESSAGES ?? env.LANG ?? "").toLowerCase();
82
+ return lang.startsWith("pt") ? pt : en;
83
+ }
@@ -0,0 +1,44 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export const FRAMEWORKS = ["react", "vue", "svelte", "angular"];
5
+
6
+ /** Reads package.json from the project folder (null when there is none). */
7
+ export function readPackageJson(cwd) {
8
+ const file = join(cwd, "package.json");
9
+ if (!existsSync(file)) return null;
10
+ try {
11
+ return JSON.parse(readFileSync(file, "utf8"));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ /** Which framework the project uses, from its dependencies. Angular and Svelte win over Vue/React (they may ship both). */
18
+ export function detectFramework(pkg) {
19
+ if (!pkg) return null;
20
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
21
+ if (deps["@angular/core"]) return "angular";
22
+ if (deps.svelte || deps["@sveltejs/kit"]) return "svelte";
23
+ if (deps.vue || deps.nuxt) return "vue";
24
+ if (deps.react || deps.next) return "react";
25
+ return null;
26
+ }
27
+
28
+ /**
29
+ * How to run shadcn with the same package manager the user called us with (npx, pnpm dlx, yarn dlx, bunx).
30
+ * Yarn 1 has no `dlx`, so it falls back to npx.
31
+ */
32
+ export function runnerFor(userAgent = "") {
33
+ const ua = userAgent.toLowerCase();
34
+ if (ua.startsWith("pnpm/")) return ["pnpm", "dlx"];
35
+ if (ua.startsWith("bun/")) return ["bunx"];
36
+ if (ua.startsWith("yarn/")) {
37
+ const major = Number(ua.slice(5).split(".")[0]);
38
+ return major >= 2 ? ["yarn", "dlx"] : ["npx", "--yes"];
39
+ }
40
+ return ["npx", "--yes"];
41
+ }
42
+
43
+ /** A React project already set up for shadcn (has components.json). */
44
+ export const hasComponentsJson = (cwd) => existsSync(join(cwd, "components.json"));
@@ -0,0 +1,48 @@
1
+ // Where Ravox Kit items live. RAVOX_REGISTRY points somewhere else (e.g. http://localhost:3200) for local testing.
2
+ export const BASE = (process.env.RAVOX_REGISTRY ?? "https://ui.ravoxlabs.com").replace(/\/$/, "");
3
+
4
+ /** The index of everything that exists (components, blocks and which framework ports are ready). */
5
+ export async function fetchIndex(fetchImpl = fetch) {
6
+ let res;
7
+ try {
8
+ res = await fetchImpl(`${BASE}/r/ravox.json`);
9
+ } catch {
10
+ throw new Error(`Could not reach ${BASE}. Check your connection and try again.`);
11
+ }
12
+ if (!res.ok) throw new Error(`Could not read the Ravox index (${res.status} at ${BASE}/r/ravox.json).`);
13
+ return res.json();
14
+ }
15
+
16
+ /** Item URL for a framework: React uses the shadcn item, the others the universal items under r/<framework>/. */
17
+ export function itemUrl(name, framework) {
18
+ return framework === "react" ? `${BASE}/r/${name}.json` : `${BASE}/r/${framework}/${name}.json`;
19
+ }
20
+
21
+ /** The theme (and the cn() helper for non-React projects) that every component reads. */
22
+ export function setupUrls(framework) {
23
+ return framework === "react" ? [`${BASE}/r/vault-tema.json`] : [`${BASE}/r/${framework}/utils.json`, `${BASE}/r/${framework}/tema.json`];
24
+ }
25
+
26
+ /** Names available for a framework: every component, plus blocks (React only). */
27
+ export function availableNames(index, framework) {
28
+ if (framework === "react") return [...index.components.map((c) => c.name), ...index.blocks.map((b) => b.name)];
29
+ return index.frameworks?.[framework] ?? [];
30
+ }
31
+
32
+ /**
33
+ * Splits what the user asked for into known names and unknown ones. Accepts "Button", "button", "button.json" and
34
+ * the old Portuguese names (`aliases`, e.g. "botao" -> "button").
35
+ */
36
+ export function resolveNames(requested, available, aliases = {}) {
37
+ const known = [];
38
+ const unknown = [];
39
+ for (const raw of requested) {
40
+ const typed = raw.trim().toLowerCase().replace(/\.json$/, "").replace(/\s+/g, "-");
41
+ const name = aliases[typed] ?? typed;
42
+ if (!name) continue;
43
+ if (available.includes(name)) {
44
+ if (!known.includes(name)) known.push(name);
45
+ } else unknown.push(raw);
46
+ }
47
+ return { known, unknown };
48
+ }
@@ -0,0 +1,30 @@
1
+ /** Edit distance between two words (how many letters to add, remove or change). */
2
+ export function distance(a, b) {
3
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
4
+ for (let i = 1; i <= a.length; i++) {
5
+ let prev = row[0];
6
+ row[0] = i;
7
+ for (let j = 1; j <= b.length; j++) {
8
+ const temp = row[j];
9
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
10
+ prev = temp;
11
+ }
12
+ }
13
+ return row[b.length];
14
+ }
15
+
16
+ /** The closest available name, when it is close enough to be a typo (or a prefix like "dropdown"). */
17
+ export function suggest(name, available) {
18
+ const word = name.toLowerCase();
19
+ const prefix = available.find((a) => a.startsWith(word) || a.split("-").includes(word));
20
+ if (prefix) return prefix;
21
+ let best = null;
22
+ let bestDistance = Infinity;
23
+ for (const a of available) {
24
+ // Also compare with the start of longer names ("dropdwn" is close to "dropdown-menu").
25
+ const start = Math.min(distance(word, a.slice(0, word.length)), distance(word, a.slice(0, word.length + 1)));
26
+ const d = Math.min(distance(word, a), start + 1);
27
+ if (d < bestDistance) [best, bestDistance] = [a, d];
28
+ }
29
+ return bestDistance <= Math.max(2, Math.floor(word.length / 3)) ? best : null;
30
+ }