diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 97ce8d1..bd06d82 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -8,6 +8,7 @@ on: - "docs/**" - "scripts/gen-command-reference.sh" - "scripts/build-docs-site.mjs" + - "scripts/docs-site-assets.mjs" - "Makefile" - ".github/workflows/pages.yml" workflow_dispatch: diff --git a/README.md b/README.md index 432eccd..f851863 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,8 @@ go run scripts/gen-auth-services-md.go ## Documentation -- [docs/README.md](docs/README.md): docs overview +- [docs/index.md](docs/index.md): docs overview (rendered at ) +- [docs/quickstart.md](docs/quickstart.md): five-minute setup walkthrough - [docs/commands/README.md](docs/commands/README.md): generated command index - [docs/safety-profiles.md](docs/safety-profiles.md): command guards and baked safe binaries - [docs/auth-clients.md](docs/auth-clients.md): OAuth clients, account mapping, and service accounts diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 4e55bc8..0000000 --- a/docs/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# gog Docs - -`gog` is a single CLI for Google Workspace automation: Gmail, Calendar, Drive, -Docs, Sheets, Slides, Contacts, Tasks, People, Forms, Apps Script, Groups, Admin, -Keep, and related agent workflows. - -## Start Here - -- Install and authenticate from the repository - [README](https://github.com/steipete/gogcli#readme). -- Read [Install and Runtime Packages](install.md) when installing from - Homebrew, Docker, GitHub releases, Windows ZIPs, or source. -- Read [Auth Clients](auth-clients.md) when setting up OAuth clients, service - accounts, or Workspace domain-wide delegation. -- Read [Command Guards and Baked Safety Profiles](safety-profiles.md) when - running `gog` from agents or automation. -- Read the bundled [`gog` agent skill](../.agents/skills/gog/SKILL.md) when an - agent needs safe auth preflight, JSON-first output, or guarded Workspace - automation patterns. -- Read [Sheets Tables](sheets-tables.md) when creating or inspecting Google - Sheets structured tables. -- Open the [Command Index](commands/README.md) for generated docs for every CLI - command. - -## Feature Pages - -- [Install and Runtime Packages](install.md) -- [Auth Clients](auth-clients.md) -- [Command Guards and Baked Safety Profiles](safety-profiles.md) -- [Raw API Dumps](raw-api.md) -- [Raw API Sensitive Field Audit](raw-audit.md) -- [Gmail Workflows](gmail-workflows.md) -- [Gmail watch](watch.md) -- [Email Tracking](email-tracking.md) -- [Drive Audits](drive-audits.md) -- [Contacts Dedupe Preview](contacts-dedupe.md) -- [Contacts JSON Update](contacts-json-update.md) -- [Google Docs Editing](docs-editing.md) -- [Sheets Tables](sheets-tables.md) -- [Sheets Formatting](sheets-formatting.md) -- [Slides from Markdown](slides-markdown.md) -- [Slides Template Replacement](slides-template-replacement.md) -- [Backups](backup.md) -- [Date and Time Input Formats](dates.md) - -## Common Paths - -```bash -gog auth add you@gmail.com --services gmail,calendar,drive -gog gmail search 'newer_than:7d' --max 10 -gog gmail get --sanitize-content --json -gog calendar events --today -gog drive ls --max 20 -``` - -## Command Docs - -Every command page under `docs/commands/` is generated from -`gog schema --json`. Do not hand-edit generated command pages. After changing -commands, flags, aliases, arguments, or help text, run: - -```bash -make docs-commands -``` - -`make docs-check` verifies that every schema command has a generated page and -that required feature pages are present and linked from this overview. - -Then build the GitHub Pages site locally: - -```bash -make docs-site -open dist/docs-site/index.html -``` - -The site is intentionally static: no framework, no package install, and no -client-side dependency beyond a small navigation script embedded by the builder. diff --git a/docs/assets/site.css b/docs/assets/site.css deleted file mode 100644 index 83da001..0000000 --- a/docs/assets/site.css +++ /dev/null @@ -1,489 +0,0 @@ -:root { - --bg0: #07070b; - --bg1: #0b0b11; - --bg2: #11111a; - --card: rgba(17, 17, 26, 0.72); - --card2: rgba(12, 12, 18, 0.64); - --stroke: rgba(255, 255, 255, 0.08); - --stroke2: rgba(255, 255, 255, 0.05); - --text: #f3f4f6; - --muted: rgba(243, 244, 246, 0.7); - --dim: rgba(243, 244, 246, 0.46); - - --b: #4285f4; - --r: #ea4335; - --y: #fbbc05; - --g: #34a853; - - --shadow: 0 24px 60px rgba(0, 0, 0, 0.55); - --shadow2: 0 16px 40px rgba(0, 0, 0, 0.45); - - --radius: 16px; - --radius2: 22px; - - --serif: "Fraunces", ui-serif, Georgia, serif; - --sans: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; -} - -* { - box-sizing: border-box; -} - -html, -body { - height: 100%; -} - -body { - margin: 0; - background: radial-gradient(1200px 800px at 50% -20%, rgba(66, 133, 244, 0.18), transparent 60%), - radial-gradient(900px 700px at 80% 18%, rgba(234, 67, 53, 0.14), transparent 55%), - radial-gradient(1000px 900px at 18% 62%, rgba(52, 168, 83, 0.14), transparent 55%), - radial-gradient(900px 900px at 65% 88%, rgba(251, 188, 5, 0.12), transparent 55%), - linear-gradient(180deg, var(--bg0), var(--bg1) 40%, var(--bg0)); - color: var(--text); - font-family: var(--sans); - -webkit-font-smoothing: antialiased; - line-height: 1.55; - overflow-x: hidden; -} - -a { - color: inherit; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -code { - font-family: var(--mono); - font-size: 0.95em; -} - -.skip { - position: absolute; - left: -999px; - top: 10px; - background: var(--bg2); - border: 1px solid var(--stroke); - color: var(--text); - padding: 10px 12px; - border-radius: 10px; - z-index: 20; -} -.skip:focus { - left: 12px; -} - -.bg { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 0; -} - -.bg__mesh { - position: absolute; - inset: 0; - background: radial-gradient(1200px 800px at 20% 20%, rgba(66, 133, 244, 0.18), transparent 60%), - radial-gradient(1000px 800px at 82% 30%, rgba(234, 67, 53, 0.12), transparent 55%), - radial-gradient(1000px 900px at 40% 85%, rgba(52, 168, 83, 0.12), transparent 55%); - filter: blur(4px) saturate(1.12); - opacity: 0.95; -} - -.bg__grid { - position: absolute; - inset: -2px; - background-image: linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px); - background-size: 72px 72px; - opacity: 0.055; - transform: perspective(900px) rotateX(58deg) translateY(-18%); - transform-origin: top; - mask-image: radial-gradient(60% 60% at 50% 30%, rgba(0, 0, 0, 1), transparent 72%); -} - -.bg__grain { - position: absolute; - inset: 0; - opacity: 0.2; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)' opacity='.45'/%3E%3C/svg%3E"); - mix-blend-mode: overlay; -} - -.wrap { - width: min(1100px, calc(100% - 48px)); - margin: 0 auto; - position: relative; - z-index: 1; -} - -.top { - position: sticky; - top: 0; - z-index: 10; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - background: rgba(7, 7, 11, 0.58); - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} - -.top__row { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 0; -} - -.brand { - display: flex; - align-items: center; - gap: 10px; - text-decoration: none !important; -} - -.brand__mark { - width: 26px; - height: 26px; - border-radius: 10px; - background: conic-gradient(from 200deg, var(--b), var(--g), var(--y), var(--r), var(--b)); - box-shadow: 0 10px 24px rgba(66, 133, 244, 0.15), 0 10px 24px rgba(52, 168, 83, 0.12); -} - -.brand__mark--small { - width: 18px; - height: 18px; - border-radius: 7px; -} - -.brand__name { - font-family: var(--mono); - font-weight: 500; - letter-spacing: -0.02em; -} - -.brand__tag { - font-size: 12px; - color: var(--dim); - border: 1px solid var(--stroke2); - background: rgba(17, 17, 26, 0.55); - padding: 3px 8px; - border-radius: 999px; -} - -.nav { - display: flex; - align-items: center; - gap: 18px; - font-size: 14px; - color: var(--muted); -} - -.nav a { - text-decoration: none; -} - -.nav a:hover { - color: var(--text); - text-decoration: none; -} - -.nav__cta { - color: var(--text) !important; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.08); - padding: 8px 12px; - border-radius: 999px; -} - -.main { - padding-bottom: 70px; -} - -.hero { - padding: 56px 0 26px; -} - -.hero__grid { - display: grid; - grid-template-columns: 1.05fr 0.95fr; - gap: 28px; - align-items: start; -} - -.kicker { - display: inline-flex; - gap: 10px; - align-items: center; - font-size: 12px; - letter-spacing: 0.12em; - text-transform: uppercase; - color: rgba(243, 244, 246, 0.58); - margin: 0 0 14px; -} - -.kicker::before { - content: ""; - width: 10px; - height: 10px; - border-radius: 3px; - background: linear-gradient(135deg, rgba(66, 133, 244, 0.9), rgba(52, 168, 83, 0.85)); - box-shadow: 0 0 0 4px rgba(66, 133, 244, 0.08); -} - -h1 { - font-family: var(--serif); - font-weight: 700; - letter-spacing: -0.03em; - line-height: 0.95; - margin: 0 0 14px; - font-size: clamp(44px, 5.5vw, 68px); -} - -.hero__word { - display: block; - opacity: 0; - transform: translateY(10px); - animation: rise 700ms cubic-bezier(0.2, 1, 0.2, 1) forwards; -} - -.hero__word:nth-child(1) { - animation-delay: 80ms; -} -.hero__word:nth-child(2) { - animation-delay: 160ms; -} -.hero__word:nth-child(3) { - animation-delay: 260ms; -} - -.hero__word--mono { - font-family: var(--mono); - font-weight: 500; - letter-spacing: -0.04em; - color: rgba(243, 244, 246, 0.92); -} - -@keyframes rise { - to { - opacity: 1; - transform: translateY(0); - } -} - -.lede { - margin: 0 0 18px; - font-size: 16.5px; - color: var(--muted); - max-width: 52ch; - opacity: 0; - transform: translateY(10px); - animation: rise 700ms cubic-bezier(0.2, 1, 0.2, 1) 320ms forwards; -} - -.lede strong { - color: var(--text); - font-weight: 600; -} - -.pills { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0 0 20px; - opacity: 0; - transform: translateY(10px); - animation: rise 700ms cubic-bezier(0.2, 1, 0.2, 1) 380ms forwards; -} - -.pill { - font-size: 12px; - border-radius: 999px; - padding: 6px 10px; - border: 1px solid var(--stroke2); - background: rgba(17, 17, 26, 0.55); - color: rgba(243, 244, 246, 0.76); -} - -.pill--b { - box-shadow: inset 0 0 0 1px rgba(66, 133, 244, 0.25); -} -.pill--r { - box-shadow: inset 0 0 0 1px rgba(234, 67, 53, 0.22); -} -.pill--y { - box-shadow: inset 0 0 0 1px rgba(251, 188, 5, 0.22); -} -.pill--g { - box-shadow: inset 0 0 0 1px rgba(52, 168, 83, 0.22); -} - -.hero__actions { - display: flex; - gap: 12px; - margin: 0 0 12px; - opacity: 0; - transform: translateY(10px); - animation: rise 700ms cubic-bezier(0.2, 1, 0.2, 1) 440ms forwards; -} - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 10px; - padding: 10px 14px; - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.08); - text-decoration: none !important; - font-weight: 600; - font-size: 14px; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22); -} - -.btn--primary { - background: linear-gradient(135deg, rgba(66, 133, 244, 0.92), rgba(52, 168, 83, 0.86)); - color: #091018; - border-color: rgba(255, 255, 255, 0.12); - box-shadow: 0 18px 44px rgba(66, 133, 244, 0.18), 0 18px 44px rgba(52, 168, 83, 0.14); -} - -.btn--ghost { - background: rgba(255, 255, 255, 0.06); - color: var(--text); -} - -.btn:hover { - transform: translateY(-1px); -} - -.note { - margin: 0; - color: rgba(243, 244, 246, 0.56); - font-size: 13px; - opacity: 0; - transform: translateY(10px); - animation: rise 700ms cubic-bezier(0.2, 1, 0.2, 1) 520ms forwards; -} - -.note code { - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.07); - padding: 2px 6px; - border-radius: 8px; - color: rgba(243, 244, 246, 0.84); -} - -.hero__panel { - display: grid; - gap: 14px; - opacity: 0; - transform: translateY(10px); - animation: rise 700ms cubic-bezier(0.2, 1, 0.2, 1) 260ms forwards; -} - -.card { - border: 1px solid var(--stroke); - background: var(--card); - border-radius: var(--radius2); - box-shadow: var(--shadow); - overflow: hidden; -} - -.term__bar { - display: flex; - align-items: center; - gap: 12px; - padding: 12px 14px; - background: rgba(12, 12, 18, 0.58); - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} - -.dots { - display: inline-flex; - gap: 6px; -} - -.dot { - width: 11px; - height: 11px; - border-radius: 999px; -} - -.dot--r { - background: #ff5f57; -} -.dot--y { - background: #febc2e; -} -.dot--g { - background: #28c840; -} - -.term__title { - margin-left: auto; - margin-right: auto; - font-family: var(--mono); - font-size: 12px; - color: rgba(243, 244, 246, 0.55); -} - -.term__body { - padding: 14px 14px 16px; - background: rgba(11, 11, 17, 0.6); -} - -.term__pre { - margin: 0; - font-family: var(--mono); - font-size: 12.5px; - color: rgba(243, 244, 246, 0.86); - line-height: 1.6; - white-space: pre-wrap; -} - -.term__pre code { - display: block; -} - -.meta { - padding: 14px 14px; - background: var(--card2); - border: 1px solid rgba(255, 255, 255, 0.06); - box-shadow: var(--shadow2); -} - -.meta__item { - display: grid; - grid-template-columns: 90px 1fr; - gap: 12px; - padding: 10px 0; -} - -.meta__item + .meta__item { - border-top: 1px solid rgba(255, 255, 255, 0.06); -} - -.meta__k { - color: rgba(243, 244, 246, 0.55); - font-size: 12px; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.meta__v { - color: rgba(243, 244, 246, 0.86); - font-size: 13px; -} - -.meta__v code { - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.07); - padding: 2px 6px; - border-radius: 8px; - color: rgba(243, 244, 246, 0.9); -} diff --git a/docs/assets/site.js b/docs/assets/site.js deleted file mode 100644 index 85f3a12..0000000 --- a/docs/assets/site.js +++ /dev/null @@ -1,34 +0,0 @@ -(() => { - const el = document.getElementById("demo"); - if (!el) return; - - const original = el.textContent || ""; - const prefersReduced = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches; - if (prefersReduced) return; - - el.textContent = ""; - - const lines = original.split("\n"); - const chunkDelay = 16; - const lineDelay = 140; - let i = 0; - let j = 0; - - const tick = () => { - if (i >= lines.length) return; - const line = lines[i]; - if (j <= line.length) { - el.textContent += line.slice(j, j + 1); - j += 1; - window.setTimeout(tick, chunkDelay); - return; - } - el.textContent += "\n"; - i += 1; - j = 0; - window.setTimeout(tick, lineDelay); - }; - - window.setTimeout(tick, 260); -})(); - diff --git a/docs/assets/site.more.css b/docs/assets/site.more.css deleted file mode 100644 index c3d0042..0000000 --- a/docs/assets/site.more.css +++ /dev/null @@ -1,239 +0,0 @@ -.section { - padding: 52px 0; -} - -.section__grid { - display: grid; - grid-template-columns: 0.36fr 0.64fr; - gap: 28px; - align-items: start; -} - -h2 { - font-family: var(--serif); - font-weight: 700; - letter-spacing: -0.02em; - margin: 0 0 6px; - font-size: 28px; -} - -h3 { - margin: 0 0 8px; - font-size: 16px; - font-weight: 700; -} - -.muted { - margin: 0; - color: var(--muted); -} - -.cols { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 14px; -} - -.block { - padding: 16px 16px; -} - -.code { - margin: 0; - padding: 12px 12px; - border-radius: 14px; - background: rgba(0, 0, 0, 0.22); - border: 1px solid rgba(255, 255, 255, 0.08); - overflow: auto; -} - -.code code { - font-family: var(--mono); - font-size: 12.5px; - color: rgba(243, 244, 246, 0.88); -} - -.steps { - display: grid; - gap: 14px; -} - -.step { - display: grid; - grid-template-columns: 46px 1fr; - gap: 14px; - padding: 16px; - border-radius: var(--radius2); - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(12, 12, 18, 0.46); - box-shadow: var(--shadow2); -} - -.step__n { - width: 40px; - height: 40px; - border-radius: 14px; - display: grid; - place-items: center; - font-family: var(--mono); - font-weight: 500; - color: rgba(243, 244, 246, 0.92); - background: linear-gradient(135deg, rgba(66, 133, 244, 0.22), rgba(52, 168, 83, 0.18)); - border: 1px solid rgba(255, 255, 255, 0.08); -} - -.step p { - margin: 0 0 10px; - color: var(--muted); -} - -.callout { - margin-top: 14px; - display: grid; - grid-template-columns: 44px 1fr; - gap: 14px; - padding: 16px; - border-radius: var(--radius2); - border: 1px solid rgba(255, 255, 255, 0.08); - background: linear-gradient(135deg, rgba(66, 133, 244, 0.14), rgba(234, 67, 53, 0.08)); - box-shadow: var(--shadow2); -} - -.callout__icon { - width: 44px; - height: 44px; - border-radius: 16px; - background: rgba(0, 0, 0, 0.18); - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: inset 0 0 0 1px rgba(66, 133, 244, 0.18); -} - -.callout__body p { - margin: 0 0 10px; - color: var(--muted); -} - -.callout__body code { - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.07); - padding: 2px 6px; - border-radius: 8px; - color: rgba(243, 244, 246, 0.92); -} - -.grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 14px; -} - -.feat { - padding: 16px; - background: rgba(12, 12, 18, 0.46); - box-shadow: var(--shadow2); -} - -.feat p { - margin: 0; - color: var(--muted); -} - -.footerline { - display: flex; - gap: 12px; - margin-top: 18px; -} - -.sitefoot { - padding: 36px 0 26px; - border-top: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(7, 7, 11, 0.35); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -.sitefoot__row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; -} - -.sitefoot__brand { - display: inline-flex; - align-items: center; - gap: 10px; - font-family: var(--mono); -} - -.sitefoot__small { - margin-top: 8px; - color: rgba(243, 244, 246, 0.6); - font-size: 13px; -} - -.sitefoot__small a { - color: rgba(243, 244, 246, 0.75); -} - -.sep { - margin: 0 8px; - color: rgba(243, 244, 246, 0.28); -} - -.sitefoot__right { - display: flex; - gap: 14px; - color: rgba(243, 244, 246, 0.7); - font-size: 14px; -} - -.sitefoot__fineprint { - margin-top: 18px; - color: rgba(243, 244, 246, 0.46); - font-size: 12px; -} - -@media (max-width: 980px) { - .hero__grid { - grid-template-columns: 1fr; - } - .section__grid { - grid-template-columns: 1fr; - } - .grid { - grid-template-columns: 1fr 1fr; - } -} - -@media (max-width: 640px) { - .wrap { - width: min(1100px, calc(100% - 28px)); - } - .nav { - display: none; - } - .cols { - grid-template-columns: 1fr; - } - .grid { - grid-template-columns: 1fr; - } - .footerline { - flex-direction: column; - align-items: flex-start; - } - .sitefoot__row { - flex-direction: column; - align-items: flex-start; - } -} - -@media (prefers-reduced-motion: reduce) { - * { - animation: none !important; - transition: none !important; - scroll-behavior: auto !important; - } -} - diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 0892c32..0000000 --- a/docs/index.html +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - gog — Google in your terminal - - - - - - - - - - - - - - - - - - - -
- -
- -
-
-
-
-

