Coalesce merges streamer output by deduplicating types with identical
key sets, collapsing {null} into nullable annotations ({string?}),
cleaning up type names (removing unnecessary numbers, deriving names
from keys when many types share a shape), and eliding duplicate type
definitions. Preserves {undefined} distinct from {null} per the
three-stage pipeline design.
Pokemon fixture: 241 raw lines → 140 coalesced lines.
Add RawPathsConfig with SampleLen to show truncated sample values
instead of type names at leaf nodes (e.g., {"Alice Anglerso..."} instead
of {string}). Add absentValue sentinel to distinguish fields absent
from a struct instance ({undefined}) vs explicitly null ({null}).
Type names now appear once on the intro line, and subsequent fields
use the bare path without repeating the type. Root no longer has a
leading dot.
Before: .{Root0}.rooms[]{RoomsItem1}._timing{Timing2}.account_ms{int}
After: .rooms[]._timing.account_ms{int}
The type intro line declares the type, making the path structure
cleaner and more readable while remaining lossless.
With ≤3 keys, one word-like key is enough to call it a struct.
With 4+ keys, a majority must be word-like — prevents a single
coincidental match (e.g., "beef" in hex keys) from misclassifying.
Also fixes short segment handling: 2-char abbreviations like "ms" in
"account_ms" are neutral rather than causing the whole key to fail
word detection.
Rewrote looksLikeMap from 6 layered heuristics down to 3 simple rules:
1. All numeric keys → map (certain)
2. Any key composed of words → struct (certain)
3. Everything else → map (safe default)
The asymmetry is deliberate: a map misidentified as a struct explodes
into hundreds of fields, while a struct misidentified as a map is
compact and easy to fix.
Word detection splits keys on _ and camelCase, checks segments for
pronounceability (vowel/consonant rhythm). Short words (id, at, on)
only count when paired with a strong 3+ char word.
Also adds raw path type naming with Item suffix for collections
(FriendsItem instead of Friend), real API test fixtures from PokeAPI
and SWAPI, and UUID-based test data replacing synthetic abc123 keys.
isBase64 matched common field names like "name" — too loose for map
detection. Replace with isPronounceable which checks vowel/consonant
rhythm: real field names have natural syllable structure (15-80% vowels,
no 5+ consonant runs), while tokens and IDs don't.
Also fix allLookLikeIDs to reject keys that don't match any ID pattern
instead of silently accepting them.
RawPaths walks JSON depth-first and emits flat paths with monotonically
numbered type names (Root0, People2, etc.). Each type intro immediately
precedes its fields, making the output streamable and order-stable.
Also improves map detection heuristics:
- All-numeric keys are always maps (even with 1-2 keys)
- Keys containing digits detected as IDs (e.g., emp_101)
- New isKnownFieldName with broad list of common field names
(timestamps, flags, relations, etc.) as strong struct signal
Separate library from CLI concerns:
- Add Resolver callback type with Decision/Response structs for all
interactive decisions (map/struct, type name, tuple/list, shape
unification, shape naming, name collision)
- Move terminal I/O (Prompter) from library to cmd/jsonpaths
- Add public API: New(), ParseFormat(), Generate(), AutoGenerate()
- Add Format type with aliases (ts, py, json-paths, etc.)
- Fix godoc comments to match exported function names
- Update tests to use scriptedResolver instead of Prompter internals
- Update doc.go and README with current API
Add tools/jsontypes library and tools/jsontypes/cmd/jsonpaths CLI.
Given a JSON sample (file, URL, or stdin), walks the structure,
detects maps vs structs, infers optional fields from multiple
instances, and produces typed definitions.
Output formats (--format):
- json-paths: flat type path notation (default)
- go: struct definitions with json tags and union support
- typescript: interfaces with optional/nullable fields
- jsdoc: @typedef annotations
- zod: validation schemas with type inference
- python: TypedDict classes
- sql: CREATE TABLE with FK relationships
- json-schema: draft 2020-12
- json-typedef: RFC 8927
Features:
- Interactive prompts for ambiguous structure (map vs struct, same
vs different types), with --anonymous mode for non-interactive use
- Answer replay: saves prompt answers to .answers files for iterative
refinement
- URL fetching with local caching and sensitive param stripping
- Curl-like auth: -H, --bearer, --user, --cookie, --cookie-jar
- Discriminated union support with sealed interfaces, unique-field
probing, and CHANGE ME comments for type/kind discriminators
- Extensive round-trip compilation tests for generated Go code
Go 1.23+ changed Windows target naming so x86_64 builds now carry a
micro-architecture suffix (e.g. _v2). Update the goreleaser YAML
generator and monorel's own .goreleaser.yaml to match:
- Remove windows from defaultGoos/almostAllGoos; emit a dedicated
<name>-windows build entry per binary. Windows is kept in a separate
build because it does not support ARM v6/v7, so it cannot share the
same goarch matrix as non-Windows builds.
- Add defaultWindowsGoarch [amd64, arm64] and almostAllWindowsGoarch
[386, amd64, arm64] for the Windows-specific architecture lists.
- Promote defaultGoamd64 from [v1, v2] to [v1, v2, v3, v4] so the
full amd64 micro-architecture matrix is always generated.
- Update the archive name_template to include
{{ if .Amd64 }}_{{ .Amd64 }}{{ end }} after x86_64.
- Include both <name> and <name>-windows in archive ids.
- Regenerate tools/monorel/.goreleaser.yaml and
cmd/sql-migrate/.goreleaser.yaml with the new format.
Default matrix (conservative, CGO_ENABLED=0):
goos: darwin freebsd js linux netbsd openbsd wasip1 windows
goarch: amd64 arm arm64 mips64le mipsle ppc64le riscv64 wasm
goarm: 6 7 (always included when arm is in goarch)
--almost-all widens to esoteric goos (aix dragonfly illumos plan9
solaris), adds 386/loong64/mips/mips64/ppc64/s390x to goarch, and
emits goamd64: v1 v2 v3 v4.
--ios generates an active iOS build entry (CGO_ENABLED=1, arm64)
instead of the default commented stub.
--android-ndk generates an active Android NDK build entry
(CGO_ENABLED=1, arm64) instead of the default commented stub.
Both --ios and --android-ndk are available on init and release.
The existing -A flag (include hidden dirs) is unchanged.
When a module has more than one binary, the shared build options (env,
ldflags, goos) are defined once via a YAML anchor on the first build and
merged into the rest with <<: *build_defaults. Single-binary modules use
plain fields with no anchor overhead.
- id: gsheet2csv - id: gsheet2csv
binary: gsheet2csv binary: gsheet2csv
env: → <<: &build_defaults
- CGO_ENABLED=0 env:
goos: - CGO_ENABLED=0
- aix goos:
- ... - aix
- id: gsheet2env - ...
binary: gsheet2env - id: gsheet2env
env: binary: gsheet2env
- CGO_ENABLED=0 <<: *build_defaults
goos: ...
The commented-out ios stubs follow the same pattern using a separate
build_defaults_ios anchor so they remain consistent when uncommented.
Also extracts defaultGoos to a package-level var to avoid repetition.
Replace linux/windows/darwin with the full CGO_ENABLED=0 goos list:
aix, darwin, dragonfly, freebsd, illumos, js, linux, netbsd, openbsd,
plan9, solaris, wasip1, windows
Add commented-out stanzas for each binary for platforms that require extra
tooling:
- iOS (CGO_ENABLED=1, Xcode toolchain required)
- Android (CGO_ENABLED=0 arm64-only; NDK required for full CGO builds)
Archive formats unchanged (tar.gz + tar.zst / zip + tar.gz for Windows).
release subcommand:
- Replace yamlLooksCorrect with yamlIsCompatible: file is considered OK if
{{ .ProjectName }} is absent AND at least one binary's VERSION string is
present. Extra hand-edited binaries (like fixtures) no longer trigger a
rewrite.
- Before overwriting an existing file, prompt the user [Y/n]. --yes does
not skip this prompt; --force does. If stdin is not a terminal and
--force is not set, the command errors rather than silently clobbering.
init subcommand: unchanged — still uses the strict yamlLooksCorrect check
(all binaries must be present, ldflags must include main.version).
gh release create now always uses --draft --prerelease so artifacts can be
uploaded before the release becomes visible. A final "Finalise release
visibility" step then runs gh release edit to remove whichever flags should
not remain:
--draft=false unless --draft flag given (keep as draft)
--prerelease=false unless --prerelease flag given OR the tag has a
pre-release suffix (-pre3, .dirty, etc.)
Flag meaning change:
--draft = keep release in draft state after uploading (don't publish)
--prerelease = keep release marked pre-release even for clean vX.Y.Z tags
The gh release create flags are now explicit and always emitted:
--draft / --draft=false
--prerelease / --prerelease=false
Publish step (gh release edit --draft=false) logic:
--draft --prerelease : NO (stays as draft pre-release)
--draft only : YES (draft needs publishing)
--prerelease only : NO (published immediately as pre-release)
neither : NO (published immediately)
The step prompt adapts: "create draft pre-release / draft / pre-release /
GitHub release <tag>" depending on the flag combination.
Replace printModuleScript (which emitted a bash script to stdout) with an
interactive step runner for the release subcommand:
- Each step shows the command(s) it will run, then prompts [Y/n]
- --dry-run: show steps without prompting or running (replaces old default)
- --yes: run all steps without prompting (happy-path automation)
New types/functions:
releaseStep — title, prompt, display lines, skip flag, run func
printModuleHeader — extracted header/info block (always shown)
buildModuleSteps — constructs the ordered step list for one module
runSteps — executes steps per dryRun/yes flags
execIn — runs a command streaming to the terminal
execInEnv — like execIn with extra environment variables
Goreleaser archive globs are expanded at step-run time (after goreleaser
has built the dist/ directory) rather than being passed as shell globs.
The gh release create --notes flag receives the notes string directly
instead of via a shell variable.
When an existing .goreleaser.yaml passes yamlLooksCorrect() and needs no
update, both init and release now print:
found config ./cmd/tcpfwd/.goreleaser.yaml with monorepo support
Path is shown relative to cwd (via cwdRelPath helper).
- Replace "Generated by monorel" header with goreleaser-style preamble
("This file is generated by monorel ... check the docs at goreleaser.com")
- Add modeline explainer comment block before the yaml-language-server line
- Add "you may remove this if you don't need go generate" comment inline
- Combine name_template comments into goreleaser-style single comment
("this name template makes the OS and Arch compatible with uname")
Replace the exact-content comparison with yamlLooksCorrect(), which checks
three semantic markers:
- -X main.version={{.Env.VERSION}} present (version injection)
- <binname>_{{ .Env.VERSION }}_ present for every binary (archive naming)
- {{ .ProjectName }} / {{.ProjectName}} absent (not a stock template)
If the file passes all checks it is left completely untouched, preserving
any compatible local edits. Only files that fail a check are regenerated.
Both init (initModuleGroup) and release (processModule) now compare the
generated content against the file on disk before writing. If identical,
the file is left untouched — preserving any compatible local edits.
Also fix processModule commit message scope to chore(<prefix>): add
.goreleaser.yaml, matching the convention already used by initModuleGroup.
- Track isNewFile before writing so updates to existing files are never
auto-committed (mirrors the same rule already in processModule/release)
- Gate auto-bump on isNewFile as well — no tag when just refreshing yaml
- Fix commit message scope: chore(<prefix>): add .goreleaser.yaml
(was chore(release): add .goreleaser.yaml for <name>)
The go.mod boundary check added to findMainPackages was stopping the
--recursive walk from descending into child module directories, so
modules like auth/csvauth/ were never found.
The check was redundant: groupByModule already uses findModuleRoot to
attribute each binary to its nearest go.mod ancestor, and initModuleGroup
already skips modules with uncommitted changes (an untracked go.mod
counts as an uncommitted change). Removing the stop restores the
pre-regression behaviour.
Add Step 3 "git push && git push --tags" to the generated release script,
between the tag creation step and the goreleaser build step. This fixes
HTTP 422 "Release.target_commitish is invalid" errors from the GitHub API,
which occur when the local commit (auto-created for a new .goreleaser.yaml)
has not yet been pushed to the remote.
If .goreleaser.yaml did not exist:
→ write it, commit it, auto-tag patch (if sole new commit since last tag)
If .goreleaser.yaml already existed:
→ write the updated file, stop — no auto-commit, no auto-tag
monorel release now mirrors the init lifecycle for the goreleaser config:
1. Write .goreleaser.yaml (always regenerate; warn on stock {{ .ProjectName }})
2. git add + commit it if the file changed
3. Auto-tag patch — but only if the yaml commit is the sole new commit
since the last stable tag (same heuristic as monorel init).
If there are >1 new commits, print a note and skip auto-tag so the
user can choose the right semver component with monorel bump.
4. Compute version info *after* the yaml commit and auto-tag, so the
generated script reflects the tag that was just created.
The previous commit was too conservative — it prevented release from
updating an existing config. Restore the original write-always behaviour
but keep the {{ .ProjectName }} monorepo warning. The file is written
unconditionally; the user commits it manually when satisfied.
Previously `monorel release` always overwrote .goreleaser.yaml, discarding
any manual customisations the user had made.
New behaviour:
- Missing → write the monorel-generated config and commit it (same as init)
- Exists, stock {{ .ProjectName }} in a monorepo subdir → warn and ask the
user to update it manually before re-running
- Exists, looks fine → print "leaving unchanged" and continue
Instead of emitting a runtime git log command in Step 4, run git log at
monorel-invocation time and embed the actual commit list as a
single-quoted bash string (with '\'' escaping for embedded apostrophes).
my_release_notes='- abc1234 first commit
- def5678 fix: don'\''t crash'
This makes the generated script self-contained and shows the user exactly
which commits will appear in the release notes before they run the script.
Removes the now-unused gitPathSpec local variable.
- findMainPackages: stop descending into child directories that have their
own go.mod on disk (even if untracked), treating them as independent
module roots so they are never double-counted with the parent module
- initModuleGroup: skip if the module has uncommitted changes (excluding
files inside child module directories so a freshly-run --cmd step does
not block the parent); new helpers hasUncommittedChanges,
findChildModuleRoots
- monorel init --cmd: scans recursively for direct children of cmd/
directories that contain package main but have no go.mod yet, runs
go mod init + tidy for each, and prints a suggested commit at the end;
new helpers initCmdModules, readModulePath, runPrintIn
Add buildTrackedDirs which runs "git ls-files" once from the walk root
and builds a set of directories containing at least one tracked file.
findMainPackages uses this set to skip untracked directories (dist/,
vendor/, node_modules/, build artifacts, etc.) without having to
enumerate git-ignored paths explicitly.
Falls back to walking everything when git is unavailable or the
directory is not inside a repository.
- All three subcommands now print "found binary …" and "found module …"
before processing each module group, with a blank line between groups
- initModuleGroup, bumpModuleTag, processModule: downgrade the prefix==""
(repository root) case from fatal error to a skip warning so that
-recursive runs continue past root-level go.mod packages instead of
aborting
Extract findLatestStableTag helper to avoid duplicating tag-scan logic.
In initModuleGroup, count commits since the last stable tag after writing
.goreleaser.yaml. Only auto-bump when ≤1 new commit exists (i.e. the
goreleaser.yaml addition is the sole change). When more commits are present,
print a note and let the user run 'monorel bump' explicitly with the right
semver component.
- Same-commit guard: downgrade from fatal error to a skip warning so
-recursive bump continues to the next module instead of aborting
- bump/init: add -dry-run flag; prints what would happen without making
any git commits or tags
- findMainPackages: skip dot/underscore-prefixed dirs by default; warn
(not error) on ReadDir failures when -A is set
- expandPaths, runRelease, runBump, runInit: thread -A flag through
- bumpModuleTag: refuse to tag a commit already tagged by the previous
stable release; -force creates an empty bump commit instead
-recursive flag (all three subcommands)
expandPaths() / findMainPackages() walk the directory tree looking for
`package main` directories, honouring stopMarkers (skips .git dirs so
the walk never crosses a repository boundary). Passes collected paths
straight to groupByModule, which handles module grouping as before.
monorel init -recursive . # init every module under .
monorel bump -recursive . # bump patch for every module
monorel release -recursive . # release script for every module
Remove GORELEASER_CURRENT_TAG from generated release script
The non-pro goreleaser does not support it. VERSION is still exported
for use via {{.Env.VERSION}} in the goreleaser YAML templates.
Indent the goreleaser subshell in the release script
Before: ( cd "auth/csvauth" && goreleaser release ... )
After:
(
cd "auth/csvauth"
goreleaser release --clean --skip=validate,announce
)
In a monorepo the module's most recent commit is often behind HEAD
(other modules may have been committed on top).
git log --format=%H -1 -- .
run from the module root returns the SHA of the last commit that
touched that directory; we pass it explicitly to `git tag <tag> <sha>`
instead of letting git default to HEAD.