feat: add stacklit export command (markdown overview) + CI matrix#36
feat: add stacklit export command (markdown overview) + CI matrix#36
Conversation
Closes #28. Reading stacklit.json directly is fine for AI agents, but it's hard to skim in a PR description, GitHub issue, or chat message. The new export command renders the same data as readable markdown: stacklit export # markdown to stdout stacklit export -o stacklit.md # markdown to a file The output includes a project header with file/line totals, a language and framework summary, entrypoints, a modules table with direct dependencies, per-module collapsible <details> blocks listing exports and types, the dependency edge list, hot files from the last 90 days, and the existing hints (test command, env vars, where to add features). Rendering is deterministic. Modules, dependencies, and hot files are sorted, so the same stacklit.json produces byte-identical markdown across runs and machines, which makes it safe to commit. Pipe characters in user-supplied strings (module purposes, etc.) are escaped so they don't break markdown table rows.
Three improvements: - Run the test suite on ubuntu-latest, macos-latest, and windows-latest. The Windows postinstall bug fixed in #35 went unnoticed because CI only built on Linux. Adding macOS and Windows runners catches platform-specific regressions before users do. - Run go test with -race so data races in the parser, walker, and engine fail the build instead of slipping through. - Add a go vet step (cheap signal, no extra deps). - Replace the 'init' smoke test with a 'generate --quiet' + 'export' smoke test. Generate exercises the same code path without trying to open a browser, and the export step verifies the new markdown command produces a non-empty file with a top-level heading on every OS. Uses go run instead of a pre-built binary so the smoke step works on Windows without needing to know about the .exe extension.
There was a problem hiding this comment.
Pull request overview
This PR adds a new stacklit export CLI command that converts an existing stacklit.json index into a human-readable, deterministic Markdown overview suitable for PRs/issues/chat, and updates CI to test across Linux/macOS/Windows while exercising the new command.
Changes:
- Add Markdown renderer (
internal/renderer/markdown.go) plus unit tests for determinism, escaping, and idempotent file writes. - Add
stacklit exportcommand to the CLI (supports stdout or-ofile output; configurable--sourceJSON path). - Expand CI to a 3-OS matrix and replace the prior smoke test with
generate --quiet+export.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| USAGE.md | Documents the new stacklit export command and what the Markdown contains. |
| README.md | Adds stacklit export to the quick command list. |
| internal/renderer/markdown.go | Implements deterministic Markdown rendering and idempotent file writing. |
| internal/renderer/markdown_test.go | Adds tests covering expected sections, pipe escaping, determinism, and WriteMarkdown idempotency. |
| internal/cli/root.go | Registers the new export subcommand with the root CLI. |
| internal/cli/export.go | Implements the stacklit export command (format/source/output flags). |
| .github/workflows/ci.yml | Adds OS matrix and smoke-tests generate + export on all runners. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fmt.Fprintf(sb, "| `%s` | %s | %d | %s | %s |\n", | ||
| n, | ||
| escapePipe(m.Purpose), | ||
| m.Files, | ||
| formatThousands(m.Lines), | ||
| deps, | ||
| ) |
There was a problem hiding this comment.
Fixed in cd250ea. Module names and each dependency name in the modules table now go through escapePipe per element. Added TestRenderMarkdownEscapesPipeEverywhere with adversarial inputs (weird|module, other|dep) to lock this in.
| if hasLangs { | ||
| sb.WriteString("| Language | Files | Lines |\n") | ||
| sb.WriteString("|----------|-------|-------|\n") | ||
| langs := sortedKeys(idx.Tech.Languages) | ||
| for _, lang := range langs { | ||
| s := idx.Tech.Languages[lang] | ||
| fmt.Fprintf(sb, "| %s | %d | %s |\n", lang, s.Files, formatThousands(s.Lines)) | ||
| } |
There was a problem hiding this comment.
Fixed in cd250ea. Language keys go through escapePipe now too, covered by the same adversarial test (weird|lang).
| sb.WriteString("| File | Commits |\n") | ||
| sb.WriteString("|------|---------|\n") | ||
| for _, h := range idx.Git.HotFiles { | ||
| fmt.Fprintf(sb, "| `%s` | %d |\n", h.Path, h.Commits90d) |
There was a problem hiding this comment.
Fixed in cd250ea. HotFile.Path is escaped with escapePipe before writing the row, and the new escape test asserts src/odd|name.go becomes src/odd\\|name.go.
| sb.WriteString("---\n") | ||
| sb.WriteString("Generated by [stacklit](https://github.com/glincker/stacklit).") | ||
| if idx.GeneratedAt != "" { | ||
| fmt.Fprintf(sb, " Source: `stacklit.json` (%s).", idx.GeneratedAt) |
There was a problem hiding this comment.
Good catch. Dropped the hard-coded filename in cd250ea; the footer now reads Generated at <timestamp>. since --source can point at any path. Test asserts Source: \stacklit.json`` no longer appears.
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| data, err := os.ReadFile(source) | ||
| if err != nil { | ||
| return fmt.Errorf("could not read %s: %w (run 'stacklit init' first)", source, err) |
There was a problem hiding this comment.
Updated in cd250ea to run 'stacklit generate' first, or 'stacklit init' so the non-browser path is the primary suggestion.
| fmt.Fprintf(cmd.OutOrStderr(), "Wrote %s\n", output) | ||
| return nil | ||
| default: | ||
| return fmt.Errorf("unknown format %q (supported: md)", format) |
There was a problem hiding this comment.
Updated in cd250ea to (supported: md, markdown) so both accepted spellings are listed.
Six issues from Copilot review: - Escape pipe characters in every table cell that interpolates a user-supplied string, not just module purpose. Module names, dependency names, language names, hot-file paths, and dependency edge endpoints all go through escapePipe now. Backticks don't protect pipes in GitHub markdown tables. - Drop the hard-coded 'Source: stacklit.json' from the footer. The --source flag can point at any JSON path, so naming a fixed file was misleading. The footer now just prints 'Generated at <ts>'. - Update the read-error hint to suggest 'stacklit generate' first (the non-browser entry point that CI uses) and mention 'stacklit init' as an alternative. - Update the unknown-format error to list 'md, markdown' since both spellings are accepted by the switch. Adds TestRenderMarkdownEscapesPipeEverywhere to cover module name, dep name, language name, and hot-file path escaping with adversarial inputs.
What this adds
A new
stacklit exportcommand that rendersstacklit.jsonas readable markdown for PR descriptions, GitHub issues, and chat. Closes #28.The output covers the same data as
stacklit.jsonbut in a form a person can skim:<details>blocks listing exports and typesRendering is deterministic. Modules, dependencies, edges, and hot files are sorted, so the same
stacklit.jsonproduces byte-identical markdown across runs and machines. Pipe characters in user-supplied strings get escaped so they don't break markdown table rows.Sample output (run on this repo)
CI improvements
The Windows install bug from #32 went unnoticed because CI only built on Linux. This PR also tightens the workflow:
ubuntu-latest,macos-latest, andwindows-latestgo test -raceso data races in the parser/walker/engine fail the buildgo vetstep (cheap, no extra deps)generate --quiet+exportso it doesn't try to open a browser, and so the new export command is exercised on every OSTest plan
go vet ./...cleango test ./... -race -count=1passesgo run ./cmd/stacklit exportproduces a non-empty markdown file with a top-level headinggo run ./cmd/stacklit export --format jsonexits non-zero with a useful errorCloses #28