Google Workspace. One binary.

-

- Google - in your - terminal -

-

- gog unifies Gmail, Calendar, Drive, Contacts, Tasks, Sheets, Docs, Slides, and People - under one CLI — with JSON output and sane defaults. -

- -
- Gmail - Calendar - Drive - Contacts - Tasks - Sheets - Docs - Slides - People -
- -
- Install - Readme -
- -

- Tip: set a default in gog auth manage or export GOG_ACCOUNT=you@gmail.com once, stop repeating --account. -

-
- -
-
- -
-

-$ brew install gogcli
-$ gog auth credentials ~/Downloads/client_secret.json
-$ gog auth add you@gmail.com
-
-$ export GOG_ACCOUNT=you@gmail.com
-$ gog gmail labels list
-$ gog calendar calendars --max 5 --json | jq '.calendars[].summary'
-$ gog drive ls --query "mimeType='application/pdf'" --max 3
-
-
-
- -
-
-
Output
-
tables / --plain / --json
-
-
-
Accounts
-
multi-account + gog auth manage
-
-
-
Secrets
-
OS keyring (Keychain / Secret Service / CredMan)
-
-
-
-
-
- -
-
-
-

Install

-

Homebrew, or build from source.

-
- -
-
-

Homebrew

-
brew install gogcli
-
-
-

