Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/landing/src/components/landing/Activation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ import { exampleCaplets } from "../../data/landing";
---

<section class="mx-auto w-[min(1180px,calc(100vw_-_32px))] scroll-mt-20 py-20" id="install" aria-labelledby="activation-title" data-reveal>
<div class="grid gap-8 lg:grid-cols-[0.68fr_1.32fr] lg:items-start [&>*]:min-w-0">
<div class="grid gap-8 lg:grid-cols-[0.62fr_1.38fr] lg:items-start [&>*]:min-w-0">
<div class="min-w-0 max-w-xl">
<p class="text-sm font-medium text-muted-foreground">Setup</p>
<p class="text-sm font-medium text-muted-foreground">First Caplet</p>
<h2 id="activation-title" class="mt-3 text-3xl leading-tight font-semibold tracking-[-0.02em] text-balance md:text-5xl">
Start with the smallest useful Caplet.
Start where auth cannot get in the way.
</h2>
<p class="mt-5 text-lg leading-8 text-pretty text-muted-foreground">
<code class="rounded bg-muted px-1.5 py-0.5 font-mono text-sm text-foreground">caplets setup</code> wires the agent integrations you choose. Add OSV first because it needs no auth; bring in GitHub or Sourcegraph after the discovery path feels right.
Copy setup from the hero, then add OSV first because it needs no auth. Bring in GitHub or Sourcegraph after the discovery path feels right.
</p>

<Button class="mt-8" href="https://catalog.caplets.dev" variant="outline">
Explore more Caplets
<ArrowUpRight class="size-4" aria-hidden="true" />
Expand Down
4 changes: 2 additions & 2 deletions apps/landing/src/components/landing/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/co
<nav class="site-header__primary-nav hidden items-center gap-1 text-sm font-medium text-muted-foreground lg:flex" aria-label="Landing page sections">
<a class="site-header__section-link inline-flex min-h-11 items-center rounded-md px-3 py-2 transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#trace">How</a>
<a class="site-header__section-link inline-flex min-h-11 items-center rounded-md px-3 py-2 transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#why">Why</a>
<a class="site-header__section-link inline-flex min-h-11 items-center rounded-md px-3 py-2 transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#install">Setup</a>
<a class="site-header__section-link inline-flex min-h-11 items-center rounded-md px-3 py-2 transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#setup">Setup</a>
<a class="site-header__section-link inline-flex min-h-11 items-center rounded-md px-3 py-2 transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#proof">Benchmark</a>
<a class="site-header__section-link inline-flex min-h-11 items-center rounded-md px-3 py-2 transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="/blog/">Blog</a>
<a class="site-header__section-link inline-flex min-h-11 items-center rounded-md px-3 py-2 transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#remote">Remote</a>
Expand Down Expand Up @@ -61,7 +61,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/co
<nav class="grid gap-1 p-1 text-sm font-medium" aria-label="Landing page sections">
<a class="starwind-dialog-close flex min-h-11 items-center rounded-md px-3 py-2.5 text-muted-foreground transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#trace">How it works</a>
<a class="starwind-dialog-close flex min-h-11 items-center rounded-md px-3 py-2.5 text-muted-foreground transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#why">Why Caplets</a>
<a class="starwind-dialog-close flex min-h-11 items-center rounded-md px-3 py-2.5 text-muted-foreground transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#install">Setup</a>
<a class="starwind-dialog-close flex min-h-11 items-center rounded-md px-3 py-2.5 text-muted-foreground transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#setup">Setup</a>
<a class="starwind-dialog-close flex min-h-11 items-center rounded-md px-3 py-2.5 text-muted-foreground transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#proof">Benchmark</a>
<a class="starwind-dialog-close flex min-h-11 items-center rounded-md px-3 py-2.5 text-muted-foreground transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="/blog/">Blog</a>
<a class="starwind-dialog-close flex min-h-11 items-center rounded-md px-3 py-2.5 text-muted-foreground transition hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none" href="#remote">Remote server</a>
Expand Down
96 changes: 50 additions & 46 deletions apps/landing/src/components/landing/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,26 @@ import Check from "@tabler/icons/outline/check.svg";
import Copy from "@tabler/icons/outline/copy.svg";

import { Button } from "@/components/starwind/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/starwind/tabs";

import { heroCommands } from "../../data/landing";
import { agentSetupPrompt, manualSetupCommand, manualSetupCommands } from "../../data/landing";

const primaryHeroCommand = heroCommands[0];
const setupOptions = [
{
label: "Manual",
copyLabel: "manual setup commands",
copyValue: manualSetupCommand,
copyAttribution: true,
lines: [manualSetupCommands.install, manualSetupCommands.setup],
},
{
label: "Agent",
copyLabel: "agent setup prompt",
copyValue: agentSetupPrompt,
copyAttribution: false,
lines: ["Read bootstrap skill", "detect -> ask -> plan -> approve"],
},
] as const;
---

