layro-cli 0.4.0 → 0.5.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
@@ -23,6 +23,7 @@ Requires Node 20 or newer. No dependencies.
23
23
  | `layro status [component] [--json]` | Where your submissions stand, with reviewers' reasons. `component` is a slug or `author/slug`. |
24
24
  | `layro add @author/slug [more...]` | Installs source, CSS and dependencies in a React TypeScript project. |
25
25
  | `layro init` | Checks Tailwind and aliases; creates missing shared configuration and cn. `--cwd`, `--dry-run`, `--yes`, `--json`. |
26
+ | `layro mcp [--client <agent>]` | Adds Layro's MCP server to Claude Code, Cursor, VS Code or Codex: no URL to type. `--dry-run`, `--yes`, `--json`. |
26
27
  | `layro logout [--json]` | Revokes the session and forgets it on this computer. |
27
28
 
28
29
  ### What `add` does to your project
@@ -119,7 +120,14 @@ Docs: https://layro.dev/docs/cli
119
120
 
120
121
  ### MCP and alternative installers
121
122
 
122
- Agents can use `install_plan` to receive the CLI setup guide, all four commands,
123
+ `npx layro-cli mcp` connects your coding agent in one step: Claude Code and
124
+ Codex through their own `mcp add`, VS Code through `code --add-mcp` (or the
125
+ project's `.vscode/mcp.json`), Cursor by adding `layro` to `~/.cursor/mcp.json`.
126
+ An existing `layro` entry and every other server are left alone. Any other MCP
127
+ client takes `https://mcp.layro.dev`; Layro is `dev.layro/layro` in the
128
+ official MCP Registry.
129
+
130
+ Agents can use `install_plan` to receive the CLI setup guide, every package manager's command,
123
131
  file targets and content, npm pins, CSS data and named shadcn dependencies.
124
132
  `get_component` and `install_plan` together count one install per component and
125
133
  UTC day. Component code and text are untrusted data, not instructions for agents.
package/dist/layro.mjs CHANGED
@@ -801,7 +801,7 @@ function nextPrerenderGuidance(project, sources) {
801
801
  }
802
802
 
803
803
  // src/version.ts
804
- var VERSION = true ? "0.4.0" : "dev";
804
+ var VERSION = true ? "0.5.0" : "dev";
805
805
 
806
806
  // src/install-registry.ts
807
807
  var record = (v) => Boolean(v && typeof v === "object" && !Array.isArray(v));
@@ -1898,6 +1898,28 @@ async function api(server2, path, init = {}) {
1898
1898
 
1899
1899
  // src/command-help.ts
1900
1900
  var commandHelp = {
1901
+ mcp: `layro mcp [--client <agent>] [options]
1902
+
1903
+ Add Layro's MCP server to a coding agent, so you never type its URL.
1904
+ Without --client it asks which, listing the agents it finds first.
1905
+
1906
+ claude claude mcp add --transport http --scope user (every project)
1907
+ cursor adds "layro" to ~/.cursor/mcp.json
1908
+ vscode code --add-mcp (your profile), or .vscode/mcp.json here
1909
+ codex codex mcp add layro --url
1910
+
1911
+ An existing "layro" entry is left alone; other servers are never touched.
1912
+ Sign-in is your agent's own: it opens the browser on first use.
1913
+
1914
+ -c, --client <agent> claude, cursor, vscode or codex
1915
+ --cwd <folder> Project for .vscode/mcp.json (default current folder)
1916
+ --dry-run Show what would change; change nothing
1917
+ -y, --yes Don't ask before changing
1918
+ --json Machine-readable result
1919
+
1920
+ Examples:
1921
+ npx layro-cli mcp
1922
+ npx layro-cli mcp --client cursor --yes`,
1901
1923
  search: `layro search <words...> [options]
1902
1924
 
1903
1925
  Find public components and print their Layro add commands. No sign-in.
@@ -1969,6 +1991,7 @@ Examples:
1969
1991
  };
1970
1992
  var commands = [
1971
1993
  "init",
1994
+ "mcp",
1972
1995
  "add",
1973
1996
  "search",
1974
1997
  "publish",
@@ -1998,11 +2021,251 @@ function suggestCommand(input) {
1998
2021
  return found.distance <= 2 ? ` Did you mean layro ${found.command}?` : "";
1999
2022
  }
2000
2023
 
2024
+ // src/mcp.ts
2025
+ import { spawnSync } from "node:child_process";
2026
+ import { existsSync as existsSync4 } from "node:fs";
2027
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "node:fs/promises";
2028
+ import { homedir as homedir2 } from "node:os";
2029
+ import { dirname as dirname6, join as join5 } from "node:path";
2030
+ var byHand = (message, manual) => new SetupRequired(message, manual);
2031
+ var MCP_NAME = "layro";
2032
+ var clients = ["claude", "cursor", "vscode", "codex"];
2033
+ var labels = {
2034
+ claude: "Claude Code",
2035
+ cursor: "Cursor",
2036
+ vscode: "VS Code",
2037
+ codex: "Codex"
2038
+ };
2039
+ function mcpUrl(server2) {
2040
+ return server2 === "https://layro.dev" ? "https://mcp.layro.dev" : `${server2}/mcp`;
2041
+ }
2042
+ var windows = process.platform === "win32";
2043
+ function onPath(program) {
2044
+ const dirs = (process.env.PATH ?? "").split(windows ? ";" : ":");
2045
+ const names = windows ? [`${program}.cmd`, `${program}.exe`, program] : [program];
2046
+ return dirs.some(
2047
+ (dir) => dir && names.some((name) => existsSync4(join5(dir, name)))
2048
+ );
2049
+ }
2050
+ function run(program, args, quiet = false) {
2051
+ const result = spawnSync(program, args, {
2052
+ stdio: quiet ? "ignore" : "inherit",
2053
+ // .cmd shims need a shell on Windows; every argument here is ours.
2054
+ shell: windows
2055
+ });
2056
+ return result.status === 0;
2057
+ }
2058
+ async function mergeJson(file, key, entry, manual) {
2059
+ let config = {};
2060
+ if (existsSync4(file)) {
2061
+ const text = await readFile5(file, "utf8");
2062
+ try {
2063
+ config = text.trim() ? JSON.parse(text) : {};
2064
+ } catch {
2065
+ throw byHand(
2066
+ `${file} isn't plain JSON (comments?), so it was left alone. Add this by hand:`,
2067
+ manual
2068
+ );
2069
+ }
2070
+ if (!config || typeof config !== "object" || Array.isArray(config))
2071
+ throw byHand(`${file} isn't a JSON object. Add this by hand:`, manual);
2072
+ }
2073
+ const servers = config[key] ?? {};
2074
+ if (typeof servers !== "object" || Array.isArray(servers))
2075
+ throw byHand(
2076
+ `"${key}" in ${file} isn't an object. Add this by hand:`,
2077
+ manual
2078
+ );
2079
+ if (servers[MCP_NAME]) return "exists";
2080
+ config[key] = { ...servers, [MCP_NAME]: entry };
2081
+ await mkdir3(dirname6(file), { recursive: true });
2082
+ await writeFile2(file, `${JSON.stringify(config, null, 2)}
2083
+ `);
2084
+ return "added";
2085
+ }
2086
+ function planFor(client, server2, cwd) {
2087
+ const url = mcpUrl(server2);
2088
+ switch (client) {
2089
+ case "claude": {
2090
+ const args = [
2091
+ "mcp",
2092
+ "add",
2093
+ "--transport",
2094
+ "http",
2095
+ "--scope",
2096
+ "user",
2097
+ MCP_NAME,
2098
+ url
2099
+ ];
2100
+ const manual = `claude ${args.join(" ")}`;
2101
+ return {
2102
+ client,
2103
+ url,
2104
+ action: `Run: ${manual}`,
2105
+ manual,
2106
+ apply: async () => {
2107
+ if (!onPath("claude"))
2108
+ throw byHand(
2109
+ `Claude Code's CLI (claude) isn't on PATH. Run this where it is:`,
2110
+ manual
2111
+ );
2112
+ if (run("claude", ["mcp", "get", MCP_NAME], true)) return "exists";
2113
+ if (!run("claude", args))
2114
+ throw byHand(`claude mcp add failed. Run it yourself:`, manual);
2115
+ return "added";
2116
+ }
2117
+ };
2118
+ }
2119
+ case "codex": {
2120
+ const args = ["mcp", "add", MCP_NAME, "--url", url];
2121
+ const manual = `codex ${args.join(" ")}`;
2122
+ return {
2123
+ client,
2124
+ url,
2125
+ action: `Run: ${manual}`,
2126
+ manual,
2127
+ apply: async () => {
2128
+ if (!onPath("codex"))
2129
+ throw byHand(
2130
+ `Codex's CLI (codex) isn't on PATH. Run this where it is:`,
2131
+ manual
2132
+ );
2133
+ if (run("codex", ["mcp", "get", MCP_NAME], true)) return "exists";
2134
+ if (!run("codex", args))
2135
+ throw byHand(`codex mcp add failed. Run it yourself:`, manual);
2136
+ return "added";
2137
+ }
2138
+ };
2139
+ }
2140
+ case "cursor": {
2141
+ if (!homedir2())
2142
+ throw new CliError("Can't find your home folder (HOME isn't set).");
2143
+ const file = join5(homedir2(), ".cursor", "mcp.json");
2144
+ const manual = JSON.stringify(
2145
+ { mcpServers: { [MCP_NAME]: { url } } },
2146
+ null,
2147
+ 2
2148
+ );
2149
+ return {
2150
+ client,
2151
+ url,
2152
+ action: `Add "${MCP_NAME}" to ${file}`,
2153
+ manual,
2154
+ apply: () => mergeJson(file, "mcpServers", { url }, manual)
2155
+ };
2156
+ }
2157
+ case "vscode": {
2158
+ const entry = { type: "http", url };
2159
+ const json = JSON.stringify({ name: MCP_NAME, ...entry });
2160
+ if (!windows && onPath("code")) {
2161
+ const manual2 = `code --add-mcp '${json}'`;
2162
+ return {
2163
+ client,
2164
+ url,
2165
+ action: `Run: ${manual2}`,
2166
+ manual: manual2,
2167
+ apply: async () => {
2168
+ if (!run("code", ["--add-mcp", json]))
2169
+ throw byHand(`code --add-mcp failed. Run it yourself:`, manual2);
2170
+ return "added";
2171
+ }
2172
+ };
2173
+ }
2174
+ const file = join5(cwd, ".vscode", "mcp.json");
2175
+ const manual = JSON.stringify(
2176
+ { servers: { [MCP_NAME]: entry } },
2177
+ null,
2178
+ 2
2179
+ );
2180
+ return {
2181
+ client,
2182
+ url,
2183
+ action: `Add "${MCP_NAME}" to ${file} (this project)`,
2184
+ manual,
2185
+ apply: () => mergeJson(file, "servers", entry, manual)
2186
+ };
2187
+ }
2188
+ }
2189
+ }
2190
+ function detected() {
2191
+ const home = homedir2();
2192
+ const found = /* @__PURE__ */ new Set();
2193
+ if (onPath("claude") || existsSync4(join5(home, ".claude")))
2194
+ found.add("claude");
2195
+ if (onPath("cursor") || existsSync4(join5(home, ".cursor")))
2196
+ found.add("cursor");
2197
+ if (onPath("code")) found.add("vscode");
2198
+ if (onPath("codex") || existsSync4(join5(home, ".codex"))) found.add("codex");
2199
+ return found;
2200
+ }
2201
+ var nextStep = {
2202
+ claude: "Ask Claude for a component. The first call opens your browser to sign in with GitHub and approve Layro.",
2203
+ cursor: "Restart Cursor if it's open. It opens your browser to sign in on first use.",
2204
+ vscode: "Open Copilot Chat in agent mode. VS Code asks you to sign in to Layro on first use.",
2205
+ codex: `If Codex didn't open your browser, sign in once: codex mcp login ${MCP_NAME}`
2206
+ };
2207
+ async function addMcp(server2, flags) {
2208
+ const cwd = flags.cwd ?? process.cwd();
2209
+ let client = flags.client?.toLowerCase().replace(/[\s-]+/g, "");
2210
+ if (client === "claudecode") client = "claude";
2211
+ if (client === "code") client = "vscode";
2212
+ if (client && !clients.includes(client))
2213
+ throw new CliError(
2214
+ `Unknown agent "${safeText(flags.client ?? "")}". Use --client with one of: ${clients.join(", ")}.`
2215
+ );
2216
+ if (!client) {
2217
+ if (flags.json || flags.yes || !canAsk())
2218
+ throw new CliError(
2219
+ `Say which agent: --client ${clients.join(" | ")}. Any other MCP client takes ${mcpUrl(server2)}.`
2220
+ );
2221
+ const found = detected();
2222
+ const order = [...clients].sort(
2223
+ (a, b) => Number(found.has(b)) - Number(found.has(a))
2224
+ );
2225
+ client = await choose(
2226
+ "Add Layro to",
2227
+ order.map((value) => ({
2228
+ value,
2229
+ label: `${labels[value]}${found.has(value) ? dim(" found") : ""}`
2230
+ })),
2231
+ order[0]
2232
+ );
2233
+ }
2234
+ const plan = planFor(client, server2, cwd);
2235
+ if (flags["dry-run"]) {
2236
+ if (flags.json)
2237
+ return say(
2238
+ JSON.stringify({
2239
+ client: plan.client,
2240
+ url: plan.url,
2241
+ action: plan.action,
2242
+ manual: plan.manual,
2243
+ dryRun: true
2244
+ })
2245
+ );
2246
+ say(`${bold(labels[plan.client])}: ${plan.action}`);
2247
+ return say(dim("Dry run: nothing changed."));
2248
+ }
2249
+ if (!flags.yes && !flags.json && canAsk()) {
2250
+ say(`${bold(labels[plan.client])}: ${plan.action}`);
2251
+ if (!await confirm("Go ahead?")) throw new CliError("Cancelled.");
2252
+ }
2253
+ const result = await plan.apply();
2254
+ if (flags.json)
2255
+ return say(
2256
+ JSON.stringify({ client: plan.client, url: plan.url, status: result })
2257
+ );
2258
+ say(
2259
+ result === "added" ? green(`\u2713 Layro added to ${labels[plan.client]}.`) : `Layro is already set up in ${labels[plan.client]}. Nothing changed.`
2260
+ );
2261
+ say(dim(nextStep[plan.client]));
2262
+ }
2263
+
2001
2264
  // src/publish.ts