From source

-
git clone https://github.com/steipete/gogcli.git
-cd gogcli
-make
-./bin/gog --help
-
-
-
-
- -
-
-
-

Quickstart

-

- You’ll need a Google Cloud “Desktop app” OAuth client JSON once. Then you can keep adding accounts. -

-
- -
-
-
1
-
-

Store credentials

-

Save your downloaded client JSON into gog’s config.

-
gog auth credentials ~/Downloads/client_secret_....json
-
-
-
-
2
-
-

Authorize an account

-

Browser flow by default. Use --manual for headless.

-
gog auth add you@gmail.com
-
-
-
-
3
-
-

Run commands

-

Use --json for scripting.

-
export GOG_ACCOUNT=you@gmail.com
-gog gmail search 'newer_than:7d' --max 10 --json | jq
-
-
-
- -
- -
-

Re-auth a service (e.g. Sheets)

-

- If you add scopes later and Google doesn’t return a refresh token, re-run with - --force-consent. -

-
gog auth add you@gmail.com --services sheets --force-consent
-
-
-
-
- -
-
-
-

Features

-

High leverage commands, consistent UX, and clean output.

-
-
-
-

Gmail

-

Search threads, send mail, manage labels, drafts, filters, settings, and watch (Pub/Sub push).

-
-
-

