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
84 changes: 84 additions & 0 deletions apps/logicsrc-web/contract/install-command.contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// The install command is the site's single most copy-pasted string, and it is
// rendered in two places by two different mechanisms -- the homepage builds
// HTML as a string, the rest of the site is JSX. That is exactly the shape that
// lets one copy drift while the other stays right, so these pin the command
// itself, both placements, and the flags that make piping to `sh` safe.
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";

import {
INSTALL_COMMAND,
INSTALL_SCRIPT_PATH,
renderInstallCommand,
} from "../src/lib/install-command";
import { renderPageMarkup } from "../src/lib/page-markup";

const repoRoot = join(__dirname, "..");

describe("the install command itself", () => {
it("is the exact one-liner", () => {
expect(INSTALL_COMMAND).toBe("curl -fsSL https://logicsrc.com/install.sh | sh");
});

it("keeps the flags that make piping into a shell safe", () => {
// -f so an HTTP error page is never piped into sh, -L so a redirect does
// not silently truncate the install. Dropping either is the bug this pins.
expect(INSTALL_COMMAND).toMatch(/curl\b[^|]*-[a-zA-Z]*f/);
expect(INSTALL_COMMAND).toMatch(/curl\b[^|]*-[a-zA-Z]*L/);
});

it("points at a script that is actually published", () => {
// public/ is served at the site root, so this is the URL in the command.
const script = readFileSync(join(repoRoot, "public", INSTALL_SCRIPT_PATH), "utf8");
expect(script.startsWith("#!/bin/sh")).toBe(true);
// The command says `| sh`; a bash shebang here would make that a lie.
expect(script).toContain(INSTALL_COMMAND);
});
});

describe.each(["hero", "rail"] as const)("the %s placement", (variant) => {
const html = renderInstallCommand(variant);

it("shows the command", () => {
expect(html).toContain(INSTALL_COMMAND);
});

it("offers a copy button carrying the same text that is on screen", () => {
expect(html).toContain(`data-copy="${INSTALL_COMMAND}"`);
// A button whose clipboard payload differs from the visible command is
// worse than no button, so the two are asserted against one constant.
const shown = html.match(/<code[^>]*>([^<]+)<\/code>/)?.[1];
const copied = html.match(/data-copy="([^"]+)"/)?.[1];
expect(shown).toBe(copied);
});

it("is a real button, reachable by keyboard and labelled", () => {
expect(html).toContain('type="button"');
expect(html).toContain('aria-label="Copy the install command"');
});
});

describe("placement on the site", () => {
const home = renderPageMarkup();

it("puts the loud version in the homepage hero", () => {
expect(home).toContain('class="install-cta"');
// Above the fold means before the first content band, not merely present.
expect(home.indexOf("install-cta")).toBeLessThan(home.indexOf('class="band"'));
});

it("also carries the compact version in the chrome", () => {
expect(home).toContain('class="install-rail"');
});

it("keeps the compact one out of the way -- inside the rail, above the nav", () => {
const rail = home.indexOf('class="install-rail"');
expect(rail).toBeGreaterThan(home.indexOf('class="rail"'));
expect(rail).toBeLessThan(home.indexOf("<nav"));
});

it("renders the command twice and no more", () => {
expect(home.split(INSTALL_COMMAND).length - 1).toBe(4); // 2 placements x (code + data-copy)
});
});
3 changes: 3 additions & 0 deletions apps/logicsrc-web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ReactNode } from "react";
import "../styles.css";
import Script from "next/script";
import { FeedbackWidget } from "@profullstack/stack/feedback";
import { CopyButtons } from "@/components/copy-buttons";

const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "");
const DESCRIPTION =
Expand Down Expand Up @@ -82,6 +83,8 @@ export default function RootLayout({ children }: { children: ReactNode }): React
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{children}
{/* one delegated handler for every [data-copy] button, site-wide */}
<CopyButtons />
<Script
data-site="56a0c760-e6cb-4875-844e-8b8aaa80b59b"
src="https://crawlproof.com/stats.js"
Expand Down
71 changes: 71 additions & 0 deletions apps/logicsrc-web/src/components/copy-buttons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";

import { useEffect } from "react";

// One delegated listener for every `[data-copy]` button on the page.
//
// Delegation rather than a handler per button because the markup these target
// is server-rendered in two different ways -- the homepage arrives as an HTML
// string through dangerouslySetInnerHTML, the rest as JSX -- and a listener on
// `document` does not care which. It also means a new copy button anywhere on
// the site needs no wiring, just the attribute.
export function CopyButtons(): null {
useEffect(() => {
const flash = (button: HTMLButtonElement, message: string): void => {
const original = button.dataset.copyLabel ?? button.textContent ?? "Copy";
button.dataset.copyLabel = original;
button.textContent = message;
button.classList.add("is-copied");
window.setTimeout(() => {
button.textContent = original;
button.classList.remove("is-copied");
}, 1600);
};

// navigator.clipboard is undefined outside a secure context, which includes
// plain-http previews and older Safari. Falling back to a throwaway
// textarea keeps the button honest there instead of silently doing nothing.
const legacyCopy = (text: string): boolean => {
const field = document.createElement("textarea");
field.value = text;
field.setAttribute("readonly", "");
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
let copied = false;
try {
copied = document.execCommand("copy");
} catch {
copied = false;
}
field.remove();
return copied;
};

const onClick = async (event: MouseEvent): Promise<void> => {
const target = event.target as HTMLElement | null;
const button = target?.closest<HTMLButtonElement>("button[data-copy]");
if (!button) return;

const text = button.dataset.copy ?? "";
if (!text) return;

try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
flash(button, "Copied");
return;
}
} catch {
// permission denied or a non-secure context -- fall through
}
flash(button, legacyCopy(text) ? "Copied" : "Press Ctrl+C");
};

document.addEventListener("click", onClick);
return () => document.removeEventListener("click", onClick);
}, []);

