docs(site): polish homepage and code highlighting

This commit is contained in:
Peter Steinberger 2026-05-06 09:55:35 +01:00
parent 2c9c1dcc8b
commit 05914139e5
No known key found for this signature in database
3 changed files with 193 additions and 25 deletions

View File

@ -4,35 +4,47 @@ permalink: /
description: "gog is a single Go CLI for Gmail, Calendar, Drive, Docs, Sheets, Slides, Forms, Apps Script, Contacts, Tasks, and Workspace admin — built for terminals, scripts, CI, and coding agents."
---
# gog
## Try it
A script-friendly Google Workspace CLI. One binary, every major Google API, predictable output for terminals, shell pipelines, CI, and coding agents.
After you store an OAuth client and authorize an account ([Quickstart](quickstart.md) walks through the five-minute version), everything is a one-liner.
## Why gog
```bash
# Search this week's mail and read a sanitized message body for an agent.
gog gmail search 'newer_than:7d' --max 10
gog gmail get <messageId> --sanitize-content --json
- **One CLI, every API.** Gmail, Calendar, Drive, Docs, Sheets, Slides, Forms, Apps Script, Contacts, People, Tasks, Classroom, Chat, Groups, Keep, and Workspace Admin under one binary.
- **Stable output.** `--json` to stdout for scripts, `--plain` TSV when you need to `awk`, human progress on stderr so pipes stay clean.
- **Multi-account, multi-client.** Many Google accounts and OAuth client projects in one config. OAuth, direct access tokens, ADC, and Workspace service accounts all supported.
- **Built for agents.** Runtime allow/deny lists (`--enable-commands`, `--disable-commands`, `--gmail-no-send`) and baked safety-profile binaries for sandboxes that should not be able to broaden their own permissions.
# Today's calendar.
gog calendar events --today
# Audit a Drive folder without changing anything.
gog drive tree --parent <folderId> --depth 2
gog drive du --parent <folderId> --max 20 --json
# Edit a Doc, append to a Sheet table, push slides from Markdown.
gog docs format <docId> --match Status --bold --font-size 18
gog sheets table append <spreadsheetId> Tasks 'Ship README|done'
gog slides create-from-markdown "Weekly update" --content-file slides.md
```
`--json` produces a stable JSON envelope on stdout, `--plain` produces TSV; human progress, prompts, and warnings always go to stderr so pipes stay parseable.
## What gog does
- **One binary, every API.** Gmail, Calendar, Drive, Docs, Sheets, Slides, Forms, Apps Script, Contacts, People, Tasks, Classroom, Chat, Groups, Keep, and Workspace Admin.
- **Stable output.** `--json` for scripts, `--plain` TSV for `awk`, human output on stderr.
- **Multi-account, multi-client.** Many Google accounts and OAuth client projects in one config; OAuth, direct access tokens, ADC, and Workspace service accounts all supported.
- **Built for agents.** Runtime allow/deny lists (`--enable-commands`, `--disable-commands`, `--gmail-no-send`) plus baked safety-profile binaries that cannot be reconfigured at runtime.
- **Read-only audits.** Drive `tree`, `du`, `inventory`; Contacts `dedupe` preview; raw API JSON dumps without ever mutating remote state.
- **Generated reference.** Every command has a Markdown page produced from `gog schema --json`.
- **Generated reference.** Every command has a docs page produced from `gog schema --json`.
## Pick your path
- **Trying it.** Read [Install](install.md), then [Quickstart](quickstart.md). Five minutes from `brew install` to your first authenticated query.
- **Wiring up an agent.** Read [Safety Profiles](safety-profiles.md) and the bundled [`gog` agent skill](https://github.com/steipete/gogcli/blob/main/.agents/skills/gog/SKILL.md). Lock the binary down before you hand it to a model.
- **Running Workspace at scale.** Read [Auth Clients](auth-clients.md) for service accounts, named OAuth clients, and domain-wide delegation.
- **Backing up an account.** Read [Backup](backup.md) before pointing `gog backup push` at a busy mailbox.
- **Looking up a flag.** Open the [Command Index](commands/) — every subcommand has a generated page.
- **Trying it.** [Install](install.md) → [Quickstart](quickstart.md). Five minutes from `brew install` to your first authenticated query.
- **Wiring up an agent.** [Safety Profiles](safety-profiles.md) and the bundled [`gog` agent skill](https://github.com/steipete/gogcli/blob/main/.agents/skills/gog/SKILL.md). Lock the binary down before handing it to a model.
- **Running Workspace at scale.** [Auth Clients](auth-clients.md) for service accounts, named OAuth clients, and domain-wide delegation.
- **Backing up an account.** [Backup](backup.md) before pointing `gog backup push` at a busy mailbox.
- **Looking up a flag.** The [Command Index](commands/) has a generated page for every subcommand.
## Status
## Project
`gog` is actively developed; the [CHANGELOG](https://github.com/steipete/gogcli/blob/main/CHANGELOG.md) has the most recent shipping log. The API surface is intentionally not 1:1 with `gmcli`/`gccli`/`gdcli` and there is no migration tooling — `gog` is the new CLI, not a port.
## Out of scope
- MCP server (this is a CLI).
- Hosted runtime, web UI, or GUI.
- Importing legacy `~/.gmcli`, `~/.gccli`, `~/.gdcli` state.
Released under the [MIT license](https://github.com/steipete/gogcli/blob/main/LICENSE). Not affiliated with Google. Google is a trademark of Google LLC.
Active development; the [changelog](https://github.com/steipete/gogcli/blob/main/CHANGELOG.md) tracks what shipped recently. Goals and non-goals live in the [spec](spec.md). Released under the [MIT license](https://github.com/steipete/gogcli/blob/main/LICENSE). Not affiliated with Google.

View File

@ -218,7 +218,8 @@ function markdownToHtml(markdown, currentRel) {
closeList();
flushBlockquote();
if (fence) {
html.push(`<pre><code class="language-${escapeAttr(fence.lang)}">${escapeHtml(fence.lines.join("\n"))}</code></pre>`);
const body = highlightCode(fence.lines.join("\n"), fence.lang);
html.push(`<pre><code class="language-${escapeAttr(fence.lang)}">${body}</code></pre>`);
fence = null;
} else {
fence = { lang: fenceMatch[1] || "text", lines: [] };
@ -538,6 +539,137 @@ function escapeAttr(value) {
return escapeHtml(value);
}
function highlightCode(code, lang) {
const language = (lang || "text").toLowerCase();
if (language === "bash" || language === "sh" || language === "shell" || language === "zsh" || language === "console") {
return highlightShell(code);
}
if (language === "json" || language === "json5") return highlightJson(code);
if (language === "ts" || language === "typescript" || language === "js" || language === "javascript" || language === "tsx" || language === "jsx") {
return highlightJs(code);
}
if (language === "go" || language === "golang") return highlightGo(code);
if (language === "yaml" || language === "yml") return highlightYaml(code);
return escapeHtml(code);
}
function stashToken(idx) {
return String.fromCharCode(0xe000 + idx);
}
function restoreStashTokens(value, stash) {
return value.replace(/[\ue000-\uf8ff]/g, (token) => {
const idx = token.charCodeAt(0) - 0xe000;
return stash[idx] ?? "";
});
}
function withStash(code, patterns) {
const stash = [];
let working = code;
for (const [re, cls] of patterns) {
working = working.replace(re, (match) => {
const idx = stash.length;
stash.push(`<span class="${cls}">${escapeHtml(match)}</span>`);
return stashToken(idx);
});
}
return restoreStashTokens(escapeHtml(working), stash);
}
function highlightShell(code) {
return code
.split("\n")
.map((line) => {
if (/^\s*#/.test(line)) return `<span class="hl-c">${escapeHtml(line)}</span>`;
const promptMatch = line.match(/^(\s*)([$#>])(\s+)(.*)$/);
if (promptMatch) {
const [, lead, sym, gap, rest] = promptMatch;
return `${escapeHtml(lead)}<span class="hl-p">${escapeHtml(sym)}</span>${escapeHtml(gap)}${highlightShellLine(rest)}`;
}
return highlightShellLine(line);
})
.join("\n");
}
function highlightShellLine(line) {
const stash = [];
const stashAdd = (match, cls) => {
const idx = stash.length;
stash.push(`<span class="${cls}">${escapeHtml(match)}</span>`);
return stashToken(idx);
};
let working = line;
working = working.replace(/(?:'[^']*'|"[^"]*")/g, (m) => stashAdd(m, "hl-s"));
working = working.replace(/\s#.*$/g, (m) => stashAdd(m, "hl-c"));
working = working.replace(/(^|\s)(--?[A-Za-z][A-Za-z0-9-]*)/g, (_, lead, flag) => `${escapeHtml(lead)}${stashAdd(flag, "hl-f")}`);
working = working.replace(/\b(gog|brew|go|git|gh|make|sudo|cd|export|cat|curl|jq|ls|mv|cp|rm|mkdir|docker|tail|node|npm|pnpm|yarn)\b/g, (m) => stashAdd(m, "hl-cmd"));
working = working.replace(/\b(\d+(?:\.\d+)?)\b/g, (m) => stashAdd(m, "hl-n"));
return restoreStashTokens(escapeHtml(working), stash);
}
function highlightJson(code) {
return withStash(code, [
[/"(?:\\.|[^"\\])*"\s*:/g, "hl-k"],
[/"(?:\\.|[^"\\])*"/g, "hl-s"],
[/\b(true|false|null)\b/g, "hl-m"],
[/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/gi, "hl-n"],
]);
}
function highlightJs(code) {
return withStash(code, [
[/\/\/[^\n]*/g, "hl-c"],
[/\/\*[\s\S]*?\*\//g, "hl-c"],
[/`(?:\\.|[^`\\])*`/g, "hl-s"],
[/"(?:\\.|[^"\\])*"/g, "hl-s"],
[/'(?:\\.|[^'\\])*'/g, "hl-s"],
[/\b(const|let|var|function|return|if|else|for|while|switch|case|break|continue|class|extends|new|import|from|export|default|async|await|try|catch|finally|throw|typeof|instanceof|interface|type|enum|as|of|in|null|undefined|true|false|this)\b/g, "hl-k"],
[/\b(\d+(?:\.\d+)?)\b/g, "hl-n"],
]);
}
function highlightGo(code) {
return withStash(code, [
[/\/\/[^\n]*/g, "hl-c"],
[/\/\*[\s\S]*?\*\//g, "hl-c"],
[/`[^`]*`/g, "hl-s"],
[/"(?:\\.|[^"\\])*"/g, "hl-s"],
[/\b(package|import|func|return|if|else|for|range|switch|case|break|continue|default|type|struct|interface|map|chan|go|defer|select|var|const|nil|true|false|iota)\b/g, "hl-k"],
[/\b(\d+(?:\.\d+)?)\b/g, "hl-n"],
]);
}
function highlightYaml(code) {
return code
.split("\n")
.map((line) => {
if (/^\s*#/.test(line)) return `<span class="hl-c">${escapeHtml(line)}</span>`;
const m = line.match(/^(\s*-?\s*)([A-Za-z0-9_.-]+)(\s*:)(.*)$/);
if (m) {
const [, lead, key, colon, rest] = m;
return `${escapeHtml(lead)}<span class="hl-k">${escapeHtml(key)}</span>${escapeHtml(colon)}${highlightYamlValue(rest)}`;
}
return escapeHtml(line);
})
.join("\n");
}
function highlightYamlValue(rest) {
if (!rest.trim()) return escapeHtml(rest);
const trimmed = rest.trim();
if (/^["'].*["']$/.test(trimmed)) {
return escapeHtml(rest.replace(trimmed, "")) + `<span class="hl-s">${escapeHtml(trimmed)}</span>`;
}
if (/^(true|false|null|~)$/i.test(trimmed)) {
return escapeHtml(rest.replace(trimmed, "")) + `<span class="hl-m">${escapeHtml(trimmed)}</span>`;
}
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
return escapeHtml(rest.replace(trimmed, "")) + `<span class="hl-n">${escapeHtml(trimmed)}</span>`;
}
return escapeHtml(rest);
}
function validateLinks(outputDir) {
const failures = [];
// Generated command pages embed literal placeholders like `(url)` / `(path)` from help text.

View File

@ -16,9 +16,17 @@ export function css() {
--code-bg:#0f172a;
--code-fg:#e6edf3;
--code-inline-fg:#1c2128;
--code-border:#1f2937;
--pill-border:#dbe2eb;
--shadow-card:0 4px 14px rgba(15,17,21,.08);
--scrollbar:#cbd5e1;
--hl-keyword:#7aa2ff;
--hl-string:#9ece6a;
--hl-number:#e0a96d;
--hl-comment:#7c8597;
--hl-flag:#c4a4ff;
--hl-meta:#f08aa0;
--hl-prompt:#64748b;
}
:root[data-theme="dark"]{
--ink:#f3f5f9;
@ -35,9 +43,17 @@ export function css() {
--code-bg:#06080d;
--code-fg:#e6edf3;
--code-inline-fg:#e6edf3;
--code-border:#1c2030;
--pill-border:#2a2f3c;
--shadow-card:0 4px 18px rgba(0,0,0,.45);
--scrollbar:#3a4154;
--hl-keyword:#7aa2ff;
--hl-string:#a6e3a1;
--hl-number:#f0a868;
--hl-comment:#6b7388;
--hl-flag:#c4a4ff;
--hl-meta:#ff8aa0;
--hl-prompt:#7e8ba3;
}
:root{color-scheme:light}
:root[data-theme="dark"]{color-scheme:dark}
@ -124,7 +140,7 @@ body:not(.home) .doc>h1:first-child{display:none}
.doc strong{font-weight:600;color:var(--ink)}
.doc em{font-style:italic}
.doc code{font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;font-size:.84em;background:var(--line-soft);border:1px solid var(--line);border-radius:5px;padding:.08em .35em;color:var(--code-inline-fg)}
.doc pre{position:relative;overflow:auto;background:var(--code-bg);color:var(--code-fg);border-radius:8px;padding:14px 18px;margin:1.3em 0;font-size:.85em;line-height:1.6;scrollbar-width:thin;scrollbar-color:#334155 transparent;border:1px solid #1f2937}
.doc pre{position:relative;overflow:auto;background:var(--code-bg);color:var(--code-fg);border-radius:8px;padding:14px 18px;margin:1.3em 0;font-size:.85em;line-height:1.6;scrollbar-width:thin;scrollbar-color:#334155 transparent;border:1px solid var(--code-border)}
.doc pre::-webkit-scrollbar{height:8px;width:8px}
.doc pre::-webkit-scrollbar-thumb{background:#334155;border-radius:8px}
.doc pre code{display:block;background:transparent;border:0;color:inherit;padding:0;font-size:1em;white-space:pre}
@ -132,6 +148,14 @@ body:not(.home) .doc>h1:first-child{display:none}
.doc pre:hover .copy,.doc pre .copy:focus{opacity:1}
.doc pre .copy:hover{background:rgba(255,255,255,.12)}
.doc pre .copy.copied{background:var(--accent);border-color:var(--accent);opacity:1}
.doc pre .hl-c{color:var(--hl-comment);font-style:italic}
.doc pre .hl-s{color:var(--hl-string)}
.doc pre .hl-n{color:var(--hl-number)}
.doc pre .hl-k{color:var(--hl-keyword);font-weight:600}
.doc pre .hl-f{color:var(--hl-flag)}
.doc pre .hl-m{color:var(--hl-meta);font-weight:600}
.doc pre .hl-p{color:var(--hl-prompt);user-select:none}
.doc pre .hl-cmd{color:var(--hl-keyword);font-weight:600}
.doc blockquote{margin:1.4em 0;padding:10px 16px;border-left:3px solid var(--accent);background:var(--accent-soft);border-radius:0 8px 8px 0;color:var(--text)}
.doc blockquote p:last-child{margin-bottom:0}
.doc table{width:100%;border-collapse:collapse;margin:1.2em 0;font-size:.92em}