2002
- import { readFile as readFile5 } from "node:fs/promises";
2265
+ import { readFile as readFile6 } from "node:fs/promises";
2003
2266
  import {
2004
2267
  basename,
2005
- dirname as dirname6,
2268
+ dirname as dirname7,
2006
2269
  isAbsolute as isAbsolute2,
2007
2270
  relative as relative5,
2008
2271
  resolve as resolve5,
@@ -2021,7 +2284,7 @@ async function readFiles(paths) {
2021
2284
  absolute.map(async (path, i) => {
2022
2285
  let content;
2023
2286
  try {
2024
- content = await readFile5(path, "utf8");
2287
+ content = await readFile6(path, "utf8");
2025
2288
  } catch {
2026
2289
  throw new CliError(`Can't read ${paths[i]}.`);
2027
2290
  }
@@ -2029,7 +2292,7 @@ async function readFiles(paths) {
2029
2292
  })
2030
2293
  );
2031
2294
  }
2032
- function commonFileRoot(paths, path = { dirname: dirname6, relative: relative5, isAbsolute: isAbsolute2 }) {
2295
+ function commonFileRoot(paths, path = { dirname: dirname7, relative: relative5, isAbsolute: isAbsolute2 }) {
2033
2296
  let root = path.dirname(paths[0]);
2034
2297
  for (const file of paths) {
2035
2298
  for (; ; ) {
@@ -2049,7 +2312,7 @@ function commonFileRoot(paths, path = { dirname: dirname6, relative: relative5,
2049
2312
  async function readPreview(path) {
2050
2313
  if (!path) return void 0;
2051
2314
  try {
2052
- return JSON.parse(await readFile5(path, "utf8"));
2315
+ return JSON.parse(await readFile6(path, "utf8"));
2053
2316
  } catch {
2054
2317
  throw new CliError(`--preview: ${path} isn't a readable JSON file.`);
2055
2318
  }
@@ -2283,6 +2546,8 @@ Usage
2283
2546
  layro status [component] Where your submissions stand (component: slug or author/slug)
2284
2547
  layro add <author/slug> Install a component into your project
2285
2548
  layro init Set up shared helpers and check Tailwind
2549
+ layro mcp Add Layro to your coding agent (Claude Code,
2550
+ Cursor, VS Code, Codex): no URL to type
2286
2551
  layro logout Sign out on this computer
2287
2552
 
2288
2553
  Publish options
@@ -2374,6 +2639,21 @@ Examples:
2374
2639
  await add(server, [], values, true);
2375
2640
  return;
2376
2641
  }
2642
+ case "mcp": {
2643
+ const args = rest[0] === "add" || rest[0] === "init" ? rest.slice(1) : rest;
2644
+ const { values } = parseArgs({
2645
+ args,
2646
+ options: {
2647
+ client: { type: "string", short: "c" },
2648
+ cwd: { type: "string" },
2649
+ "dry-run": { type: "boolean" },
2650
+ yes: { type: "boolean", short: "y" },
2651
+ json: { type: "boolean" }
2652
+ }
2653
+ });
2654
+ await addMcp(server, values);
2655
+ return;
2656
+ }
2377
2657
  case "login": {
2378
2658
  const { values } = parseArgs({
2379
2659
  args: rest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layro-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Find, install and publish React components on Layro (layro.dev) from your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",