diff --git a/docs/index.md b/docs/index.md index 0f228e0..27dc5b5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 --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 --depth 2 +gog drive du --parent --max 20 --json + +# Edit a Doc, append to a Sheet table, push slides from Markdown. +gog docs format --match Status --bold --font-size 18 +gog sheets table append 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. diff --git a/scripts/build-docs-site.mjs b/scripts/build-docs-site.mjs index a8680ad..425fe2b 100755 --- a/scripts/build-docs-site.mjs +++ b/scripts/build-docs-site.mjs @@ -218,7 +218,8 @@ function markdownToHtml(markdown, currentRel) { closeList(); flushBlockquote(); if (fence) { - html.push(`
${escapeHtml(fence.lines.join("\n"))}
`); + const body = highlightCode(fence.lines.join("\n"), fence.lang); + html.push(`
${body}
`); 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(`${escapeHtml(match)}`); + return stashToken(idx); + }); + } + return restoreStashTokens(escapeHtml(working), stash); +} + +function highlightShell(code) { + return code + .split("\n") + .map((line) => { + if (/^\s*#/.test(line)) return `${escapeHtml(line)}`; + const promptMatch = line.match(/^(\s*)([$#>])(\s+)(.*)$/); + if (promptMatch) { + const [, lead, sym, gap, rest] = promptMatch; + return `${escapeHtml(lead)}${escapeHtml(sym)}${escapeHtml(gap)}${highlightShellLine(rest)}`; + } + return highlightShellLine(line); + }) + .join("\n"); +} + +function highlightShellLine(line) { + const stash = []; + const stashAdd = (match, cls) => { + const idx = stash.length; + stash.push(`${escapeHtml(match)}`); + 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 `${escapeHtml(line)}`; + const m = line.match(/^(\s*-?\s*)([A-Za-z0-9_.-]+)(\s*:)(.*)$/); + if (m) { + const [, lead, key, colon, rest] = m; + return `${escapeHtml(lead)}${escapeHtml(key)}${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, "")) + `${escapeHtml(trimmed)}`; + } + if (/^(true|false|null|~)$/i.test(trimmed)) { + return escapeHtml(rest.replace(trimmed, "")) + `${escapeHtml(trimmed)}`; + } + if (/^-?\d+(\.\d+)?$/.test(trimmed)) { + return escapeHtml(rest.replace(trimmed, "")) + `${escapeHtml(trimmed)}`; + } + return escapeHtml(rest); +} + function validateLinks(outputDir) { const failures = []; // Generated command pages embed literal placeholders like `(url)` / `(path)` from help text. diff --git a/scripts/docs-site-assets.mjs b/scripts/docs-site-assets.mjs index 74f8e9e..2fc1a59 100644 --- a/scripts/docs-site-assets.mjs +++ b/scripts/docs-site-assets.mjs @@ -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}