docs(site): polish homepage and code highlighting
This commit is contained in:
parent
2c9c1dcc8b
commit
05914139e5
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user