edikt-core 0.3.0

edikt core: value model, Feature capabilities, the expression language, and the Document/Convert seams.
Documentation
# edikt

[![CI](https://github.com/jhheider/edikt/actions/workflows/ci.yml/badge.svg)](https://github.com/jhheider/edikt/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/jhheider/edikt/badge.svg?branch=main)](https://coveralls.io/github/jhheider/edikt?branch=main)
[![crates.io](https://img.shields.io/crates/v/edikt.svg)](https://crates.io/crates/edikt)
[![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

**Edit config files without reflowing them.**

You want to change one value in a commented, hand-formatted config. Today every
option costs you something:

```console
$ jq '.compilerOptions.target = "ES2022"' tsconfig.json
# comments gone, trailing commas gone, the whole file reindented,
# and JSONC-with-comments won't round-trip through jq at all
```

edikt changes the one value and nothing else:

```console
$ edikt -i '.compilerOptions.target = "ES2022"' tsconfig.json
# one line of the diff moves; every comment, blank line, indent, and
# trailing comma comes back byte for byte
```

*edit, meets edict.* A lossless, format-preserving editor for **JSONC/JSON5**,
**TOML**, **YAML**, **INI**, **KDL**, and flat key-value files (**`.env`**,
**`.properties`**), driven by a jq-flavored expression language and a sed-shaped
execution model. It touches only the bytes you point at.

```sh
# query - reads like jq
edikt '.compilerOptions.strict' tsconfig.json

# edit in place - comments, indent, comma style all preserved
edikt -i '.compilerOptions.target = "ES2022"' tsconfig.json

# edit YAML in place - anchors, flow style, and comments survive
edikt -i '.services.web.replicas = 3' compose.yaml

# compute, not just place
edikt -i '.version |= . + "-dev"' package.jsonc

# stream-first, like sed (stdin has no extension, so name the format)
cat settings.jsonc | edikt -t jsonc 'del(.telemetry) | .theme = "dark"'

# script from a file
edikt -f release.edk -i config.jsonc

# convert, where feasible - comments carried across, in the target's syntax
edikt -T yaml tsconfig.jsonc

# Markdown frontmatter - edit the metadata block, the prose untouched
edikt -i '.draft = false' post.md

# multi-document YAML - one edit maps over every ---document in the stream
edikt -i '.metadata.labels.env = "prod"' k8s.yaml

# ...or target one document by position with ^dN (0-based, strict)
edikt -i '^d1 | .spec.replicas = 3' k8s.yaml

# PEP 723 / scriptbox blocks - bump the pin, the code below untouched
edikt -t frontmatter -i '.["requires-python"] = ">=3.12"' app.py
```

Every one of these writes back a file that is byte-identical except for the
value you changed, the kind of diff a reviewer reads at a glance.

## Beyond config files: frontmatter and streams

**Markdown frontmatter.** For a `.md`/`.markdown`/`.mdx`/`.qmd` file (or
`-t markdown`), edikt edits the leading metadata block, YAML (`---`), TOML
(`+++`), tagged (`---json`), or Hugo bare-brace JSON, and leaves the document
body byte-for-byte opaque. `edikt '.title' post.md` queries it;
`edikt -i '.draft = false' post.md` rewrites one key with the prose intact.

**Commented host-language blocks.** edikt reads PEP 723 `# /// script` ... `# ///`
blocks in a Python file (uv) or a shell script (scriptbox), bumps the pin, and
re-applies the `# ` prefix with the code below untouched.

**Multi-document YAML.** A `---`-separated stream (Kubernetes manifests, Ansible,
Helm output) is first-class. An edit maps over every document by default and
silently skips any that lack the target path; a query returns one result per
document. `select(pred)` targets documents by content
(`select(.kind == "Service") | .spec.type = "LoadBalancer"`), and `^dN` selects
one by position (`^d0` is the first), strict there, so `^d5` on a
three-document stream is an error, not a no-op.

## Install

```sh
brew install jhheider/tap/edikt   # Homebrew
cargo install edikt               # Cargo (crates.io)
pkgx install edikt                # pkgx (builds from source)
```

Or grab a prebuilt binary (Linux, macOS, and Windows; x86_64 and arm64) from the
[releases page](https://github.com/jhheider/edikt/releases).

## A worked example: bumping a Cargo workspace

edikt is happy editing the config it ships in. Here is the whole version bump
for a release, a `Cargo.toml` per crate plus the internal dependency pins in
the workspace root, done losslessly:

```sh
# each crate's own version
for c in crates/*/Cargo.toml; do
  edikt -i '.package.version = "1.2.3"' "$c"
done

# the [workspace.dependencies] pins (inline tables), chained with -e
edikt -i \
  -e '.workspace.dependencies."edikt-core".version   = "1.2.3"' \
  -e '.workspace.dependencies."edikt-syntax".version = "1.2.3"' \
  Cargo.toml
```

Each edit moves exactly one `version = "..."`. The `# Internal crates` comment
above the dependency table, the `path = "crates/edikt-core"` sitting next to
each version, the brace-and-space style of every inline table, and every other
byte come back unchanged, the kind of diff a reviewer reads at a glance.

One quoting note: a key containing `-` is written `."edikt-core"` (or
`.["edikt-core"]`), because a bare `.edikt-core` would read the `-` as
subtraction. edikt says so with a clear error rather than guessing.

## Why edikt?

Honestly, a weekend project, one that started with a real itch. The problem
crystallized while reading [*"Respectful" YAML Patching in
Rust*](https://verrchu.github.io/blog/2-respectful-yaml-patching-in-rust/),
which surveys the Rust libraries for patching YAML and lands on the gap in one
line: **none of them preserve both** the formatting *and* the comments. That's
the exact thing I kept wanting, surgically change one value (or one comment)
and leave every other byte, comment, and blank line alone, and not just for
YAML but for the whole pile of config formats a project accumulates.

The good tools each own their corner: `jq` and `yq` for querying, `taplo` and
`prettier` for formatting, and the excellent `toml_edit` and `kdl-rs` crates for
lossless edits (edikt is *built on* those last two). edikt isn't trying to
replace them, it's the piece I couldn't find off the shelf: one jq-flavored
tool that edits **and** queries **and** converts across JSONC, INI, TOML, YAML,
KDL, and `.env`, touching only the bytes you point at. If your need is
single-format, reach for the specialist; if it's "the same surgical edit, across
all of these," that's the gap this fills.

## What edikt won't do

It edits values and comments; it does not reformat, lint, or validate schemas,
reach for `taplo`/`prettier` for that. The expression language is a curated
subset of jq (navigation, mutation, arithmetic, `//` defaults, regex,
`split`/`join`); `if/then`, `reduce`, variables (`as $x`), and user-defined
functions are not in v1. And `.env` is flat and string-valued, always. Stating
the boundaries plainly is the point: inside them, the surgical-edit promise
holds byte for byte.

## Status

Seven config formats with lossless in-place edit, query, and comment-preserving
conversion, plus a frontmatter lens (Markdown and PEP 723 host-language blocks)
and multi-document YAML streams with `select`/`^dN` targeting. On crates.io,
Homebrew, and pkgx, the badge above tracks the current version. See
[`CLAUDE.md`](./CLAUDE.md) for the build contract.

## Scripting notes

- **Exit codes are sed-shaped:** `0` = success, a query that matches nothing
  is a *silent no-op* (safe under `set -e`); `2` = parse or evaluation error.
  For presence tests, `--exit-status` opts into jq's `1` on zero matches; for
  defaults, use `//`: `edikt '.maybe.key // "fallback"' f.yaml`.
- **The expression language is deliberately capped in v1.** jq's navigation,
  mutation, arithmetic, `//` defaults, and a curated builtin registry
  (including regex `test`/`match`/`capture`/`sub`/`gsub`, `split`/`join`) are
  in; variables (`as $x`), `if/then`, `reduce`, and user-defined functions are
  not (yet).
- **Many files, sed-style:** `edikt -i '.v = 9' a.json b.json` (let the shell
  glob: `edikt -i 'del(.telemetry)' config/*.jsonc`). Queries over several
  files concatenate results in order.
- **`.env` is flat and string-valued**, no arrays or nesting, ever, but
  string computation on values works fine:
  `edikt -i '.VERSION |= sub("^v"; "")' .env`.

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT), at your option.