return null;
}
4 changes: 4 additions & 0 deletions apps/logicsrc-web/src/components/site-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import { renderInstallCommand } from "@/lib/install-command";

// Mirrors the rail/nav from page-markup.ts so standalone routes (e.g. /blog)
// share the site chrome. Anchor links point at the homepage sections.
Expand Down Expand Up @@ -40,6 +41,9 @@ export function SiteShell({
<small>Open coordination standards</small>
</div>
</a>
{/* Same markup the homepage uses, so the two can never drift apart.
Static content from a module constant -- nothing user-supplied. */}
<div dangerouslySetInnerHTML={{ __html: renderInstallCommand("rail") }} />
<nav aria-label="LogicSRC sections">
{NAV.map((item) => (
<a
Expand Down
53 changes: 53 additions & 0 deletions apps/logicsrc-web/src/lib/install-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// The one-line CLI install, rendered as HTML so the string-built homepage
// (page-markup.ts) and the React chrome (SiteShell) can share one definition.
// Two renderings of the same command is how a shipped hint ends up disagreeing
// with itself, so there is deliberately only one here.

/**
* The command, verbatim.
*
* The flags are not decoration. Without `-f`, curl prints an HTTP error body
* and exits 0, so a 404 gets piped into `sh`; without `-L` the install breaks
* the moment the URL redirects. `-sS` keeps the progress meter out of the pipe
* while leaving real errors visible. This is the form install.sh documents in
* its own header.
*/
export const INSTALL_COMMAND = "curl -fsSL https://logicsrc.com/install.sh | sh";

/** Where the script itself lives, for people who read before they pipe. */
export const INSTALL_SCRIPT_PATH = "/install.sh";

/**
* A copy button. The command is static and contains no markup-significant
* characters, so it goes into the attribute as-is; `copy-buttons.tsx` reads it
* back out. Keeping the text on the button means the clipboard can never
* disagree with what is on screen.
*/
function copyButton(className: string): string {
return `<button type="button" class="${className}" data-copy="${INSTALL_COMMAND}" aria-label="Copy the install command">Copy</button>`;
}

/**
* @param variant - `hero` is the homepage's unmissable version; `rail` is the
* compact one that rides along in the site chrome on every other page.
*/
export function renderInstallCommand(variant: "hero" | "rail"): string {
if (variant === "rail") {
return `<div class="install-rail">
<span class="install-rail-label">Install the CLI</span>
<div class="install-rail-row">
<code>${INSTALL_COMMAND}</code>
${copyButton("install-copy install-copy-sm")}
</div>
</div>`;
}

return `<div class="install-cta">
<p class="install-cta-label">Get the CLI</p>
<div class="install-cta-row">
<code class="install-cta-cmd">${INSTALL_COMMAND}</code>
${copyButton("install-copy")}
</div>
<p class="install-cta-note">macOS and Linux · needs Node 18+ · <a href="${INSTALL_SCRIPT_PATH}">read the script first</a></p>
</div>`;
}
3 changes: 3 additions & 0 deletions apps/logicsrc-web/src/lib/page-markup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// same class hooks — now rendered on the server for SEO instead of in the
// browser. Interactivity (hire-us form, CoinPay button, section scroll) lives in
// the `home-interactivity` client component.
import { renderInstallCommand } from "./install-command";

const primitives = [
{ name: "Identity", detail: "DIDs, OAuth accounts, profiles, and organization membership." },
Expand Down Expand Up @@ -119,6 +120,7 @@ export function renderPageMarkup(): string {
<small>Open coordination standards</small>
</div>
</div>
${renderInstallCommand("rail")}
<nav aria-label="LogicSRC sections">
<a class="active" href="#overview">Overview</a>
<a href="#schemas">Schemas</a>
Expand Down Expand Up @@ -146,6 +148,7 @@ export function renderPageMarkup(): string {
<p class="eyebrow">Profullstack open spec project</p>
<h1>LogicSRC</h1>
<p class="lede">Open schemas, primitives, and conventions for coordination between humans, AI agents, plugins, payment systems, and hosted products.</p>
${renderInstallCommand("hero")}
<div class="hero-actions">
<a class="button-primary" href="/api/oauth/coinpay/start">Connect CoinPay</a>
</div>
Expand Down
Loading
Loading