Calendar

-

List/create/update events, respond to invites, detect conflicts, and check free/busy.

-
-
-

Drive

-

List/search/upload/download, export Docs formats, permissions, folders, URLs.

-
-
-

Sheets / Docs / Slides

-

Read/write Sheets; export Docs/Slides/Sheets to PDF/DOCX/PPTX/XLSX/CSV via Drive.

-
-
-

Contacts / People

-

Personal contacts, “other contacts”, Workspace directory, and your profile.

-
-
-

Tasks

-

Tasklists + tasks: add/update/done/undo/delete/clear with paging and JSON output.

-
-
-
-
- -
-
-
-

Examples

-

A few commands you’ll actually use.

-
-
-
-

Find unread mail

-
gog gmail search 'is:unread newer_than:7d' --max 20
-

Pipe JSON to jq for scripts.

-
gog gmail search 'newer_than:7d' --max 50 --json | jq '.threads[] | .subject'
-
-
-

Export a Sheet as PDF

-
gog sheets export <spreadsheetId> --format pdf --out ./sheet.pdf
-

Docs and Slides are similar.

-
gog docs export <docId> --format docx --out ./doc.docx
-gog slides export <presentationId> --format pptx --out ./deck.pptx
-
-
- - -
-
-
- - - - - - diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..0f228e0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,38 @@ +--- +title: Overview +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 + +A script-friendly Google Workspace CLI. One binary, every major Google API, predictable output for terminals, shell pipelines, CI, and coding agents. + +## Why gog + +- **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. +- **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`. + +## 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. + +## Status + +`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. diff --git a/docs/install.md b/docs/install.md index db48182..b4c349b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,13 +1,9 @@ -# Install and Runtime Packages - -read_when: -- Updating release packages, Docker images, or install instructions. -- Debugging version mismatches between source, Homebrew, and downloaded assets. +# Install `gog` ships as a single binary. The visible version is injected at build time: release builds use the tag, while local builds use `git describe`. -## Homebrew +## Homebrew (macOS, Linux) ```bash brew install gogcli @@ -15,39 +11,24 @@ gog --version ``` The Homebrew formula lives in `steipete/homebrew-tap` and installs the `gog` -binary. Release verification should install or upgrade the tap formula and run: +binary. Release verification should run: ```bash brew test steipete/tap/gogcli gog --version ``` -## GitHub Releases +## Docker / GHCR -Release assets are uploaded by GoReleaser: - -- `gogcli__darwin_amd64.tar.gz` -- `gogcli__darwin_arm64.tar.gz` -- `gogcli__linux_amd64.tar.gz` -- `gogcli__linux_arm64.tar.gz` -- `gogcli__windows_amd64.zip` -- `gogcli__windows_arm64.zip` -- `checksums.txt` - -Windows users download the matching ZIP, extract `gog.exe`, and add the -directory to `PATH`. - -## Docker - -Release tags publish a GitHub Container Registry image: +Release tags publish a non-root GitHub Container Registry image: ```bash docker run --rm ghcr.io/steipete/gogcli:latest version docker run --rm ghcr.io/steipete/gogcli:v0.15.0 version ``` -Authenticated container runs should mount a persistent config directory and use -the encrypted file keyring: +Authenticated container runs should mount a persistent config directory and +use the encrypted file keyring: ```bash docker volume create gogcli-config @@ -60,10 +41,35 @@ docker run --rm -it \ auth add you@gmail.com --services gmail,calendar,drive ``` -Keep `GOG_KEYRING_PASSWORD` in the shell session or CI secret store. Do not bake -it into images, scripts, or checked-in profiles. +Keep `GOG_KEYRING_PASSWORD` in the shell session or your CI secret store. Do +not bake it into images, scripts, or checked-in profiles. -## Source Builds +## Windows + +Download the matching ZIP from the +[latest release](https://github.com/steipete/gogcli/releases): + +- `gogcli__windows_amd64.zip` +- `gogcli__windows_arm64.zip` + +Extract `gog.exe` and put its directory on `PATH`. + +## GitHub releases (raw binaries) + +Release assets are uploaded by GoReleaser: + +- `gogcli__darwin_amd64.tar.gz` +- `gogcli__darwin_arm64.tar.gz` +- `gogcli__linux_amd64.tar.gz` +- `gogcli__linux_arm64.tar.gz` +- `gogcli__windows_amd64.zip` +- `gogcli__windows_arm64.zip` +- `checksums.txt` + +Browse the [releases page](https://github.com/steipete/gogcli/releases) for +the latest tag and the full asset list. + +## Build from source ```bash git clone https://github.com/steipete/gogcli.git @@ -74,9 +80,45 @@ make Source builds require the Go version declared in `go.mod`. -## Related Command Pages +## Safety-profile binaries + +When `gog` is going to be invoked by an agent, sandbox, or other caller that +should not be able to broaden its own permissions, build a safety-profile +binary instead of the default one. See [Safety Profiles](safety-profiles.md). + +```bash +./build-safe.sh safety-profiles/agent-safe.yaml -o bin/gog-agent-safe +./build-safe.sh safety-profiles/readonly.yaml -o bin/gog-readonly +``` + +## Verify the install + +```bash +gog --version +gog auth keyring # report current keyring backend +gog --help # discover top-level commands +``` + +After running [`gog auth credentials`](commands/gog-auth-credentials.md) and +[`gog auth add`](commands/gog-auth-add.md), `gog auth doctor --check` reports +keyring health, refresh-token validity, and Workspace-specific failure modes. + +## Updating + +- **Homebrew:** `brew upgrade gogcli`. +- **Docker:** pull a new tag (`ghcr.io/steipete/gogcli:vX.Y.Z`). +- **GitHub release archives:** download the new tarball/ZIP and replace the + binary. +- **Source builds:** `git pull && make` — the version string comes from + `git describe`. + +Refresh tokens and OAuth clients are forward-compatible across point releases; +no migration step is required for normal upgrades. + +## Related command pages - [`gog version`](commands/gog-version.md) - [`gog auth keyring`](commands/gog-auth-keyring.md) - [`gog auth credentials`](commands/gog-auth-credentials.md) - [`gog auth add`](commands/gog-auth-add.md) +- [`gog auth doctor`](commands/gog-auth-doctor.md) diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..e7ca4fc --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,131 @@ +--- +title: Quickstart +description: "Five minutes from a clean machine to a working gog setup with one Google account." +--- + +# Quickstart + +Five minutes from a clean machine to authenticated Gmail, Calendar, and Drive +queries. For a deeper look at OAuth clients, service accounts, and named +profiles, read [Auth Clients](auth-clients.md) after this. + +## 1. Install + +```bash +brew install gogcli +gog --version +``` + +Other options (Docker, Windows ZIPs, source builds) are documented on +[Install](install.md). + +## 2. Get an OAuth client + +`gog` talks to Google APIs as you, using your own Cloud project. The one-time +setup is: + +1. Open and create a project. +2. Enable the APIs you intend to use: Gmail, Calendar, Drive, Docs, Sheets, + Slides, Forms, Apps Script, People (Contacts), Tasks, Classroom — whatever + you actually need. The [API library](https://console.cloud.google.com/apis/library) + is the fastest way to enable several at once. +3. Configure the [OAuth consent screen](https://console.cloud.google.com/auth/branding) + for "External" + your email; that is enough for personal use. +4. Create a **Desktop app** OAuth client at + and download the JSON. + +Personal `gmail.com` accounts work for normal user APIs (Gmail, Calendar, +Drive, Docs, Sheets, Slides, Forms, Apps Script, Contacts/People, Tasks, +Classroom). Workspace-only APIs (Admin Directory, Cloud Identity Groups, Chat, +Keep with domain-wide delegation) require a managed domain — see +[Auth Clients](auth-clients.md). + +> External + Testing OAuth apps issue refresh tokens that expire after seven +> days. Publish the OAuth app for long-lived tokens, or be ready to re-run +> `gog auth add` weekly. + +## 3. Store the OAuth client + +```bash +gog auth credentials ~/Downloads/client_secret_*.json +``` + +The file is copied to your per-user config (`$XDG_CONFIG_HOME/gogcli/` or the +OS-equivalent) with mode `0600`. + +## 4. Authorize an account + +```bash +gog auth add you@gmail.com --services gmail,calendar,drive,docs,sheets,contacts +``` + +A browser tab opens, you grant the requested scopes, and `gog` stores a +refresh token in your OS keyring (Keychain on macOS, Secret Service on Linux, +Credential Manager on Windows). Headless? Add `--manual` for a paste-the-URL +flow, or `--remote --step 1`/`--step 2` for fully split server runs. + +Verify: + +```bash +gog auth list --check +gog auth doctor --check +``` + +## 5. Set a default account + +```bash +export GOG_ACCOUNT=you@gmail.com +# or persist a default with gog auth alias +gog auth alias set default you@gmail.com +``` + +Now you can drop `--account` from every command. + +## 6. Run real commands + +```bash +# Gmail +gog gmail search 'newer_than:7d' --max 10 +gog gmail get --sanitize-content --json + +# Calendar +gog calendar events --today +gog calendar create --summary "Review" \ + --from "2026-05-06T10:00:00+02:00" \ + --to "2026-05-06T10:30:00+02:00" + +# Drive +gog drive ls --max 20 +gog drive tree --parent --depth 2 +gog drive du --parent --max 20 --json + +# Docs / Sheets / Slides +gog docs cat --tab "Notes" +gog sheets get 'Sheet1!A1:D20' --json +gog slides create-from-markdown "Weekly update" --content-file slides.md + +# Profile +gog me +``` + +`--json` produces a stable JSON envelope on stdout; `--plain` produces TSV. +Human-facing progress, hints, and warnings always go to stderr, so pipes stay +parseable. + +## 7. Shell completion (optional) + +```bash +gog completion bash >> ~/.bash_completion +gog completion zsh > "${fpath[1]}/_gog" +gog completion fish > ~/.config/fish/completions/gog.fish +``` + +## Where next + +- [Auth Clients](auth-clients.md) — named clients, service accounts, ADC, + Workspace domain-wide delegation, OIDC subject migration. +- [Safety Profiles](safety-profiles.md) — runtime allow/deny lists and baked + agent-safe binaries. +- [Gmail Workflows](gmail-workflows.md) and [Drive Audits](drive-audits.md) for + the two surfaces most people start automating. +- [Command Index](commands/) — generated reference for every subcommand. diff --git a/scripts/build-docs-site.mjs b/scripts/build-docs-site.mjs index bc55c7b..cbcbfe0 100755 --- a/scripts/build-docs-site.mjs +++ b/scripts/build-docs-site.mjs @@ -2,72 +2,125 @@ import fs from "node:fs"; import path from "node:path"; +import { css, faviconSvg, js } from "./docs-site-assets.mjs"; + const root = process.cwd(); const docsDir = path.join(root, "docs"); const outDir = path.join(root, "dist", "docs-site"); -const repoEditBase = "https://github.com/steipete/gogcli/edit/main/docs"; +const repoBase = "https://github.com/steipete/gogcli"; +const repoEditBase = `${repoBase}/edit/main/docs`; +const cname = readCname(); +const siteBase = cname ? `https://${cname}` : ""; + +const productName = "gog"; +const productTagline = "Google Workspace in your terminal"; +const productDescription = + "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."; +const brewInstall = "brew install gogcli"; const sections = [ - ["Start", ["README.md", "install.md", "auth-clients.md", "spec.md", "dates.md"]], - ["Commands", rels("commands")], + ["Start", ["index.md", "install.md", "quickstart.md", "auth-clients.md", "safety-profiles.md"]], ["Gmail", ["gmail-workflows.md", "gmail-autoreply.md", "watch.md", "email-tracking.md", "email-tracking-worker.md"]], - ["Workspace", ["raw-api.md", "raw-audit.md", "drive-audits.md", "contacts-dedupe.md", "contacts-json-update.md", "docs-editing.md", "sheets-tables.md", "sheets-formatting.md", "slides-markdown.md", "slides-template-replacement.md", "backup.md", "sedmat.md"]], - ["Safety", ["safety-profiles.md", "RELEASING.md"]], + ["Drive & Files", ["drive-audits.md", "raw-api.md", "raw-audit.md"]], + ["Docs, Sheets, Slides", ["docs-editing.md", "sedmat.md", "sheets-tables.md", "sheets-formatting.md", "slides-markdown.md", "slides-template-replacement.md"]], + ["Contacts", ["contacts-dedupe.md", "contacts-json-update.md"]], + ["Backup", ["backup.md"]], + ["Reference", ["dates.md", "spec.md", "RELEASING.md", "commands/README.md"]], ]; +// Skip these from page generation (internal notes, generated subpages we don't want as their own +// nav-less HTML files, etc.). Generated `commands/*.md` ARE built (deep-linkable) but only the +// commands index appears in the sidebar. +const buildExcludes = [/^refactor\//, /^commands\.generated\.md$/]; + fs.rmSync(outDir, { recursive: true, force: true }); fs.mkdirSync(outDir, { recursive: true }); -const pages = allMarkdown(docsDir).map((file) => { +const allPages = allMarkdown(docsDir).map((file) => { const rel = path.relative(docsDir, file).replaceAll(path.sep, "/"); - const markdown = fs.readFileSync(file, "utf8"); - const title = firstHeading(markdown) || titleize(path.basename(rel, ".md")); - return { file, rel, title, outRel: outPath(rel), markdown }; + const raw = fs.readFileSync(file, "utf8"); + const { frontmatter, body } = parseFrontmatter(raw); + const cleaned = stripStrayDirectives(body); + const title = frontmatter.title || firstHeading(cleaned) || titleize(path.basename(rel, ".md")); + return { file, rel, title, outRel: outPath(rel, frontmatter), markdown: cleaned, frontmatter }; }); +const pages = allPages.filter((page) => !buildExcludes.some((re) => re.test(page.rel))); const pageMap = new Map(pages.map((page) => [page.rel, page])); -const nav = sections - .map(([name, relList]) => ({ - name, - pages: relList.map((rel) => pageMap.get(rel)).filter(Boolean), - })) - .filter((section) => section.pages.length > 0); -const orderedPages = nav.flatMap((section) => section.pages); -const sectionByRel = new Map(); -for (const section of nav) { - for (const page of section.pages) sectionByRel.set(page.rel, section.name); +const permalinkMap = new Map(); +for (const page of pages) { + if (page.frontmatter.permalink) { + permalinkMap.set(normalizePermalink(page.frontmatter.permalink), page); + } } +const nav = sections + .map(([name, rels]) => ({ + name, + pages: rels.map((rel) => pageMap.get(rel)).filter(Boolean), + })) + .filter((section) => section.pages.length); + +const sectionByRel = new Map(); +for (const section of nav) for (const page of section.pages) sectionByRel.set(page.rel, section.name); +const orderedPages = nav.flatMap((s) => s.pages); + for (const page of pages) { const html = markdownToHtml(page.markdown, page.rel); const toc = tocFromHtml(html); - const idx = orderedPages.findIndex((candidate) => candidate.rel === page.rel); + const idx = orderedPages.findIndex((p) => p.rel === page.rel); const prev = idx > 0 ? orderedPages[idx - 1] : null; const next = idx >= 0 && idx < orderedPages.length - 1 ? orderedPages[idx + 1] : null; + const sectionName = sectionByRel.get(page.rel) || "Reference"; const pageOut = path.join(outDir, page.outRel); fs.mkdirSync(path.dirname(pageOut), { recursive: true }); - fs.writeFileSync( - pageOut, - layout({ page, html, toc, prev, next, sectionName: sectionByRel.get(page.rel) || "Docs" }), - "utf8", - ); + fs.writeFileSync(pageOut, layout({ page, html, toc, prev, next, sectionName }), "utf8"); } -for (const name of ["CNAME"]) { - const src = path.join(docsDir, name); - if (fs.existsSync(src)) fs.copyFileSync(src, path.join(outDir, name)); -} +fs.writeFileSync(path.join(outDir, "favicon.svg"), faviconSvg(), "utf8"); fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8"); +if (cname) fs.writeFileSync(path.join(outDir, "CNAME"), cname, "utf8"); +validateLinks(outDir); console.log(`built docs site: ${path.relative(root, outDir)}`); -function rels(dir) { - const full = path.join(docsDir, dir); - if (!fs.existsSync(full)) return []; - return fs - .readdirSync(full) - .filter((name) => name.endsWith(".md")) - .sort((a, b) => (a === "README.md" ? -1 : b === "README.md" ? 1 : a.localeCompare(b))) - .map((name) => `${dir}/${name}`); +function readCname() { + for (const candidate of [path.join(docsDir, "CNAME"), path.join(root, "CNAME")]) { + if (fs.existsSync(candidate)) return fs.readFileSync(candidate, "utf8").trim(); + } + return ""; +} + +function parseFrontmatter(raw) { + const match = raw.match(/^---\n([\s\S]*?)\n---\n?/); + if (!match) return { frontmatter: {}, body: raw }; + const fm = {}; + for (const line of match[1].split("\n")) { + const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/); + if (!m) continue; + let value = m[2]; + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + fm[m[1]] = value; + } + return { frontmatter: fm, body: raw.slice(match[0].length) }; +} + +function stripStrayDirectives(body) { + return body + .replace(/\r\n/g, "\n") + .split("\n") + .filter((line) => !/^\s*\{:\s*[^}]*\}\s*$/.test(line)) + .map((line) => line.replace(/\s*\{:\s*[^}]*\}\s*$/, "")) + .join("\n"); +} + +function normalizePermalink(value) { + let v = value.trim(); + if (!v) return "/"; + if (!v.startsWith("/")) v = `/${v}`; + if (v.length > 1 && v.endsWith("/")) v = v.slice(0, -1); + return v; } function allMarkdown(dir) { @@ -81,7 +134,13 @@ function allMarkdown(dir) { .sort(); } -function outPath(rel) { +function outPath(rel, frontmatter = {}) { + if (frontmatter.permalink) { + const permalink = normalizePermalink(frontmatter.permalink); + if (permalink === "/") return "index.html"; + return `${permalink.slice(1)}/index.html`; + } + if (rel === "index.md") return "index.html"; if (rel === "README.md") return "index.html"; if (rel.endsWith("/README.md")) return rel.replace(/README\.md$/, "index.html"); return rel.replace(/\.md$/, ".html"); @@ -101,6 +160,7 @@ function markdownToHtml(markdown, currentRel) { let paragraph = []; let list = null; let fence = null; + let blockquote = []; const flushParagraph = () => { if (!paragraph.length) return; @@ -112,30 +172,33 @@ function markdownToHtml(markdown, currentRel) { html.push(``); list = null; }; + const flushBlockquote = () => { + if (!blockquote.length) return; + const inner = markdownToHtml(blockquote.join("\n"), currentRel); + html.push(`
${inner}
`); + blockquote = []; + }; const splitRow = (line) => { - const trimmed = line.replace(/^\s*\|/, "").replace(/\|\s*$/, ""); + let trimmed = line.trim(); + if (trimmed.startsWith("|")) trimmed = trimmed.slice(1); + if (trimmed.endsWith("|") && !trimmed.endsWith("\\|")) trimmed = trimmed.slice(0, -1); const cells = []; - let cell = ""; - let escaped = false; - for (const ch of trimmed) { - if (escaped) { - cell += ch; - escaped = false; + let current = ""; + for (let idx = 0; idx < trimmed.length; idx++) { + const char = trimmed[idx]; + if (char === "\\" && trimmed[idx + 1] === "|") { + current += "\\|"; + idx += 1; continue; } - if (ch === "\\") { - escaped = true; - cell += ch; + if (char === "|") { + cells.push(current.trim().replace(/\\\|/g, "|")); + current = ""; continue; } - if (ch === "|") { - cells.push(cell.trim()); - cell = ""; - continue; - } - cell += ch; + current += char; } - cells.push(cell.trim()); + cells.push(current.trim().replace(/\\\|/g, "|")); return cells; }; const isDivider = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(line); @@ -146,6 +209,7 @@ function markdownToHtml(markdown, currentRel) { if (fenceMatch) { flushParagraph(); closeList(); + flushBlockquote(); if (fence) { html.push(`
${escapeHtml(fence.lines.join("\n"))}
`); fence = null; @@ -158,11 +222,24 @@ function markdownToHtml(markdown, currentRel) { fence.lines.push(line); continue; } + if (/^>\s?/.test(line)) { + flushParagraph(); + closeList(); + blockquote.push(line.replace(/^>\s?/, "")); + continue; + } + flushBlockquote(); if (!line.trim()) { flushParagraph(); closeList(); continue; } + if (/^\s*---+\s*$/.test(line)) { + flushParagraph(); + closeList(); + html.push("
"); + continue; + } const heading = line.match(/^(#{1,4})\s+(.+)$/); if (heading) { flushParagraph(); @@ -171,36 +248,35 @@ function markdownToHtml(markdown, currentRel) { const text = heading[2].trim(); const id = slug(text); const inner = inline(text, currentRel); - const anchor = level === 1 ? "" : `#`; - html.push(`${anchor}${inner}`); + if (level === 1) { + html.push(`

