diff --git a/Makefile b/Makefile index 42ef803..ed3c1ae 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,7 @@ docs-site: docs-commands @node scripts/build-docs-site.mjs docs-check: docs-site + @node scripts/check-docs-coverage.mjs tools: @mkdir -p $(TOOLS_DIR) diff --git a/docs/README.md b/docs/README.md index 6ba4be0..4e55bc8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,8 @@ Keep, and related agent workflows. - 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 @@ -20,6 +22,27 @@ Keep, and related agent workflows. - 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 @@ -40,6 +63,9 @@ commands, flags, aliases, arguments, or help text, run: 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 diff --git a/docs/contacts-dedupe.md b/docs/contacts-dedupe.md new file mode 100644 index 0000000..60050b8 --- /dev/null +++ b/docs/contacts-dedupe.md @@ -0,0 +1,68 @@ +# Contacts Dedupe Preview + +read_when: +- Finding duplicate Google Contacts. +- Reviewing or changing `gog contacts dedupe`. + +`gog contacts dedupe` finds likely duplicate personal contacts and prints a +merge plan. It is preview-only: it does not merge, update, or delete contacts. + +## Command Page + +- [`gog contacts dedupe`](commands/gog-contacts-dedupe.md) + +## Basic Use + +```bash +gog contacts dedupe +gog contacts dedupe --json +gog contacts dedupe --max 500 --json +``` + +Default matching uses normalized email and phone values: + +```bash +gog contacts dedupe --match email,phone +``` + +Name matching is opt-in because it can produce false positives: + +```bash +gog contacts dedupe --match email,phone,name +``` + +## Output + +The command groups contacts that share a matching key. JSON output includes: + +- `scanned`: number of contacts examined +- `groups`: likely duplicate groups +- `primary`: the contact gog would keep first in a hypothetical merge plan +- `merged`: merged emails/phones for preview +- `matched_on`: duplicate email/phone/name keys that caused the group +- `members`: all contacts in the group + +## Safety + +`contacts dedupe` is read-only. There is no apply flag. + +Use `--dry-run` in automation anyway when you want a uniform safety habit across +commands: + +```bash +gog contacts dedupe --dry-run --json +``` + +Use `--fail-empty` in scheduled checks when "no duplicates" should be reported +as a distinct exit code: + +```bash +gog contacts dedupe --fail-empty +``` + +## Related Pages + +- [Raw API Dumps](raw-api.md) +- [Generated Contacts command pages](commands/gog-contacts.md) +- [`gog contacts export`](commands/gog-contacts-export.md) +- [`gog contacts raw`](commands/gog-contacts-raw.md) diff --git a/docs/docs-editing.md b/docs/docs-editing.md new file mode 100644 index 0000000..fe85c06 --- /dev/null +++ b/docs/docs-editing.md @@ -0,0 +1,94 @@ +# Google Docs Editing + +read_when: +- Editing Google Docs content, tabs, formatting, comments, or raw Docs output. +- Reviewing Docs write, format, find-replace, or tab commands. + +Docs commands cover document creation, export, content writes, find/replace, +comments, tabs, formatting, and raw API inspection. + +## Write Markdown + +Append Markdown and convert it to Google Docs formatting: + +```bash +gog docs write --append --markdown --text '## Status' +``` + +Replace the document body with Markdown from a file: + +```bash +gog docs write --replace --markdown --content-file README.md +``` + +Command pages: + +- [`gog docs write`](commands/gog-docs-write.md) +- [`gog docs export`](commands/gog-docs-export.md) +- [`gog docs cat`](commands/gog-docs-cat.md) + +## Format Text + +Apply text or paragraph formatting: + +```bash +gog docs format --match Status --bold --font-size 18 +gog docs format --match "Action item" --text-color '#b00020' +gog docs format --match Heading --alignment center --line-spacing 120 +``` + +Use `--match-all` when every occurrence should be formatted. + +Command page: + +- [`gog docs format`](commands/gog-docs-format.md) + +## Tabs + +Manage Google Docs tabs: + +```bash +gog docs list-tabs +gog docs add-tab --title "Notes" +gog docs rename-tab "Archive" +gog docs delete-tab --force +``` + +Tab-aware commands accept `--tab` by title or ID: + +```bash +gog docs write --append --tab "Notes" --text "Follow-up" +gog docs find-replace old new --tab "Notes" --dry-run +``` + +Command pages: + +- [`gog docs list-tabs`](commands/gog-docs-list-tabs.md) +- [`gog docs add-tab`](commands/gog-docs-add-tab.md) +- [`gog docs rename-tab`](commands/gog-docs-rename-tab.md) +- [`gog docs delete-tab`](commands/gog-docs-delete-tab.md) + +## Find and Replace + +```bash +gog docs find-replace old new --dry-run +gog docs find-replace old '' --first +gog docs find-replace PLACEHOLDER --content-file replacement.md --format markdown +``` + +`--dry-run` is read-only and reports match counts. Empty replacement strings are +allowed and delete matches. + +Command page: + +- [`gog docs find-replace`](commands/gog-docs-find-replace.md) + +## Raw Docs Output + +Use raw output when a script needs the Google Docs API object: + +```bash +gog docs raw --pretty +``` + +See [Raw API Dumps](raw-api.md) for lossless-output safety notes. diff --git a/docs/drive-audits.md b/docs/drive-audits.md new file mode 100644 index 0000000..9665bf9 --- /dev/null +++ b/docs/drive-audits.md @@ -0,0 +1,75 @@ +# Drive Audits + +read_when: +- Auditing Drive folder contents, size, or inventory without changing files. +- Reviewing `drive tree`, `drive du`, or `drive inventory`. + +Drive audit commands are read-only reporting helpers. They are meant for cleanup +planning, migration review, and automation that needs stable JSON without +writing back to Drive. + +## Commands + +- [`gog drive tree`](commands/gog-drive-tree.md) +- [`gog drive du`](commands/gog-drive-du.md) +- [`gog drive inventory`](commands/gog-drive-inventory.md) +- [`gog drive ls`](commands/gog-drive-ls.md) +- [`gog drive get`](commands/gog-drive-get.md) +- [`gog drive raw`](commands/gog-drive-raw.md) + +## Folder Tree + +Print a readable folder tree: + +```bash +gog drive tree --parent --depth 2 +``` + +Use JSON when another tool should consume the result: + +```bash +gog drive tree --parent --depth 3 --json +``` + +## Size Summary + +Summarize folder sizes: + +```bash +gog drive du --parent --max 20 +gog drive du --parent --depth 2 --sort size --json +``` + +`drive du` counts files under folders and sorts by `size`, `path`, or `files`. + +## Inventory Export + +Export a read-only item inventory: + +```bash +gog drive inventory --parent --json +gog drive inventory --parent --max 0 --depth 0 --json > drive-inventory.json +``` + +Use inventory output when you need a machine-readable list of Drive objects for +review, diffing, or downstream cleanup scripts. + +## Shared Drives + +The audit commands include shared drives by default where the underlying Drive +API supports it. Pass `--no-all-drives` to restrict a scan to My Drive: + +```bash +gog drive inventory --parent root --no-all-drives --json +``` + +## Custom Fields + +For object-level inspection, use `drive get --fields`: + +```bash +gog drive get --fields 'id,name,mimeType,size,owners,emailAddress' --json +``` + +Use [`gog drive raw`](commands/gog-drive-raw.md) when you need the raw Drive API +object, with the sensitive-field behavior described in [Raw API Dumps](raw-api.md). diff --git a/docs/gmail-workflows.md b/docs/gmail-workflows.md new file mode 100644 index 0000000..be8ec91 --- /dev/null +++ b/docs/gmail-workflows.md @@ -0,0 +1,88 @@ +# Gmail Workflows + +read_when: +- Working with Gmail content, filters, watches, labels, or agent-safe reads. +- Reviewing Gmail commands that cross from read-only into send or modify flows. + +Gmail is one of gog's broadest surfaces. Use command-specific pages for exact +flags, and use this page to choose the right workflow shape. + +## Search and Read + +```bash +gog gmail search 'newer_than:7d' --max 10 --json +gog gmail get --json +gog gmail thread get --json +``` + +For agents, logs, or issue reports, prefer sanitized content: + +```bash +gog gmail get --sanitize-content --json +gog gmail thread get --sanitize-content --json +``` + +`--sanitize-content` strips unsafe/raw payload details while keeping useful +message text for automation. + +## Filters + +Export filters as Gmail WebUI-compatible XML: + +```bash +gog gmail settings filters export --out filters.xml +``` + +Keep API JSON when a script needs the Gmail API shape: + +```bash +gog gmail settings filters export --format json --json +``` + +Command pages: + +- [`gog gmail settings filters export`](commands/gog-gmail-settings-filters-export.md) +- [`gog gmail settings filters list`](commands/gog-gmail-settings-filters-list.md) +- [`gog gmail settings filters create`](commands/gog-gmail-settings-filters-create.md) +- [`gog gmail settings filters delete`](commands/gog-gmail-settings-filters-delete.md) + +## Send Guardrails + +Block send operations globally for one run: + +```bash +gog --gmail-no-send gmail send --to you@example.com --subject test --text body +``` + +Or use the environment variable in agent shells: + +```bash +export GOG_GMAIL_NO_SEND=1 +``` + +For account-specific send blocking, use the no-send config commands: + +- [`gog config no-send set`](commands/gog-config-no-send-set.md) +- [`gog config no-send list`](commands/gog-config-no-send-list.md) +- [`gog config no-send remove`](commands/gog-config-no-send-remove.md) + +## Watches and Push + +Gmail watch/PubSub workflows are documented in [Gmail watch](watch.md). + +Key command pages: + +- [`gog gmail watch start`](commands/gog-gmail-settings-watch-start.md) +- [`gog gmail watch serve`](commands/gog-gmail-settings-watch-serve.md) +- [`gog gmail watch renew`](commands/gog-gmail-settings-watch-renew.md) +- [`gog gmail history`](commands/gog-gmail-history.md) + +## Email Tracking + +Open tracking is documented in [Email Tracking](email-tracking.md) and +[Email Tracking Worker](email-tracking-worker.md). + +## Raw Gmail + +Use [`gog gmail raw`](commands/gog-gmail-raw.md) when you need the underlying +Gmail API `Message` object. See [Raw API Dumps](raw-api.md) for safety notes. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..db48182 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,82 @@ +# Install and Runtime Packages + +read_when: +- Updating release packages, Docker images, or install instructions. +- Debugging version mismatches between source, Homebrew, and downloaded assets. + +`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 + +```bash +brew install gogcli +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: + +```bash +brew test steipete/tap/gogcli +gog --version +``` + +## GitHub Releases + +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: + +```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: + +```bash +docker volume create gogcli-config + +docker run --rm -it \ + -e GOG_KEYRING_BACKEND=file \ + -e GOG_KEYRING_PASSWORD \ + -v gogcli-config:/home/gog/.config/gogcli \ + ghcr.io/steipete/gogcli:latest \ + 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. + +## Source Builds + +```bash +git clone https://github.com/steipete/gogcli.git +cd gogcli +make +./bin/gog --version +``` + +Source builds require the Go version declared in `go.mod`. + +## 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) diff --git a/docs/raw-api.md b/docs/raw-api.md new file mode 100644 index 0000000..500141a --- /dev/null +++ b/docs/raw-api.md @@ -0,0 +1,64 @@ +# Raw API Dumps + +read_when: +- Using `gog raw` commands for lossless Google API JSON. +- Passing Google API responses into scripts, debuggers, or LLM workflows. +- Reviewing sensitive-field behavior for raw output. + +Raw commands return the canonical Google API response shape instead of gog's +normal curated table/JSON output. They are useful when a script needs a field +that gog does not model yet, or when debugging an API object exactly as Google +returns it. + +## Commands + +- [`gog calendar raw`](commands/gog-calendar-raw.md) +- [`gog contacts raw`](commands/gog-contacts-raw.md) +- [`gog docs raw`](commands/gog-docs-raw.md) +- [`gog drive raw`](commands/gog-drive-raw.md) +- [`gog forms raw`](commands/gog-forms-raw.md) +- [`gog gmail raw`](commands/gog-gmail-raw.md) +- [`gog people raw`](commands/gog-people-raw.md) +- [`gog sheets raw`](commands/gog-sheets-raw.md) +- [`gog slides raw`](commands/gog-slides-raw.md) +- [`gog tasks raw`](commands/gog-tasks-raw.md) + +## Examples + +```bash +gog drive raw --pretty +gog docs raw --json > doc-api.json +gog gmail raw --format metadata --json +gog sheets raw --include-grid-data --json +``` + +Use service-native field masks when available: + +```bash +gog drive raw --fields 'id,name,mimeType,owners(emailAddress)' --json +gog contacts raw people/c123 --person-fields names,emailAddresses,phoneNumbers --json +``` + +## Safety Model + +Raw output is intentionally less opinionated than normal gog output. It may +include private document content, contact data, event attendees, Gmail payloads, +or service-specific metadata. + +Drive has the highest capability-URL risk. By default, `gog drive raw` redacts +fields such as `thumbnailLink`, `webContentLink`, `exportLinks`, `resourceKey`, +`properties`, `appProperties`, and embedded thumbnail bytes unless the user +explicitly names fields via `--fields`. + +Sheets warns when grid data or developer metadata could expose sensitive data, +but keeps output lossless. Docs and Slides may include short-lived image URLs. + +For the full sensitive-field review, read [Raw API Sensitive Field Audit](raw-audit.md). + +## Automation Tips + +- Prefer `--json` for scripts. +- Prefer `--pretty` for humans. +- Use narrow `--fields` or service-specific field masks whenever possible. +- Do not pipe raw output into logs or LLMs unless you are comfortable with the + object's full Google API payload. diff --git a/scripts/build-docs-site.mjs b/scripts/build-docs-site.mjs index f2e15da..bc55c7b 100755 --- a/scripts/build-docs-site.mjs +++ b/scripts/build-docs-site.mjs @@ -8,10 +8,10 @@ const outDir = path.join(root, "dist", "docs-site"); const repoEditBase = "https://github.com/steipete/gogcli/edit/main/docs"; const sections = [ - ["Start", ["README.md", "auth-clients.md", "spec.md", "dates.md"]], + ["Start", ["README.md", "install.md", "auth-clients.md", "spec.md", "dates.md"]], ["Commands", rels("commands")], - ["Gmail", ["gmail-autoreply.md", "watch.md", "email-tracking.md", "email-tracking-worker.md"]], - ["Workspace", ["backup.md", "sheets-tables.md", "contacts-json-update.md", "slides-markdown.md", "slides-template-replacement.md", "sedmat.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"]], ]; diff --git a/scripts/check-docs-coverage.mjs b/scripts/check-docs-coverage.mjs new file mode 100644 index 0000000..0088c81 --- /dev/null +++ b/scripts/check-docs-coverage.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); +const bin = process.env.GOG_BIN || path.join(root, "bin", "gog"); +const docsDir = path.join(root, "docs"); +const commandsDir = path.join(docsDir, "commands"); + +const requiredFeatureDocs = [ + "install.md", + "auth-clients.md", + "safety-profiles.md", + "raw-api.md", + "raw-audit.md", + "gmail-workflows.md", + "watch.md", + "email-tracking.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", + "dates.md", +]; + +const schema = JSON.parse(execFileSync(bin, ["schema", "--json"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 })); +const commands = Array.from(walk(schema.command || {})); +const seenSlugs = new Set(); +const missingCommandPages = []; + +for (const command of commands) { + const base = commandSlug(command); + let slug = base; + let suffix = 2; + while (seenSlugs.has(slug)) { + slug = `${base}-${suffix}`; + suffix += 1; + } + seenSlugs.add(slug); + + const page = path.join(commandsDir, `${slug}.md`); + if (!fs.existsSync(page)) { + missingCommandPages.push(path.relative(root, page)); + } +} + +const docsReadme = fs.readFileSync(path.join(docsDir, "README.md"), "utf8"); +const missingFeaturePages = []; +const unlinkedFeaturePages = []; +const brokenLinks = checkMarkdownLinks(docsDir); + +for (const rel of requiredFeatureDocs) { + const page = path.join(docsDir, rel); + if (!fs.existsSync(page)) { + missingFeaturePages.push(`docs/${rel}`); + continue; + } + if (!docsReadme.includes(`(${rel})`)) { + unlinkedFeaturePages.push(`docs/${rel}`); + } +} + +if (missingCommandPages.length || missingFeaturePages.length || unlinkedFeaturePages.length || brokenLinks.length) { + for (const name of missingCommandPages) console.error(`missing command doc: ${name}`); + for (const name of missingFeaturePages) console.error(`missing feature doc: ${name}`); + for (const name of unlinkedFeaturePages) console.error(`feature doc not linked from docs/README.md: ${name}`); + for (const item of brokenLinks) console.error(`broken docs link: ${item}`); + process.exit(1); +} + +console.log(`docs coverage ok: ${commands.length} command pages, ${requiredFeatureDocs.length} feature pages`); + +function* walk(command) { + yield command; + for (const child of command.subcommands || []) { + yield* walk(child); + } +} + +function canonicalTokens(commandPath) { + return (commandPath || "") + .split(/\s+/) + .filter((part) => part && !(part.startsWith("(") && part.endsWith(")"))); +} + +function canonicalPath(command) { + return canonicalTokens(command.path || command.name || "").join(" "); +} + +function commandSlug(command) { + const slug = canonicalPath(command) + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + return slug || "gog"; +} + +function checkMarkdownLinks(dir) { + const broken = []; + for (const file of allMarkdown(dir)) { + const markdown = fs.readFileSync(file, "utf8"); + const linkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g; + let match; + while ((match = linkPattern.exec(markdown)) !== null) { + const rawTarget = match[1].trim().replace(/^<|>$/g, ""); + if (!rawTarget || rawTarget.startsWith("#")) continue; + if (/^[a-z][a-z0-9+.-]*:/i.test(rawTarget)) continue; + + const targetWithoutTitle = rawTarget.split(/\s+["'][^"']*["']\s*$/)[0]; + const targetPath = targetWithoutTitle.split("#")[0]; + if (!targetPath) continue; + if (/^(url|path|file)$/i.test(targetPath)) continue; + + const resolved = path.resolve(path.dirname(file), targetPath); + if (!fs.existsSync(resolved)) { + broken.push(`${path.relative(root, file)} -> ${targetPath}`); + } + } + } + return broken; +} + +function allMarkdown(dir) { + return fs + .readdirSync(dir, { withFileTypes: true }) + .flatMap((entry) => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return allMarkdown(full); + return entry.name.endsWith(".md") ? [full] : []; + }); +}