<section class="landing-hero relative isolate w-full scroll-mt-20 overflow-hidden border-y border-border/70 py-6 md:py-8 lg:py-10" id="trace" aria-labelledby="hero-title">
Expand All @@ -20,55 +36,43 @@ const primaryHeroCommand = heroCommands[0];
<p class="landing-hero__summary mt-5 max-w-2xl text-lg leading-8 text-pretty text-[oklch(88%_0.026_82)]">
Turn sprawling MCP servers into focused capability cards. Your agent can start with one typed route, zoom into tools only when needed, and keep huge schemas out of the prompt until they matter.
</p>
<div class="landing-hero__route mt-5 max-w-2xl rounded-lg border border-border bg-card/92 p-4 text-sm leading-6 text-foreground md:mt-6" aria-label="Caplets capability route example">
<p class="text-muted-foreground">First route the agent sees</p>
<p class="landing-hero__route-copy mt-2 text-foreground">One card opens into inspect, search, schema, and call only when needed.</p>
<div class="landing-hero__route-path mt-3 flex flex-wrap items-center gap-2 font-mono text-xs text-foreground">
<code class="rounded bg-muted px-2 py-1">osv</code>
<span class="text-muted-foreground">/</span>
<code class="rounded bg-muted px-2 py-1">inspect</code>
<span class="text-muted-foreground">/</span>
<code class="rounded bg-muted px-2 py-1">search_tools</code>
<span class="text-muted-foreground">/</span>
<code class="rounded bg-muted px-2 py-1">get_tool</code>
<span class="text-muted-foreground">/</span>
<code class="rounded bg-muted px-2 py-1">call_tool</code>
</div>
</div>
<div class="landing-hero__actions mt-6 flex flex-wrap gap-3 md:mt-8" aria-label="Primary actions">
<Button href="#install" variant="primary">Install & run setup</Button>
<Button href="https://catalog.caplets.dev" variant="outline">Browse catalog</Button>
<Button href="https://docs.caplets.dev" variant="outline">Read the docs</Button>
</div>
<div class="landing-hero__quickstart mt-4 max-w-2xl md:mt-6" id="quickstart">
<div class="landing-hero__quickstart-card rounded-lg border border-border bg-card p-3 text-card-foreground" aria-label="Quick install command">
<div class="flex items-center justify-between gap-3">
{
primaryHeroCommand && (
<>
<code class="min-w-0 overflow-x-auto font-mono text-sm text-muted-foreground">
{primaryHeroCommand.command}
</code>

<Tabs id="setup" defaultValue="Manual" class="landing-hero__setup mt-6 max-w-2xl" aria-label="Caplets setup options">
<TabsList class="border-[oklch(86%_0.024_82_/_0.24)] bg-[oklch(96%_0.018_82_/_0.08)]">
{setupOptions.map((option) => <TabsTrigger value={option.label}>{option.label}</TabsTrigger>)}
</TabsList>
{
setupOptions.map((option) => (
<TabsContent value={option.label} class="mt-3">
<div class="rounded-lg border border-[oklch(86%_0.024_82_/_0.28)] bg-[oklch(10%_0.014_100_/_0.72)] p-3 text-[oklch(94%_0.018_82)]">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="grid gap-1 font-mono text-sm leading-6">
{option.lines.map((line) => <code class="break-words">{line}</code>)}
</div>
</div>
<button
class="group inline-flex min-h-11 shrink-0 items-center gap-2 rounded-md border border-border px-3 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:ring-3 focus-visible:ring-outline/50 focus-visible:outline-none"
class="group inline-flex min-h-10 shrink-0 items-center justify-center gap-2 rounded-md border border-[oklch(84%_0.024_82_/_0.34)] px-3 text-sm font-medium text-[oklch(95%_0.018_82)] transition hover:bg-[oklch(96%_0.018_82_/_0.12)] focus-visible:ring-3 focus-visible:ring-[oklch(66%_0.14_38_/_0.45)] focus-visible:outline-none"
type="button"
aria-label="Copy install command"
data-copy-value={primaryHeroCommand.command}
data-copy-label="install command"
aria-label={`Copy ${option.copyLabel}`}
data-copy-value={option.copyValue}
data-copy-label={option.copyLabel}
data-copy-attribution={String(option.copyAttribution)}
>
<Copy class="size-4 group-data-[copied=true]:hidden" aria-hidden="true" />
<Check
class="hidden size-4 text-success group-data-[copied=true]:block"
aria-hidden="true"
/>
Copy
<Check class="hidden size-4 text-success group-data-[copied=true]:block" aria-hidden="true" />
<span>Copy</span>
</button>
</>
)
}
</div>
<p class="landing-hero__quickstart-note mt-2 text-sm leading-6 text-muted-foreground">Requires Node 24+. Run setup from the install section next.</p>
</div>
</div>
</div>
</TabsContent>
))
}
</Tabs>