${inner}

`); + } else { + html.push(`#${inner}`); + } continue; } if (line.trimStart().startsWith("|") && line.includes("|", line.indexOf("|") + 1) && isDivider(lines[i + 1] || "")) { flushParagraph(); closeList(); const header = splitRow(line); + const aligns = splitRow(lines[i + 1]).map((cell) => { + const left = cell.startsWith(":"); + const right = cell.endsWith(":"); + return right && left ? "center" : right ? "right" : left ? "left" : ""; + }); i += 1; const rows = []; while (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("|")) { i += 1; rows.push(splitRow(lines[i])); } - const th = header.map((cell) => `${inline(cell, currentRel)}`).join(""); - const tb = rows - .map((row) => `${row.map((cell) => `${inline(cell, currentRel)}`).join("")}`) - .join(""); + const th = header.map((c, idx) => `${inline(c, currentRel)}`).join(""); + const tb = rows.map((r) => `${r.map((c, idx) => `${inline(c, currentRel)}`).join("")}`).join(""); html.push(`${th}${tb}
`); continue; } const bullet = line.match(/^\s*-\s+(.+)$/); const numbered = line.match(/^\s*\d+\.\s+(.+)$/); - const quote = line.match(/^>\s+(.+)$/); - if (quote) { - flushParagraph(); - closeList(); - html.push(`
${inline(quote[1], currentRel)}
`); - continue; - } if (bullet || numbered) { flushParagraph(); const tag = bullet ? "ul" : "ol"; @@ -216,6 +292,7 @@ function markdownToHtml(markdown, currentRel) { } flushParagraph(); closeList(); + flushBlockquote(); return html.join("\n"); } @@ -227,79 +304,155 @@ function inline(text, currentRel) { }); out = escapeHtml(out) .replace(/\*\*([^*]+)\*\*/g, "$1") - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `${label}`); + .replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, "$1$2") + .replace(/(^|[^_])_([^_\s][^_]*?)_(?!_)/g, "$1$2") + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `${label}`) + .replace(/<(https?:\/\/[^\s<>]+)>/g, '$1'); out = out.replace(/\\\|/g, "|"); out = out.replace(/<br>/g, "
"); - return out.replace(/\u0000(\d+)\u0000/g, (_, index) => stash[Number(index)]); + return out.replace(/\u0000(\d+)\u0000/g, (_, i) => stash[Number(i)]); } function rewriteHref(href, currentRel) { - if (/^(https?:|mailto:|#)/.test(href)) return href; + if (/^(https?:|mailto:|tel:|#)/.test(href)) return href; const [raw, hash = ""] = href.split("#"); - if (!raw) return `#${hash}`; + if (!raw) return hash ? `#${hash}` : ""; + if (raw.startsWith("/")) { + const target = permalinkMap.get(normalizePermalink(raw)); + if (target) { + const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel); + const out = hrefToOutRel(target.outRel, currentOut); + return hash ? `${out}#${hash}` : out; + } + return href; + } if (!raw.endsWith(".md")) return href; - const target = path.posix.normalize(path.posix.join(path.posix.dirname(currentRel), raw)); - const rewritten = path.posix.relative(path.posix.dirname(outPath(currentRel)), outPath(target)) || "index.html"; + const from = path.posix.dirname(currentRel); + const target = path.posix.normalize(path.posix.join(from, raw)); + let rewritten = pageMap.get(target)?.outRel || outPath(target); + const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel); + rewritten = hrefToOutRel(rewritten, currentOut); return `${rewritten}${hash ? `#${hash}` : ""}`; } function tocFromHtml(html) { const items = []; const re = /([\s\S]*?)<\/h[23]>/g; - let match; - while ((match = re.exec(html))) { - const text = match[3] + let m; + while ((m = re.exec(html))) { + const text = m[3] .replace(/]*>.*?<\/a>/, "") .replace(/<[^>]+>/g, "") .trim(); - items.push({ level: Number(match[1]), id: match[2], text }); + items.push({ level: Number(m[1]), id: m[2], text }); } if (items.length < 2) return ""; - return `