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 +9 -1
- package/dist/layro.mjs +286 -6
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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
|
|
2265
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
2003
2266
|
import {
|
|
2004
2267
|
basename,
|
|
2005
|
-
dirname as
|
|
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
|
|
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:
|
|
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
|
|
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,
|