From 63df38105d487ce8295032e4cc7bbecafe0e78e2 Mon Sep 17 00:00:00 2001 From: Michael Cole Date: Wed, 1 Jul 2026 17:10:02 -0400 Subject: [PATCH] Make CLI success output copy-paste-safe; add --serve, docs hub, key hygiene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A successful clone printed a bare, timestamped app path that wrapped mid-word in the terminal, so users' `cd` failed and `npm install && npm run dev` ran in the wrong directory — a confusing failure from a successful clone. Plus there was no first-party "see it locally" path, no central docs page, "clone" was conflated with `git clone`, and key hygiene wasn't stated. CLI (compiler): - Add compiler/src/cliSummary.ts: a copy-paste-safe success block on stderr — a single QUOTED `cd "" && npm install && npm run dev` line that survives terminal wrapping — plus the AGENTS.md safe-to-edit pointers (content.ts, components/). The machine-readable {"event":"done",...} JSON stays on stdout. - Add --serve (npm install + npm run dev in the generated app) and --open (also open the browser at the detected dev URL). Wired into single + multi. - Add writeLatestPointer(): refresh a runs//latest symlink to the newest run so there's a stable, timestamp-free cd/script target. The reuse/regen scanners already filter to digit-prefixed run dirs, so `latest` is ignored. - Tests: cliSummary (quoting, single-line command, Next/Vite roots, stable-path preference, --serve/--open hint) and runsLayout (symlink write + in-place refresh, timestamp-free stable path). Docs: - Add docs/README.md as a central documentation index. - State prominently that ditto "cloning" = generating a codebase from a live URL, NOT `git clone` (no source repo required), in README + docs hub. - Add "keys are secrets: use $DITTO_API_KEY, never inline/commit, rotate anytime" beside the key/auth examples in README + docs/SERVICE.md. - Document --serve/--open and the latest symlink in README + compiler/README. Note: the live ditto.site/docs web route and the dashboard's inline-token snippet live in the marketing-site repo and still need a change there. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 21 ++++++ README.md | 31 ++++++++- compiler/README.md | 9 +++ compiler/src/cli.ts | 48 +++++++++++-- compiler/src/cliSummary.ts | 112 +++++++++++++++++++++++++++++++ compiler/src/site/cloneSite.ts | 8 ++- compiler/test/cliSummary.test.ts | 53 +++++++++++++++ compiler/test/runsLayout.test.ts | 31 +++++++++ docs/README.md | 57 ++++++++++++++++ docs/SERVICE.md | 5 ++ 10 files changed, 364 insertions(+), 11 deletions(-) create mode 100644 compiler/src/cliSummary.ts create mode 100644 compiler/test/cliSummary.test.ts create mode 100644 compiler/test/runsLayout.test.ts create mode 100644 docs/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index faf3aaf..2577993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ so minor/patch semantics are not yet guaranteed. ## [Unreleased] +### Changed — Local CLI success output and orientation + +- The `clone-static` CLI now prints a copy-paste-safe success summary to stderr: a + single **quoted** `cd "" && npm install && npm run dev` line (survives terminal + wrapping — no more broken `cd` from a wrapped timestamped path) plus the key + `AGENTS.md` safe-to-edit pointers (`src/app/content.ts`, `src/app/components/`). The + machine-readable `{ "event": "done", ... }` JSON line on stdout is unchanged (now + also carrying `stableApp`). +- Added `--serve` (run `npm install` + `npm run dev` in the generated app) and + `--open` (also launch the browser at the dev URL) so "see it locally" is one flag. +- In the default runs layout, each clone now refreshes a `runs//latest` symlink + pointing at the newest run, giving a stable, timestamp-free path for `cd` and scripts. + +### Added — Docs hub and terminology/secret-hygiene clarifications + +- Added [`docs/README.md`](docs/README.md) as a central documentation index. +- Clarified prominently that ditto.site "cloning" means generating a codebase from a + live URL, **not** `git clone` (no source repo required), in the README and docs hub. +- Added an explicit "API keys are secrets — use `$DITTO_API_KEY`, never inline/commit, + rotate anytime" note beside the key/auth examples in the README and `docs/SERVICE.md`. + ### Added — `ditto` unpack CLI - **`packages/cli`** — a zero-dependency `ditto` command-line helper. `ditto unpack diff --git a/README.md b/README.md index 1f86121..bfc7abe 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,14 @@ Router project by default, or Vite React when requested. The compiler is not an LLM page author. It is a capture-to-code pipeline: same frozen capture in, byte-stable app out. +> **"Cloning" here means generating a codebase from a live URL — not `git clone`.** +> You don't need an existing repository, and you don't need the site's source. Point +> ditto.site at a public URL and it writes you a fresh project from what the page +> renders in a browser. + Read the public development and evaluation method in -[docs/METHODOLOGY.md](docs/METHODOLOGY.md). +[docs/METHODOLOGY.md](docs/METHODOLOGY.md). For a map of all the docs, see +[docs/README.md](docs/README.md). ## Usage @@ -35,6 +41,11 @@ curl -sS -X POST "https://api.ditto.site/v1/signup/request" \ The emailed verification link lands on `/api-key?token=...`, which calls `POST /v1/signup/verify` and displays the new `dtto_live_...` key once. +> **Keys are secrets.** Put your key in an environment variable (`export +> DITTO_API_KEY=dtto_live_...`) and reference `$DITTO_API_KEY` in every command — +> never paste the raw key inline, where it lands in shell history, logs, or a chat. +> Don't commit it. You can rotate a key anytime from the dashboard. + ### REST API Start a clone job: @@ -169,6 +180,8 @@ src/app/page.tsx, and src/app/ditto.css. ### Local CLI ```bash +# this git clone gets the ditto.site tool itself — the URL you clone into a +# codebase comes later, as the argument to `npm run clone`. git clone https://github.com/ion-design/ditto.site.git cd ditto.site @@ -178,7 +191,17 @@ npx playwright install chromium npm run clone -- https://example.com/ --out=./output ``` -The generated app lands under `output//app`. +The generated app lands under `output//app`. On success the CLI prints a +copy-paste-safe summary — a single quoted `cd … && npm install && npm run dev` +line and pointers to the safe-to-edit files (`src/app/content.ts`, +`src/app/components/`; the app's `AGENTS.md` has the full guide). + +To skip the copy-paste entirely and go straight to a running preview: + +```bash +npm run clone -- https://example.com/ --serve # clone, npm install, npm run dev +npm run clone -- https://example.com/ --open # ...and open the browser too +``` Common local variants: @@ -189,6 +212,10 @@ npm run clone -- https://example.com/ --framework=vite npm run validate-site -- runs/site-example.com/ ``` +Without `--out`, runs land under `runs///` and a stable +`runs//latest` symlink always points at the newest clone, so scripts and +`cd` targets don't depend on the timestamp. + ### Local REST And MCP Service Quick inline mode, with no database: diff --git a/compiler/README.md b/compiler/README.md index 30e702d..f37fb3c 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -16,6 +16,8 @@ npm install npx playwright install chromium npm run clone -- https://example.com/ +npm run clone -- https://example.com/ --serve # then npm install + npm run dev +npm run clone -- https://example.com/ --open # ...and open the browser too npm run clone -- https://example.com/ --mode=multi --styling=tailwind npm run clone -- https://example.com/ --mode=single --framework=vite npm run clone-site -- https://example.com/ @@ -34,6 +36,13 @@ npm run typecheck Root-level scripts forward to these commands, so `npm run clone -- ` works from the repository root too. +("Clone" here = generating a codebase from a live URL, not `git clone`; no source +repo required.) On success the CLI prints a copy-paste-safe summary: a single +quoted `cd … && npm install && npm run dev` line plus safe-to-edit pointers. Pass +`--serve` to run install + dev automatically, or `--open` to also launch the +browser. Without `--out`, a `runs//latest` symlink always points at the +newest run so paths aren't timestamp-fragile. + Multi-page generation defaults to the fast no-validation path. Use `--validate` when the clone command itself should run the full build/render/gates QA pass, or run `validate-site` separately. `--concurrency` controls source route capture; diff --git a/compiler/src/cli.ts b/compiler/src/cli.ts index 198fa6f..ba1a5a5 100755 --- a/compiler/src/cli.ts +++ b/compiler/src/cli.ts @@ -1,11 +1,12 @@ #!/usr/bin/env -S npx tsx import { basename, dirname, join, resolve, sep } from "node:path"; -import { cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs"; import { captureSite, REQUIRED_VIEWPORTS, SAMPLE_VIEWPORTS, type CaptureResult } from "./capture/capture.js"; import { fileURLToPath } from "node:url"; import { generateAll } from "./generate/pipeline.js"; import { refineSizing } from "./generate/refineSizing.js"; import { writeJSON, writeText, ensureDir, readJSON, fileExists } from "./util/fsx.js"; +import { doneSummary, serveApp } from "./cliSummary.js"; import type { AppFramework } from "./generate/app.js"; export type CloneOptions = { @@ -34,6 +35,9 @@ export type CloneResult = { sourceDir: string; appDir: string; sourceUrl: string; + /** A stable, timestamp-free path to the app (via the `runs//latest` symlink), + * present in runs-layout mode when the symlink could be created. */ + stableAppDir?: string; }; export function siteIdFromUrl(url: string): string { @@ -462,16 +466,34 @@ export async function runClone(opts: CloneOptions): Promise { writeText(join(runDir, "logs", "compiler.log.jsonl"), logEvents.map((e) => JSON.stringify(e)).join("\n") + "\n"); + let stableAppDir: string | undefined; if (out) { // Publish the deliverable to /app; keep working artifacts in .clone. exportApp(appDir, out.appDir); logBoth({ event: "exported", app: out.appDir }); } else { - // latest pointer (runs/ layout, used by --reuse / regen) - writeJSON(join(runsDir, siteId, "latest.json"), { runDir, ts: timestamp() }); + // latest pointer (runs/ layout, used by --reuse / regen) + a `latest` symlink so the + // app has a stable, timestamp-free path that survives copy-paste. + stableAppDir = writeLatestPointer(runsDir, siteId, runDir); } - return { runDir, sourceDir, appDir: out ? out.appDir : appDir, sourceUrl: opts.url }; + return { runDir, sourceDir, appDir: out ? out.appDir : appDir, sourceUrl: opts.url, stableAppDir }; +} + +/** Record the newest run for a site in the runs layout: a `latest.json` breadcrumb (used by + * `--reuse`/regen) and a `latest` symlink → runDir. Returns the stable app path when the + * symlink was created (symlinks can be unavailable, e.g. Windows without privilege — non-fatal). */ +export function writeLatestPointer(runsDir: string, siteId: string, runDir: string): string | undefined { + writeJSON(join(runsDir, siteId, "latest.json"), { runDir, ts: timestamp() }); + const link = join(runsDir, siteId, "latest"); + try { + // Refresh an existing symlink; refuse (via non-recursive rm) to clobber a real directory. + rmSync(link, { force: true }); + symlinkSync(runDir, link, "junction"); + return join(link, "generated", "app"); + } catch { + return undefined; + } } function copySourceRef(from: string, to: string): void { @@ -543,12 +565,22 @@ async function main(): Promise { const args = process.argv.slice(2); const url = args.find((a) => !a.startsWith("--")); if (!url) { - console.error("usage: clone-static [--mode=single|multi] [--styling=tailwind|css] [--framework=next|vite] [--out=]"); + console.error("usage: clone-static [--mode=single|multi] [--styling=tailwind|css] [--framework=next|vite] [--out=] [--serve] [--open]"); process.exit(1); } const mode = parseProductMode(args); const styling = parseProductStyling(args); const framework = parseProductFramework(args); + // --serve installs deps + starts the dev server after cloning; --open also launches the browser. + const open = hasAnyFlag(args, ["--open"]); + const serve = open || hasAnyFlag(args, ["--serve"]); + const finish = async (res: { appDir: string; stableAppDir?: string }) => { + if (serve) { + await serveApp(res.appDir, { open }); + } else { + process.stderr.write(doneSummary({ url, appDir: res.appDir, framework, stableAppDir: res.stableAppDir })); + } + }; const vpArg = firstFlagValue(args, ["--dev-viewports", "--viewports"]); const runsArg = firstFlagValue(args, ["--dev-runs", "--runs"]); // --out=: clean //{app,.clone} layout (default: ./output when bare --out). @@ -604,7 +636,8 @@ async function main(): Promise { tier, log: (e) => console.log(JSON.stringify(e)), }); - console.log(JSON.stringify({ event: "done", runDir: res.runDir, app: res.appDir })); + console.log(JSON.stringify({ event: "done", runDir: res.runDir, app: res.appDir, stableApp: res.stableAppDir })); + await finish(res); return; } @@ -625,7 +658,8 @@ async function main(): Promise { reuseSource, log: (e) => console.log(JSON.stringify(e)), }); - console.log(JSON.stringify({ event: "done", runDir: res.runDir, app: res.appDir })); + console.log(JSON.stringify({ event: "done", runDir: res.runDir, app: res.appDir, stableApp: res.stableAppDir })); + await finish(res); } if (import.meta.url === `file://${process.argv[1]}`) { diff --git a/compiler/src/cliSummary.ts b/compiler/src/cliSummary.ts new file mode 100644 index 0000000..fa56c3e --- /dev/null +++ b/compiler/src/cliSummary.ts @@ -0,0 +1,112 @@ +/** + * Human-facing success output for the `clone-static` CLI. + * + * The CLI streams machine-readable JSON events to stdout; this module renders the + * copy-paste-friendly summary a person actually reads at the end, plus the optional + * `--serve` runner that installs deps and starts the dev server for them. + * + * Why this exists: a bare `runs///generated/app` path pasted into a + * terminal wraps mid-word, so `cd` fails and the follow-up `npm run dev` runs in the + * wrong directory. The preview command here is a single quoted line that survives + * wrapping. + */ +import { spawn } from "node:child_process"; +import { platform } from "node:os"; + +export type DoneSummaryInput = { + url: string; + /** The exact generated app directory for this run. */ + appDir: string; + framework: "next" | "vite"; + /** Stable path (a `runs//latest` symlink target) preferred as the `cd` target when present. */ + stableAppDir?: string; +}; + +/** Double-quote a path for a POSIX shell so spaces and wrapping don't break copy-paste. */ +export function shellQuote(p: string): string { + return `"${p.replace(/(["\\$`])/g, "\\$1")}"`; +} + +/** The success block. The preview command is one quoted line on purpose. */ +export function doneSummary(input: DoneSummaryInput): string { + const root = input.framework === "next" ? "src/app" : "src"; + const cdTarget = input.stableAppDir ?? input.appDir; + const lines: string[] = [ + "", + `✓ Done — cloned ${input.url}`, + "", + " Preview it locally (copy-paste this one line):", + "", + ` cd ${shellQuote(cdTarget)} && npm install && npm run dev`, + "", + " What's safe to edit — full guide in AGENTS.md inside the app:", + ` • page copy & content → ${root}/content.ts`, + ` • components → ${root}/components/`, + "", + " Or re-run with --serve to install deps and start the dev server for you", + " (add --open to launch the browser too).", + ]; + if (input.stableAppDir && input.stableAppDir !== input.appDir) { + lines.push(""); + lines.push(` The path above is a stable pointer to the newest clone. This exact run:`); + lines.push(` ${input.appDir}`); + } + return lines.join("\n") + "\n"; +} + +function openBrowser(url: string): void { + const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open"; + const args = platform() === "win32" ? ["/c", "start", "", url] : [url]; + try { + const child = spawn(cmd, args, { stdio: "ignore", detached: true }); + child.on("error", () => {}); // opening a browser is best-effort + child.unref(); + } catch { + /* best-effort */ + } +} + +function runToCompletion(cmd: string, args: string[], cwd: string): Promise { + return new Promise((resolvePromise, reject) => { + const child = spawn(cmd, args, { cwd, stdio: "inherit", shell: platform() === "win32" }); + child.on("error", reject); + child.on("exit", (code) => + code === 0 ? resolvePromise() : reject(new Error(`\`${cmd} ${args.join(" ")}\` exited with code ${code}`)), + ); + }); +} + +/** + * Run `npm install` then `npm run dev` in the generated app (foreground — `npm run dev` + * keeps running until the user stops it). With `open`, launches the default browser at + * the dev URL once the server prints it. + */ +export async function serveApp(appDir: string, opts: { open: boolean }): Promise { + process.stderr.write(`\n→ Installing dependencies in ${appDir}\n`); + await runToCompletion("npm", ["install"], appDir); + + process.stderr.write(`→ Starting the dev server (Ctrl-C to stop)\n\n`); + await new Promise((resolvePromise, reject) => { + const dev = spawn("npm", ["run", "dev"], { + cwd: appDir, + stdio: opts.open ? ["inherit", "pipe", "inherit"] : "inherit", + shell: platform() === "win32", + }); + if (opts.open && dev.stdout) { + let opened = false; + dev.stdout.on("data", (chunk: Buffer) => { + process.stdout.write(chunk); + if (opened) return; + const m = String(chunk).match(/https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?\S*/i); + if (m) { + opened = true; + openBrowser(m[0]); + } + }); + } + dev.on("error", reject); + dev.on("exit", (code) => + code === 0 || code === null ? resolvePromise() : reject(new Error(`\`npm run dev\` exited with code ${code}`)), + ); + }); +} diff --git a/compiler/src/site/cloneSite.ts b/compiler/src/site/cloneSite.ts index 2803da2..1bf2b3a 100644 --- a/compiler/src/site/cloneSite.ts +++ b/compiler/src/site/cloneSite.ts @@ -21,7 +21,7 @@ import { structurallySimilar } from "./signature.js"; import { generateSiteApp, routeToSegment, routeKey, type RouteArtifact } from "./generateSite.js"; import { detectSharedChrome, chromeSignatureId } from "./sharedLayout.js"; import { validateSite, type SiteReport } from "./validateSite.js"; -import { siteIdFromUrl, namedOutDirs, exportApp } from "../cli.js"; +import { siteIdFromUrl, namedOutDirs, exportApp, writeLatestPointer } from "../cli.js"; import { writeJSON, readJSON, ensureDir, fileExists, writeText } from "../util/fsx.js"; import { seoInventoryToMarkdown } from "../generate/seo.js"; import type { AppFramework } from "../generate/app.js"; @@ -56,6 +56,8 @@ export type CloneSiteResult = { plan: RoutePlan; routes: RouteArtifact[]; siteReport?: SiteReport; + /** Stable, timestamp-free path to the app (via the `runs//latest` symlink), when created. */ + stableAppDir?: string; }; function timestamp(): string { @@ -236,8 +238,10 @@ export async function runCloneSite(opts: CloneSiteOptions): Promise { + it("wraps a path in double quotes so spaces survive copy-paste", () => { + assert.equal(shellQuote("/Users/x/runs/site/20260701/generated/app"), '"/Users/x/runs/site/20260701/generated/app"'); + assert.equal(shellQuote("/Users/x/My Sites/app"), '"/Users/x/My Sites/app"'); + }); + + it("escapes characters the shell would otherwise interpret inside double quotes", () => { + assert.equal(shellQuote('/a/"b"/$c/`d`/e\\f'), '"/a/\\"b\\"/\\$c/\\`d\\`/e\\\\f"'); + }); +}); + +describe("doneSummary", () => { + const base = { url: "https://academyux.com/", appDir: "/runs/academyux.com/20260701-173012/generated/app", framework: "next" as const }; + + it("emits a single quoted preview line (no mid-word wrap on the path)", () => { + const out = doneSummary(base); + const line = out.split("\n").find((l) => l.includes("cd ")); + assert.ok(line, "has a cd line"); + // one line, path is quoted so a wrapping terminal can't split the command + assert.match(line!, /cd "\/runs\/academyux\.com\/20260701-173012\/generated\/app" && npm install && npm run dev/); + }); + + it("points at the Next safe-edit areas from AGENTS.md", () => { + const out = doneSummary(base); + assert.match(out, /src\/app\/content\.ts/); + assert.match(out, /src\/app\/components\//); + assert.match(out, /AGENTS\.md/); + }); + + it("uses the Vite src root when framework is vite", () => { + const out = doneSummary({ ...base, framework: "vite" }); + assert.match(out, /cd .* && npm install && npm run dev/); + assert.match(out, /• page copy & content {2}→ {2}src\/content\.ts/); + assert.match(out, /• components {11}→ {2}src\/components\//); + }); + + it("prefers the stable path as the cd target and still shows the exact run", () => { + const out = doneSummary({ ...base, stableAppDir: "/runs/academyux.com/latest/generated/app" }); + const cdLine = out.split("\n").find((l) => l.includes("cd "))!; + assert.match(cdLine, /cd "\/runs\/academyux\.com\/latest\/generated\/app"/); + assert.match(out, /This exact run/); + assert.match(out, /\/runs\/academyux\.com\/20260701-173012\/generated\/app/); + }); + + it("mentions the --serve / --open shortcut", () => { + assert.match(doneSummary(base), /--serve/); + assert.match(doneSummary(base), /--open/); + }); +}); diff --git a/compiler/test/runsLayout.test.ts b/compiler/test/runsLayout.test.ts new file mode 100644 index 0000000..79b1b57 --- /dev/null +++ b/compiler/test/runsLayout.test.ts @@ -0,0 +1,31 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, existsSync, readlinkSync, realpathSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { writeLatestPointer } from "../src/cli.js"; + +describe("writeLatestPointer", () => { + it("writes latest.json and a `latest` symlink to the newest run, refreshing it in place", () => { + const runs = mkdtempSync(join(tmpdir(), "ditto-runs-")); + const siteId = "example.com"; + + const run1 = join(runs, siteId, "20260701-100000"); + mkdirSync(join(run1, "generated", "app"), { recursive: true }); + const stable1 = writeLatestPointer(runs, siteId, run1); + + assert.ok(existsSync(join(runs, siteId, "latest.json")), "writes latest.json breadcrumb"); + assert.ok(stable1 && existsSync(stable1), "returned stable app path resolves through the symlink"); + assert.equal(realpathSync(readlinkSync(join(runs, siteId, "latest"))), realpathSync(run1)); + + // A second run must re-point the existing symlink, not throw on the collision. + const run2 = join(runs, siteId, "20260701-200000"); + mkdirSync(join(run2, "generated", "app"), { recursive: true }); + const stable2 = writeLatestPointer(runs, siteId, run2); + + assert.equal(realpathSync(readlinkSync(join(runs, siteId, "latest"))), realpathSync(run2)); + assert.ok(stable2 && existsSync(stable2)); + // The stable path is timestamp-free (goes through `latest`, not the run's timestamp). + assert.match(stable2!, /example\.com\/latest\/generated\/app$/); + }); +}); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..353189f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,57 @@ +# ditto.site Documentation + +The central index for ditto.site's docs. Start here. + +> **First, the word "clone."** In ditto.site, *cloning* means **generating a +> codebase from a live URL** — it is **not** `git clone`. You do **not** need an +> existing Git repository, and you do **not** need the target site's source code. +> You give ditto.site a public URL; it captures what the page renders in a browser +> and writes you a fresh, runnable project. (The only `git clone` involved is +> optionally cloning *this tool's* repository to run the compiler locally.) + +## Get started + +| I want to… | Go to | +| --- | --- | +| Understand what ditto.site is and see it run | [Project README](../README.md) | +| Call the hosted **REST API** or **MCP** server | [Project README → Usage](../README.md#usage), [SERVICE.md](SERVICE.md) | +| Turn a clone result JSON into files on disk | [`ditto unpack` CLI](../packages/cli/README.md) | +| Run the **compiler locally** from the command line | [compiler/README.md](../compiler/README.md) | +| Deploy the service | [DEPLOY.md](DEPLOY.md) | +| Read the development & evaluation method | [METHODOLOGY.md](METHODOLOGY.md) | +| Understand responsible-use boundaries | [RESPONSIBLE_USE.md](RESPONSIBLE_USE.md) | +| Cut a release | [RELEASING.md](RELEASING.md) | + +## API keys are secrets + +Keys look like `dtto_live_...`. Keep them in an environment variable and reference +it in commands: + +```bash +export DITTO_API_KEY="dtto_live_..." +curl -sS -H "authorization: Bearer $DITTO_API_KEY" "$DITTO_API_URL/v1/clones" +``` + +Never paste a raw key inline (it leaks into shell history, logs, and chat), and +never commit one. Rotate a leaked key anytime from the dashboard. See +[SERVICE.md](SERVICE.md) for the full auth model. + +## The short version of the workflow + +1. **Clone** a URL → `POST /v1/clones` (API) or `npm run clone -- ` (local CLI). +2. **Get the app** → unpack the result JSON with [`ditto unpack`](../packages/cli/README.md), + download the `bundle?format=tgz` archive, or read files from `runs//latest/`. +3. **Preview it** → `cd` into the app and `npm install && npm run dev` (or let the + local CLI do it for you with `--serve` / `--open`). +4. **Edit safely** → each generated app ships an `AGENTS.md` describing what's safe + to change (copy in `src/app/content.ts`, components in `src/app/components/`, etc.). + +## Full doc list + +- [SERVICE.md](SERVICE.md) — REST + MCP service reference (endpoints, options, env vars). +- [DEPLOY.md](DEPLOY.md) — production deployment. +- [METHODOLOGY.md](METHODOLOGY.md) — how the compiler is developed and evaluated. +- [RESPONSIBLE_USE.md](RESPONSIBLE_USE.md) — acceptable-use boundaries. +- [RELEASING.md](RELEASING.md) — release process. +- [../compiler/README.md](../compiler/README.md) — local compiler commands. +- [../packages/cli/README.md](../packages/cli/README.md) — the `ditto unpack` CLI. diff --git a/docs/SERVICE.md b/docs/SERVICE.md index cc35d71..01e0c5d 100644 --- a/docs/SERVICE.md +++ b/docs/SERVICE.md @@ -89,6 +89,11 @@ GET /healthz → { ok: true } (unauthenticated) `/v1/clones*` and `/mcp` are authenticated when `API_KEYS` is set or DB-backed keys exist. Use `Authorization: Bearer ` or `x-api-key: `. + +> **Treat keys as secrets.** In any snippet you copy or share, template the key as +> an environment variable (`Authorization: Bearer $DITTO_API_KEY`) rather than an +> inline `dtto_live_...` token — inline keys leak into shell history, logs, and +> pasted transcripts. Never commit a key; rotate a leaked one from the dashboard. Signup routes are intentionally public only when `SIGNUP_ENABLED=true` **and** `DATABASE_URL` is set. Direct `POST /v1/signup` mints a `dtto_live_...` key immediately when `SIGNUP_DIRECT_ENABLED=true`. For public production signup,