<div class="landing-hero__actions mt-6 flex flex-wrap gap-3 md:mt-8" aria-label="Primary actions">
<Button href="https://catalog.caplets.dev" variant="primary">Browse catalog</Button>
<Button href="https://docs.caplets.dev" variant="outline">Read the docs</Button>
</div>
</div>
</div>
Expand Down
11 changes: 11 additions & 0 deletions apps/landing/src/data/landing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export const heroCommands = [

export const quickstartCommand = heroCommands.map((item) => item.command).join("\n");

export const manualSetupCommands = {
install: "npm install -g caplets",
setup: "caplets setup",
} as const;

export const manualSetupCommand = Object.values(manualSetupCommands).join("\n");

export const agentSetupPrompt = `Read and follow this Caplets bootstrap skill: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/skills/installing-caplets/SKILL.md

Set up Caplets for this environment. Detect the environment first. Do not install packages, modify config, start remote login, or write files until you have asked me the setup questions, shown the exact commands and files/config areas you plan to change, and I approve that plan.`;

export const proofStats = [
{
value: "10/10",
Expand Down
3 changes: 2 additions & 1 deletion apps/landing/src/scripts/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ async function copyValue(button: HTMLButtonElement) {
? mobileValue
: button.dataset.copyValue;
if (!value) return;
const copyValue = attributedLandingCommand(value);
const copyValue =
button.dataset.copyAttribution === "false" ? value : attributedLandingCommand(value);

if (!navigator.clipboard?.writeText) {
setCopyFeedback(button, "Copy unavailable", 2200);
Expand Down
22 changes: 22 additions & 0 deletions apps/landing/test/activation-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,26 @@ describe("activation links", () => {
'href="https://github.com/spiritledsoftware/caplets/tree/main/caplets"',
);
});

it("offers manual and agent setup modes without rendering the full agent prompt", () => {
const componentSource = readFileSync(
join(repoRoot, "apps/landing/src/components/landing/Hero.astro"),
"utf8",
);
const dataSource = readFileSync(join(repoRoot, "apps/landing/src/data/landing.ts"), "utf8");

expect(componentSource).toContain('id="setup"');
expect(componentSource).toContain("<TabsList");
expect(componentSource).toContain("<TabsTrigger");
expect(componentSource).toContain('label: "Manual"');
expect(componentSource).toContain('label: "Agent"');
expect(componentSource).toContain('copyLabel: "agent setup prompt"');
expect(componentSource).toContain("data-copy-attribution={String(option.copyAttribution)}");
expect(dataSource).toContain("agentSetupPrompt");
expect(dataSource).toContain(
"https://raw.githubusercontent.com/spiritledsoftware/caplets/main/skills/installing-caplets/SKILL.md",
);
expect(componentSource).not.toContain("Read and follow this Caplets bootstrap skill");
expect(componentSource).not.toContain("First route the agent sees");
});
});
13 changes: 13 additions & 0 deletions apps/landing/test/observability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ describe("landing observability", () => {
);
});

it("can copy raw prompt text without command attribution", async () => {
document.body.innerHTML = `
<button data-copy-value="Read this setup skill" data-copy-label="setup prompt" data-copy-attribution="false"></button>
<div data-copy-status></div>
`;

await import("../src/scripts/copy");
document.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await Promise.resolve();

expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Read this setup skill");
});

it("loads without initializing providers when env is absent", async () => {
await expect(import("../src/scripts/observability")).resolves.toBeDefined();
});
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2641,18 +2641,20 @@ export function createProgram(io: CliIO = {}): Command {
.command(cliCommands.doctor)
.description("Diagnose Caplets local, remote, and project-sync configuration.")
.option("--json", "print JSON output")
.action(async (options: { json?: boolean }) => {
.option("--format <format>", "output format: plain, markdown, md, or json", parseOutputFormat)
.action(async (options: { json?: boolean; format?: CliOutputFormat }) => {
const doctorOptions = {
env,
...(io.fetch ? { fetch: io.fetch } : {}),
...(io.authDir ? { authDir: io.authDir } : {}),
...(io.daemon ? { daemon: io.daemon } : {}),
};
if (options.json) {
const format = options.format ?? (options.json ? "json" : "plain");
if (format === "json") {
writeOut(`${JSON.stringify(await doctorJsonReport(doctorOptions), null, 2)}\n`);
return;
}
writeOut(await formatDoctorReport(doctorOptions));
writeOut(await formatDoctorReport(doctorOptions, format));
});

const vault = program.command(cliCommands.vault).description("Manage Caplets Vault values.");
Expand Down
Loading