@udondan/avanti 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +488 -64
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +82 -18
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/lock.d.ts.map +1 -1
- package/dist/commands/lock.js +6 -4
- package/dist/commands/lock.js.map +1 -1
- package/dist/commands/log.d.ts.map +1 -1
- package/dist/commands/log.js +7 -5
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +682 -42
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/reset.d.ts.map +1 -1
- package/dist/commands/reset.js +43 -11
- package/dist/commands/reset.js.map +1 -1
- package/dist/commands/revert.d.ts.map +1 -1
- package/dist/commands/revert.js +68 -14
- package/dist/commands/revert.js.map +1 -1
- package/dist/condition.d.ts.map +1 -1
- package/dist/condition.js +9 -6
- package/dist/condition.js.map +1 -1
- package/dist/config-writeback.d.ts.map +1 -1
- package/dist/config-writeback.js +17 -0
- package/dist/config-writeback.js.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +346 -3
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +19 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +317 -12
- package/dist/diff.js.map +1 -1
- package/dist/extract.d.ts +4 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +142 -0
- package/dist/extract.js.map +1 -0
- package/dist/filter.d.ts +3 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +126 -0
- package/dist/filter.js.map +1 -0
- package/dist/history.d.ts +7 -1
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +89 -9
- package/dist/history.js.map +1 -1
- package/dist/paths.d.ts +4 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +97 -0
- package/dist/paths.js.map +1 -1
- package/dist/processors/ini.d.ts +33 -0
- package/dist/processors/ini.d.ts.map +1 -0
- package/dist/processors/ini.js +500 -0
- package/dist/processors/ini.js.map +1 -0
- package/dist/processors/insert.d.ts.map +1 -1
- package/dist/processors/insert.js +338 -15
- package/dist/processors/insert.js.map +1 -1
- package/dist/processors/on.d.ts +4 -0
- package/dist/processors/on.d.ts.map +1 -0
- package/dist/processors/on.js +54 -0
- package/dist/processors/on.js.map +1 -0
- package/dist/ref.d.ts +21 -0
- package/dist/ref.d.ts.map +1 -0
- package/dist/ref.js +65 -0
- package/dist/ref.js.map +1 -0
- package/dist/sources/bitbucket.d.ts.map +1 -1
- package/dist/sources/bitbucket.js +51 -12
- package/dist/sources/bitbucket.js.map +1 -1
- package/dist/sources/git.d.ts.map +1 -1
- package/dist/sources/git.js +54 -6
- package/dist/sources/git.js.map +1 -1
- package/dist/sources/github.d.ts.map +1 -1
- package/dist/sources/github.js +188 -51
- package/dist/sources/github.js.map +1 -1
- package/dist/sources/gitlab.d.ts.map +1 -1
- package/dist/sources/gitlab.js +242 -44
- package/dist/sources/gitlab.js.map +1 -1
- package/dist/sources/index.d.ts +4 -2
- package/dist/sources/index.d.ts.map +1 -1
- package/dist/sources/index.js +220 -49
- package/dist/sources/index.js.map +1 -1
- package/dist/sources/local.d.ts +3 -0
- package/dist/sources/local.d.ts.map +1 -1
- package/dist/sources/local.js +44 -0
- package/dist/sources/local.js.map +1 -1
- package/dist/types.d.ts +38 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/variables-remote.d.ts +1 -1
- package/dist/variables-remote.d.ts.map +1 -1
- package/dist/variables-remote.js +4 -3
- package/dist/variables-remote.js.map +1 -1
- package/dist/variables.d.ts +1 -0
- package/dist/variables.d.ts.map +1 -1
- package/dist/variables.js +37 -2
- package/dist/variables.js.map +1 -1
- package/dist/writer.d.ts +17 -0
- package/dist/writer.d.ts.map +1 -1
- package/dist/writer.js +848 -20
- package/dist/writer.js.map +1 -1
- package/package.json +14 -10
package/README.md
CHANGED
|
@@ -16,28 +16,45 @@ atomic rollbacks, and diff-before-apply safety.
|
|
|
16
16
|
- [`avanti diff`](#avanti-diff)
|
|
17
17
|
- [`avanti pull`](#avanti-pull)
|
|
18
18
|
- [`avanti lock`](#avanti-lock)
|
|
19
|
+
- [`--verbose` / `-v`](#--verbose---v)
|
|
19
20
|
- [History](#history)
|
|
20
21
|
- [`avanti log`](#avanti-log)
|
|
21
22
|
- [`avanti diff <pullId>`](#avanti-diff-pullid)
|
|
22
23
|
- [`avanti revert [pullId]`](#avanti-revert-pullid)
|
|
23
24
|
- [`avanti reset`](#avanti-reset)
|
|
24
|
-
- [`--verbose` / `-v`](#--verbose---v)
|
|
25
25
|
- [Working Directory](#working-directory)
|
|
26
26
|
- [Path Constraints](#path-constraints)
|
|
27
27
|
- [Configuration](#configuration)
|
|
28
28
|
- [File Entry Fields](#file-entry-fields)
|
|
29
29
|
- [Source Types](#source-types)
|
|
30
|
+
- [SHA pinning](#sha-pinning)
|
|
31
|
+
- [Filter](#filter)
|
|
32
|
+
- [Extract](#extract)
|
|
30
33
|
- [Directory Sources](#directory-sources)
|
|
31
34
|
- [JSON Merging](#json-merging)
|
|
32
35
|
- [YAML Merging](#yaml-merging)
|
|
33
36
|
- [TOML Merging](#toml-merging)
|
|
37
|
+
- [INI Merging](#ini-merging)
|
|
34
38
|
- [Template Rendering](#template-rendering)
|
|
39
|
+
- [Event Hooks](#event-hooks)
|
|
35
40
|
- [Insert Mode](#insert-mode)
|
|
36
41
|
- [Conditions](#conditions)
|
|
42
|
+
- [Condition fields](#condition-fields)
|
|
43
|
+
- [Examples](#examples)
|
|
37
44
|
- [Scaffold Pattern](#scaffold-pattern)
|
|
38
45
|
- [Backup](#backup)
|
|
46
|
+
- [Path variables](#path-variables)
|
|
47
|
+
- [Counter pattern](#counter-pattern)
|
|
48
|
+
- [Security: backup_roots](#security-backup_roots)
|
|
49
|
+
- [Backup examples](#backup-examples)
|
|
39
50
|
- [Write in Place](#write-in-place)
|
|
51
|
+
- [Follow Symlink](#follow-symlink)
|
|
52
|
+
- [Sudo](#sudo)
|
|
40
53
|
- [Variables](#variables)
|
|
54
|
+
- [List and object variables](#list-and-object-variables)
|
|
55
|
+
- [Accessing nested values with \${expr}](#accessing-nested-values-with-expr)
|
|
56
|
+
- [Source-based variables](#source-based-variables)
|
|
57
|
+
- [System-injected variables](#system-injected-variables)
|
|
41
58
|
- [$self — Self-managing Config](#self--self-managing-config)
|
|
42
59
|
- [Authentication](#authentication)
|
|
43
60
|
- [Private Instances](#private-instances)
|
|
@@ -105,9 +122,10 @@ avanti revert # roll back instantly if something breaks
|
|
|
105
122
|
- **JSON merging** — deep-merge multiple JSON/JSONC sources with configurable conflict, array, and object strategies; format output with configurable indentation, trailing commas, key sorting, minification, and comment stripping
|
|
106
123
|
- **YAML merging** — deep-merge multiple YAML/YML sources with the same strategies, with full comment preservation
|
|
107
124
|
- **TOML merging** — deep-merge multiple TOML sources with configurable conflict, array, and table strategies
|
|
125
|
+
- **INI merging** — deep-merge multiple INI/CFG sources with the same strategies, with full comment and key-order preservation
|
|
108
126
|
- **Variables** — define reusable values in a `variables:` block and reference them anywhere with `$name`; variables can be plain strings, `$env:NAME` environment variable references, or fetched from any remote/local source (the same source types as `files:`)
|
|
109
127
|
- **Post-processing** — apply text replacements (string or regex) and/or pipe content through a shell script
|
|
110
|
-
- **Release artifacts** — download release assets attached to a GitHub or GitLab release by tag,
|
|
128
|
+
- **Release artifacts** — download release assets attached to a GitHub or GitLab release by tag, `$latest` (newest stable semver tag), `$recent` (most recently created/published tag), or `/pattern/[flags]` (GitLab prefers `package`-type links; falls back to all links)
|
|
111
129
|
- **Directory sync** — recursively sync directories from GitLab/GitHub/Bitbucket/git/S3/local sources
|
|
112
130
|
- **SHA pinning** — pin any remote source to a content fingerprint with `sha:`; use `avanti lock` to compute and write SHAs automatically; `avanti pull --accept-changes` reviews a mismatch and updates the pin
|
|
113
131
|
- **`$self`** — avanti can manage its own config file; declare `$self` in `files:` and the fetched content becomes the active config for the rest of the run, including YAML/JSON merge from multiple sources
|
|
@@ -188,10 +206,30 @@ Run `avanti pull --accept-changes` to review the diff and update SHA values.
|
|
|
188
206
|
|
|
189
207
|
`avanti diff` shows a `⚠ SHA mismatch` warning inline for any source that no longer matches its pinned SHA.
|
|
190
208
|
|
|
191
|
-
SHA is computed over the raw fetched content of each source, before any `replace` or `
|
|
209
|
+
SHA is computed over the raw fetched content of each source, before any `replace` or `on.write` processing. Each file's path and content are fed into the hash in sorted order, separated by null bytes — so renames and additions affect the fingerprint even for single-file sources. Pull history records the observed SHA for every source, so `avanti log` shows a full audit trail of what changed and when.
|
|
192
210
|
|
|
193
211
|
Excluded from SHA pinning: local paths and `raw:` sources (their content is either authored locally or inline in the config, so changes are always visible).
|
|
194
212
|
|
|
213
|
+
### `--verbose` / `-v`
|
|
214
|
+
|
|
215
|
+
Pass `--verbose` (or `-v`) to any command to print internal debug details to stderr. Verbose output does not appear on stdout, so piping diff output is unaffected.
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
avanti diff --verbose
|
|
219
|
+
avanti pull -v
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Each line is prefixed with `[verbose]` and includes:
|
|
223
|
+
|
|
224
|
+
- The source being fetched (e.g. `github:org/repo:file@main`)
|
|
225
|
+
- Every HTTP request URL and response status code
|
|
226
|
+
- Retry delays and reasons
|
|
227
|
+
- CLI tool invocations (`gh`, `glab`, `vault`, `git`)
|
|
228
|
+
- AWS SDK API calls (`s3 GetObject`, `ssm GetParameter`, `secrets-manager GetSecretValue`)
|
|
229
|
+
- Cache hits
|
|
230
|
+
|
|
231
|
+
**Credential safety:** tokens are read from environment variables and sent as HTTP headers, which are never logged. Git URLs with embedded credentials are redacted. `exec:` source commands are logged verbatim — if your config embeds secrets in an exec command (e.g. `exec: curl -H "Token: $env:MY_SECRET"`), those secrets will appear in verbose output after variable substitution.
|
|
232
|
+
|
|
195
233
|
## History
|
|
196
234
|
|
|
197
235
|
Every successful `avanti pull` that writes at least one file is recorded in a local history store. This lets you inspect what changed, preview past states, revert the whole project, or fully undo all avanti changes.
|
|
@@ -287,30 +325,34 @@ Apply? [y/N]
|
|
|
287
325
|
|
|
288
326
|
Use `--yes` to skip the prompt. The history log is preserved — you can still run `avanti log` after a reset.
|
|
289
327
|
|
|
290
|
-
|
|
328
|
+
## Working Directory
|
|
291
329
|
|
|
292
|
-
|
|
330
|
+
Relative `src` and `target` paths are resolved against different bases:
|
|
293
331
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
avanti pull -v
|
|
297
|
-
```
|
|
332
|
+
- **`target` paths** (map keys) — resolved relative to the **working directory** (where you invoke `avanti`, or the path given with `-w`). This controls where pulled files land on disk.
|
|
333
|
+
- **`src` paths** (plain string, for fetching content) — resolved relative to the **config file's location**. If the config is a local file, relative sources resolve relative to its directory. If the config is remote (GitHub, GitLab, HTTPS, `git+ssh://`), relative plain-string sources resolve to the same remote location. **Exception:** when `symlink:` is set, `src` is the symlink target path (always a local filesystem path) and resolves against the working directory — it is never config-relative. `path:` object sources also always resolve relative to the working directory.
|
|
298
334
|
|
|
299
|
-
|
|
335
|
+
This means a config at `./configs/avanti.yml` can use `src: ./templates/foo.sh` to reference `./configs/templates/foo.sh`, regardless of what working directory you pass with `-w`.
|
|
300
336
|
|
|
301
|
-
|
|
302
|
-
- Every HTTP request URL and response status code
|
|
303
|
-
- Retry delays and reasons
|
|
304
|
-
- CLI tool invocations (`gh`, `glab`, `aws`, `vault`, `git`)
|
|
305
|
-
- Cache hits
|
|
337
|
+
For remote configs, relative source paths are resolved within the same remote context:
|
|
306
338
|
|
|
307
|
-
|
|
339
|
+
```yaml
|
|
340
|
+
# config loaded from github:owner/repo:configs/avanti.yml
|
|
341
|
+
files:
|
|
342
|
+
dist/script.sh:
|
|
343
|
+
src: ./scripts/build.sh # fetches github:owner/repo:configs/scripts/build.sh
|
|
344
|
+
```
|
|
308
345
|
|
|
309
|
-
|
|
346
|
+
The `path:` object source always refers to the local filesystem and its relative paths resolve against the working directory, regardless of whether the config file is local or remote.
|
|
310
347
|
|
|
311
|
-
|
|
348
|
+
This is independent of where the config file lives only for targets. A config loaded from another location with `-c /shared/avanti.yml` writes target files into your working directory but reads sources from `/shared/`.
|
|
312
349
|
|
|
313
|
-
|
|
350
|
+
The path given to `-w` supports tilde expansion: `~` resolves to the home directory and `~/some/path` resolves to a subdirectory of it:
|
|
351
|
+
|
|
352
|
+
```sh
|
|
353
|
+
avanti -w ~ pull # home directory as working dir
|
|
354
|
+
avanti -w ~/projects/foo pull # subdirectory of home
|
|
355
|
+
```
|
|
314
356
|
|
|
315
357
|
Use `-w` to deploy the same config to multiple locations without `cd`-ing there first:
|
|
316
358
|
|
|
@@ -360,7 +402,8 @@ files:
|
|
|
360
402
|
some-file.yml:
|
|
361
403
|
src:
|
|
362
404
|
exec: glab api "projects/group%2Fproject/repository/files/some-file.yaml/raw?ref=main"
|
|
363
|
-
|
|
405
|
+
on:
|
|
406
|
+
write: sed -e 's/v3/v4/g'
|
|
364
407
|
|
|
365
408
|
renovate.json:
|
|
366
409
|
src:
|
|
@@ -407,30 +450,44 @@ A brace group is only expanded when it contains **at least one comma** (e.g. `{f
|
|
|
407
450
|
|
|
408
451
|
> **YAML quoting:** YAML treats `{` at the start of a plain key as a flow mapping. If the brace group is the first character of a key, quote it: `'{dev,prod}.yml':` or `"{dev,prod}.yml":`. Keys where the brace group appears after a path prefix (e.g. `config/{dev,prod}.yml`) do not need quoting.
|
|
409
452
|
|
|
410
|
-
| Field
|
|
411
|
-
|
|
|
412
|
-
| `src`
|
|
413
|
-
| `if`
|
|
414
|
-
| `ifAny`
|
|
415
|
-
| `mode`
|
|
416
|
-
| `
|
|
417
|
-
| `
|
|
418
|
-
| `
|
|
419
|
-
| `
|
|
420
|
-
| `
|
|
421
|
-
| `
|
|
422
|
-
| `
|
|
453
|
+
| Field | Required | Description |
|
|
454
|
+
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
455
|
+
| `src` | Yes | Source (see below). May be a single source or a **list** of sources to concatenate. |
|
|
456
|
+
| `if` | No | Condition object (or list of objects). All must pass for the entry to be processed. See [Conditions](#conditions). |
|
|
457
|
+
| `ifAny` | No | List of condition objects. At least one must pass. See [Conditions](#conditions). |
|
|
458
|
+
| `mode` | No | File permission mode. Use a quoted octal string (`"0755"`) or a YAML octal literal (`0o755`). Mode-only changes (content unchanged) are detected by `diff` and applied by `pull`. **POSIX only** — ignored on Windows. |
|
|
459
|
+
| `backup` | No | Path to copy the current file to before overwriting it. Supports path variables (`$dirname`, `$filename`, `$datetime`) and the `%d+` counter token for auto-incrementing slots. See [Backup](#backup). |
|
|
460
|
+
| `replace` | No | List of `{from, to}` replacement rules. `from` may be a plain string or `/pattern/flags` regex. |
|
|
461
|
+
| `on` | No | Lifecycle event hooks. See [Event Hooks](#event-hooks). |
|
|
462
|
+
| `template` | No | Treat the fetched content as a template and render it with avanti config variables as context. See [Template Rendering](#template-rendering). |
|
|
463
|
+
| `json` | No | JSON merge/format options (see below). When omitted, merging is auto-enabled if all sources have a `.json` or `.jsonc` extension. Use `true`/`false` to force on or off regardless of extension. |
|
|
464
|
+
| `yaml` | No | YAML merge/format options (see below). When omitted, merging is auto-enabled if all sources have a `.yaml` or `.yml` extension. Use `true`/`false` to force on or off regardless of extension. Comments are preserved in merged output. |
|
|
465
|
+
| `toml` | No | TOML merge/format options (see below). When omitted, merging is auto-enabled if all sources have a `.toml` extension. Use `true`/`false` to force on or off regardless of extension. See [TOML Merging](#toml-merging). |
|
|
466
|
+
| `ini` | No | INI merge/format options (see below). When omitted, merging is auto-enabled if all sources have a `.ini` or `.cfg` extension. Use `true`/`false` to force on or off regardless of extension. Comments and key order are preserved. See [INI Merging](#ini-merging). |
|
|
467
|
+
| `strategy` | No | Write strategy: `replace` _(default)_ — overwrite the target file entirely; `insert` — merge content into the existing file without clobbering unrelated content. See [Insert Mode](#insert-mode). |
|
|
468
|
+
| `writeInPlace` | No | If `true`, replaces file content in-place instead of using an atomic rename. Preserves the existing inode. **Not atomic** — use only when inode stability is required. Errors if the target is a symlink. See [Write in Place](#write-in-place). |
|
|
469
|
+
| `followSymlink` | No | If `true` and the target path is a symlink, writes the fetched content to the **symlink's target** instead of replacing the symlink itself. The resolved target must not be a directory and must stay inside the working directory. See [Follow Symlink](#follow-symlink). |
|
|
470
|
+
| `symlink` | No | Create a symlink at the target path instead of writing file content. `src` must be a single local path. Use `true` or `"absolute"` to create an absolute symlink; use `"relative"` to express the symlink target as a path relative to the symlink's parent directory. Cannot be combined with `replace`, `template`, `json`, `yaml`, `toml`, `ini`, `on.write`, `extract`, `writeInPlace`, `strategy`, `followSymlink`, `mode`, or a list `src`. See [Symlink](#symlink). |
|
|
471
|
+
| `extract` | No | Unpack an archive (`.zip`, `.tar`, `.tar.gz`, `.tgz`) downloaded from a single-file source before writing. Target must end with `"/"`. Use `true` to extract all files, or a list of patterns to extract only matching entries. Cannot be combined with a list `src`. See [Extract](#extract). |
|
|
472
|
+
| `sudo` | No | Write the file using elevated privileges. Use `true` to write as root, or a username string (e.g. `"www-data"`) to write as a specific user via `sudo -u`. avanti authenticates once per distinct identity before any writes — the OS sudo credential cache is reused for all subsequent operations within the same pull session. **POSIX only** — `pull` errors on Windows when any file has `sudo` set. **Note:** `sudo` is honored by `pull` only (including stale-file cleanup). The `revert` and `reset` commands restore files using normal (non-elevated) file operations and will fail on root-owned paths. |
|
|
423
473
|
|
|
424
474
|
### Source Types
|
|
425
475
|
|
|
426
|
-
**Plain string** — HTTP/HTTPS URL or
|
|
476
|
+
**Plain string** — HTTP/HTTPS URL, local path, or remote source spec (`github:`, `gitlab:`, `git+ssh://`, etc.):
|
|
427
477
|
|
|
428
478
|
```yaml
|
|
429
479
|
src: https://example.com/file.txt
|
|
430
480
|
src: ~/templates/file.txt
|
|
431
481
|
src: /absolute/path/file.txt
|
|
482
|
+
src: ./relative/path/file.txt # relative to the config file's directory
|
|
432
483
|
```
|
|
433
484
|
|
|
485
|
+
Relative paths (no leading `/` or `~/`) are resolved relative to the config file's location, not the working directory. If the config is a local file at `./configs/avanti.yml`, then `src: ./scripts/build.sh` fetches `./configs/scripts/build.sh`. For remote configs, a relative src resolves within the same remote context — it becomes a remote source of the same type, not a local file:
|
|
486
|
+
|
|
487
|
+
- Config `github:owner/repo:configs/avanti.yml` + `src: ./scripts/build.sh` → fetches `github:owner/repo:configs/scripts/build.sh`
|
|
488
|
+
- Config `https://example.com/configs/avanti.yml` + `src: ./scripts/build.sh` → fetches `https://example.com/configs/scripts/build.sh`
|
|
489
|
+
- Config `git+ssh://git@host/org/repo.git//configs/avanti.yml@main` + `src: ./scripts/build.sh` → fetches `git+ssh://git@host/org/repo.git//configs/scripts/build.sh@main`
|
|
490
|
+
|
|
434
491
|
**Map** — for path, url, exec, gitlab, github, bitbucket, git, aws_s3,
|
|
435
492
|
aws_secrets_manager, aws_systems_manager_parameter, vault, http, raw:
|
|
436
493
|
|
|
@@ -461,7 +518,7 @@ src:
|
|
|
461
518
|
gitlab:
|
|
462
519
|
project: group/repo # GitLab project path
|
|
463
520
|
file: path/to/file.txt # file or directory in repo (mutually exclusive with release)
|
|
464
|
-
ref: main # branch, tag,
|
|
521
|
+
ref: main # branch, tag, $latest, $recent, or /pattern/ (optional)
|
|
465
522
|
sha: abc123... # optional SHA-256 fingerprint
|
|
466
523
|
host: gitlab.mycompany.com # override default gitlab.com (optional)
|
|
467
524
|
via: cli # api, cli, or list (default: [api, cli])
|
|
@@ -470,16 +527,19 @@ src:
|
|
|
470
527
|
src:
|
|
471
528
|
gitlab:
|
|
472
529
|
project: group/repo # GitLab project path
|
|
473
|
-
release: v1.2.3 # release tag,
|
|
530
|
+
release: v1.2.3 # release tag, $latest, $recent, or /pattern/ (mutually exclusive with file)
|
|
474
531
|
sha: abc123... # optional SHA-256 fingerprint
|
|
475
532
|
host: gitlab.mycompany.com # override default gitlab.com (optional)
|
|
476
533
|
via: cli # api, cli, or list (default: [api, cli])
|
|
534
|
+
filter: # optional: keep only matching assets (see below)
|
|
535
|
+
- installer.deb
|
|
536
|
+
- checksums-{amd64,arm64}.txt
|
|
477
537
|
|
|
478
538
|
src:
|
|
479
539
|
github:
|
|
480
540
|
repo: owner/repo # GitHub owner/repo
|
|
481
541
|
file: path/to/file.txt # file or directory in repo (mutually exclusive with release)
|
|
482
|
-
ref: main # branch, tag,
|
|
542
|
+
ref: main # branch, tag, $latest, $recent, or /pattern/ (optional)
|
|
483
543
|
sha: abc123... # optional SHA-256 fingerprint
|
|
484
544
|
host: github.mycompany.com # GitHub Enterprise Server hostname (optional)
|
|
485
545
|
via: cli # api, cli, or list (default: [api, cli])
|
|
@@ -488,10 +548,14 @@ src:
|
|
|
488
548
|
src:
|
|
489
549
|
github:
|
|
490
550
|
repo: owner/repo # GitHub owner/repo
|
|
491
|
-
release: v1.2.3 # release tag,
|
|
551
|
+
release: v1.2.3 # release tag, $latest, $recent, or /pattern/ (mutually exclusive with file)
|
|
492
552
|
sha: abc123... # optional SHA-256 fingerprint
|
|
493
553
|
host: github.mycompany.com # GitHub Enterprise Server hostname (optional)
|
|
494
554
|
via: cli # api, cli, or list (default: [api, cli])
|
|
555
|
+
filter: # optional: keep only matching assets (see below)
|
|
556
|
+
- exact-match.png
|
|
557
|
+
- file-{a,b,c}.yml
|
|
558
|
+
- /^some.*\.jpg/
|
|
495
559
|
|
|
496
560
|
src:
|
|
497
561
|
bitbucket:
|
|
@@ -547,6 +611,79 @@ The optional `sha` field pins a source to a specific content fingerprint. When p
|
|
|
547
611
|
|
|
548
612
|
Use `avanti lock` to compute and write SHA values automatically. Use `avanti pull --accept-changes` to review a mismatch and update the pinned SHA. Plain string sources (a bare local path or URL string) and `raw:` sources do not support `sha`. Use the explicit `path:` or `url:` map form to pin a local file or HTTP URL.
|
|
549
613
|
|
|
614
|
+
#### Filter
|
|
615
|
+
|
|
616
|
+
The optional `filter` field narrows which files are kept when a source returns multiple files (directory sources, release artifacts, S3 prefixes). It is supported on `path:`, `github:`, `gitlab:`, `bitbucket:`, `git:`, and `aws_s3:` sources.
|
|
617
|
+
|
|
618
|
+
`filter` is a list of one or more patterns. A file is kept if **any** pattern matches its path relative to the source root (the filename for flat sources like release assets, or the relative path for directory sources). Paths are always matched using forward slashes (`/`) regardless of the platform — on Windows, write `subdir/file.yml`, not `subdir\file.yml`.
|
|
619
|
+
|
|
620
|
+
| Pattern | Matches |
|
|
621
|
+
| ------------------ | -------------------------------------------------------------------------------------- |
|
|
622
|
+
| `exact.png` | Exact string equality |
|
|
623
|
+
| `subdir/` | Directory prefix — all entries whose path starts with `subdir/` |
|
|
624
|
+
| `file-{a,b,c}.yml` | Brace-expanded alternatives — `file-a.yml`, `file-b.yml`, `file-c.yml` |
|
|
625
|
+
| `/^some.*\.jpg/` | JavaScript regular expression (delimited by `/`) tested against the full relative path |
|
|
626
|
+
|
|
627
|
+
```yaml
|
|
628
|
+
files:
|
|
629
|
+
assets/:
|
|
630
|
+
src:
|
|
631
|
+
github:
|
|
632
|
+
repo: owner/repo
|
|
633
|
+
release: $latest
|
|
634
|
+
filter:
|
|
635
|
+
- exact-match.png
|
|
636
|
+
- dist/ # all files under dist/
|
|
637
|
+
- checksums-{amd64,arm64}.txt
|
|
638
|
+
- /^some.*\.jpg/
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Variables are resolved in filter patterns before matching, so patterns like `$env:ARCH.tar.gz` or `$platform-release.zip` work as expected. An error is raised if the filter matches zero files, preventing silent misconfiguration. The `sha` fingerprint (if present) is computed over the filtered set.
|
|
642
|
+
|
|
643
|
+
> **Note:** brace expansion is not supported in directory-prefix patterns (patterns ending with `/`). Use separate patterns instead — e.g. `"core/"` and `"utils/"` rather than `"{core,utils}/"`.
|
|
644
|
+
|
|
645
|
+
#### Extract
|
|
646
|
+
|
|
647
|
+
The optional `extract` field unpacks a downloaded archive before writing files. It applies to any single-file source (HTTP URL, local path, etc.) that returns an archive. Set `extract: true` to extract all entries, or provide a list of patterns to keep only matching entries.
|
|
648
|
+
|
|
649
|
+
**The target must be a directory** (end with `/`). Archive extraction writes multiple files; a non-directory target is rejected at parse time.
|
|
650
|
+
|
|
651
|
+
| Format | Extensions |
|
|
652
|
+
| ---------- | ----------------- |
|
|
653
|
+
| ZIP | `.zip` |
|
|
654
|
+
| tar | `.tar` |
|
|
655
|
+
| tar + gzip | `.tar.gz`, `.tgz` |
|
|
656
|
+
|
|
657
|
+
Patterns use the same syntax as [`filter`](#filter):
|
|
658
|
+
|
|
659
|
+
| Pattern | Matches |
|
|
660
|
+
| ------------- | -------------------------------------------------------------------------------------- |
|
|
661
|
+
| `exact.png` | Exact string equality |
|
|
662
|
+
| `subdir/` | Directory prefix — all entries whose path starts with `subdir/` |
|
|
663
|
+
| `{a,b,c}.yml` | Brace-expanded alternatives |
|
|
664
|
+
| `/^.*\.jpg/` | JavaScript regular expression (delimited by `/`) tested against the full relative path |
|
|
665
|
+
|
|
666
|
+
> **Note:** brace expansion is not supported in directory-prefix patterns (patterns ending with `/`). Use separate patterns instead — e.g. `"core/"` and `"utils/"` rather than `"{core,utils}/"`.
|
|
667
|
+
|
|
668
|
+
```yaml
|
|
669
|
+
files:
|
|
670
|
+
# Extract all files from a release archive into a local directory
|
|
671
|
+
tools/:
|
|
672
|
+
src: https://example.com/release.tar.gz
|
|
673
|
+
extract: true
|
|
674
|
+
|
|
675
|
+
# Extract only matching entries
|
|
676
|
+
assets/:
|
|
677
|
+
src: https://example.com/bundle.zip
|
|
678
|
+
extract:
|
|
679
|
+
- readme.md # exact match
|
|
680
|
+
- images/ # all entries under images/
|
|
681
|
+
- libs/{core,utils}.js # brace expansion (not with trailing /)
|
|
682
|
+
- /^assets\/.*\.png$/ # regex
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Variables are resolved in patterns before matching. An error is raised if the pattern list matches zero entries. `extract` cannot be combined with a list `src`. Entry paths are validated — archives containing path-traversal sequences (`../`) or absolute paths are rejected for security. The `sha` fingerprint (if present) is computed over the archive before extraction.
|
|
686
|
+
|
|
550
687
|
### Directory Sources
|
|
551
688
|
|
|
552
689
|
Any source type that references a path (local, GitLab, GitHub, Bitbucket, git, S3) can point to a directory instead of a single file. End the path with `/` to declare it a directory explicitly; without a trailing slash the tool probes the remote to decide.
|
|
@@ -637,7 +774,7 @@ files:
|
|
|
637
774
|
ref: main
|
|
638
775
|
```
|
|
639
776
|
|
|
640
|
-
Sources are fetched in order and joined with a newline. Post-processing (`replace`, `
|
|
777
|
+
Sources are fetched in order and joined with a newline. Post-processing (`replace`, `on.write`) is applied to the combined result. If any source fails, the entire entry is aborted.
|
|
641
778
|
|
|
642
779
|
### JSON Merging
|
|
643
780
|
|
|
@@ -844,11 +981,111 @@ files:
|
|
|
844
981
|
src: ./config.toml
|
|
845
982
|
```
|
|
846
983
|
|
|
984
|
+
### INI Merging
|
|
985
|
+
|
|
986
|
+
When all sources in a list have a `.ini` or `.cfg` extension, INI merging is enabled
|
|
987
|
+
automatically — no extra config needed:
|
|
988
|
+
|
|
989
|
+
```yaml
|
|
990
|
+
files:
|
|
991
|
+
merged.ini:
|
|
992
|
+
src:
|
|
993
|
+
- ./defaults.ini
|
|
994
|
+
- ./overrides.ini
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
To merge sources that don't have an INI extension (e.g. `exec:`, `raw:`, or a URL without `.ini`), set `ini: true`:
|
|
998
|
+
|
|
999
|
+
```yaml
|
|
1000
|
+
files:
|
|
1001
|
+
merged.ini:
|
|
1002
|
+
src:
|
|
1003
|
+
- exec: cat defaults.ini
|
|
1004
|
+
- ./overrides.ini
|
|
1005
|
+
ini: true
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
To opt out of auto-detection and force plain concatenation, set `ini: false`.
|
|
1009
|
+
|
|
1010
|
+
**Fine-grained options** — pass an object to control merge behavior:
|
|
1011
|
+
|
|
1012
|
+
```yaml
|
|
1013
|
+
files:
|
|
1014
|
+
merged.ini:
|
|
1015
|
+
src:
|
|
1016
|
+
- ./defaults.ini
|
|
1017
|
+
- github:
|
|
1018
|
+
repo: org/configs
|
|
1019
|
+
file: overrides.ini
|
|
1020
|
+
ini:
|
|
1021
|
+
conflicts: last_wins # abort | first_wins | last_wins (default)
|
|
1022
|
+
arrays: replace # replace (default) | concat | dedupe
|
|
1023
|
+
objects: merge # merge (default) | replace
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
The options behave identically to JSON, YAML, and TOML merging:
|
|
1027
|
+
|
|
1028
|
+
- `conflicts` — what to do when the same key holds a scalar (or an array/object when their strategy is `replace`):
|
|
1029
|
+
- `last_wins` _(default)_ — the last source's value wins
|
|
1030
|
+
- `first_wins` — the first source's value is kept
|
|
1031
|
+
- `abort` — throw an error (identical values are not considered a conflict)
|
|
1032
|
+
- `arrays` — how to combine arrays at the same key (written as `key[] = val` in INI):
|
|
1033
|
+
- `replace` _(default)_ — the later source's array replaces the earlier one
|
|
1034
|
+
- `concat` — arrays are concatenated (no deduplication)
|
|
1035
|
+
- `dedupe` — items from the later source are appended only if not already present
|
|
1036
|
+
- `objects` — how to combine sections at the same name:
|
|
1037
|
+
- `merge` _(default)_ — deep merge, applying the same rules recursively to section keys
|
|
1038
|
+
- `replace` — the later source's section replaces the earlier one entirely
|
|
1039
|
+
|
|
1040
|
+
**Comments and key order are preserved in the base (first) source.** The INI processor uses
|
|
1041
|
+
a line-level AST, so comment lines, inline comments, and blank lines from the first source
|
|
1042
|
+
are preserved through the merge. Minor whitespace normalization may occur (e.g. a single
|
|
1043
|
+
space is inserted before inline comments, and spacing around `=` follows the base key's
|
|
1044
|
+
original separator). When a key's value is updated by a later source, the base key's inline
|
|
1045
|
+
comment is kept and the key stays at its original position — it is not shuffled to the end.
|
|
1046
|
+
**Comment behavior under `objects: merge` (default):** Comment lines (`; ...` / `# ...`),
|
|
1047
|
+
blank lines, and section header comments from overlay sources are not transferred when merging
|
|
1048
|
+
individual keys. For keys that already exist in the base, the base key's inline comment is
|
|
1049
|
+
kept. For new keys introduced only by the overlay, their inline comments are preserved (there
|
|
1050
|
+
is no base inline comment to fall back on). When a new section is introduced by the overlay
|
|
1051
|
+
(one that does not exist in the base), it is inserted without its section header comment or
|
|
1052
|
+
any internal comment/blank nodes — only its key-value pairs are carried over.
|
|
1053
|
+
|
|
1054
|
+
**Comment behavior under `objects: replace`:** When the overlay section's key-value content
|
|
1055
|
+
differs from the base, the entire overlay section is used as-is — including its section
|
|
1056
|
+
header comment, internal comment lines, blank lines, and inline comments. If the two sections
|
|
1057
|
+
have identical key-value content (even when they differ only in comments or whitespace), no
|
|
1058
|
+
replacement occurs — the base section is kept unchanged.
|
|
1059
|
+
|
|
1060
|
+
**Inline comment limitation for arrays:** When the same key appears as multiple `key[] = val`
|
|
1061
|
+
entries, all values are coalesced into a single array node. Only the inline comment from the
|
|
1062
|
+
_first_ occurrence (if any) is preserved; inline comments on subsequent `key[] = val` lines are
|
|
1063
|
+
discarded.
|
|
1064
|
+
|
|
1065
|
+
**Supported INI features:** sections (`[section]`), subsections (`[section "name"]`),
|
|
1066
|
+
`key = value` pairs, bare keys, quoted values (`"..."` / `'...'`), comment lines (`;` and `#`),
|
|
1067
|
+
inline comments, blank lines, backslash line continuation, and arrays via `key[] = val`. All
|
|
1068
|
+
`key[] = val` entries for the same key are collected into one array regardless of position in
|
|
1069
|
+
the file; non-contiguous entries are normalized to appear at the first occurrence of that key.
|
|
1070
|
+
|
|
1071
|
+
**Value coercion:** Unquoted values that match `true` or `false` (case-insensitive) are parsed
|
|
1072
|
+
as booleans, and values that parse as a valid number are parsed as numbers. This means
|
|
1073
|
+
`enabled = true` is stored as a boolean and `port = 8080` as a number; on format or merge
|
|
1074
|
+
these are re-serialised as `true` / `8080` respectively. Strings like `001` are normalised to
|
|
1075
|
+
`1`. To preserve the exact string form, quote the value: `port = "8080"`.
|
|
1076
|
+
|
|
1077
|
+
**Inline comment delimiter:** `;` and `#` are treated as comment delimiters when they appear
|
|
1078
|
+
outside of quoted strings. If a value contains a literal `;` or `#` character, quote the value
|
|
1079
|
+
(e.g. `url = "https://example.com#anchor"`) to prevent it from being interpreted as a comment.
|
|
1080
|
+
|
|
1081
|
+
**Pretty-printing a single file** — `ini` works on single-source entries too. Auto-detection
|
|
1082
|
+
applies here as well, so a single `.ini` or `.cfg` source is normalized automatically.
|
|
1083
|
+
|
|
847
1084
|
### Template Rendering
|
|
848
1085
|
|
|
849
1086
|
Set `template` to treat the fetched content as a template. avanti renders it at deploy time using all avanti config variables as the template context, then writes the rendered output to the target file.
|
|
850
1087
|
|
|
851
|
-
> **Security note** — EJS and Eta templates execute arbitrary JavaScript at render time. Handlebars, Nunjucks, Liquid, and Mustache are logic-limited and do not execute raw JS. For any engine, template sources must be trusted: either authored locally, fetched from a controlled internal source, or SHA-pinned (see [`sha:`](#sha-pinning)). Treat a compromised remote template as equivalent to a compromised `
|
|
1088
|
+
> **Security note** — EJS and Eta templates execute arbitrary JavaScript at render time. Handlebars, Nunjucks, Liquid, and Mustache are logic-limited and do not execute raw JS. For any engine, template sources must be trusted: either authored locally, fetched from a controlled internal source, or SHA-pinned (see [`sha:`](#sha-pinning)). Treat a compromised remote template as equivalent to a compromised `on.write` script or `exec:` source.
|
|
852
1089
|
|
|
853
1090
|
```yaml
|
|
854
1091
|
variables:
|
|
@@ -889,7 +1126,42 @@ For multi-source arrays (`src: [a, b, c]`) and directory-to-single-file merges,
|
|
|
889
1126
|
|
|
890
1127
|
**`jinja2` alias** — `template: jinja2` is equivalent to `template: nunjucks`. Nunjucks is a JavaScript implementation heavily inspired by Jinja2; most Jinja2 templates work without changes.
|
|
891
1128
|
|
|
892
|
-
**Pipeline order** — template rendering runs first, before `replace` and `
|
|
1129
|
+
**Pipeline order** — template rendering runs first, before `replace` and `on.write`. Subsequent processors receive the already-rendered content.
|
|
1130
|
+
|
|
1131
|
+
### Event Hooks
|
|
1132
|
+
|
|
1133
|
+
The `on:` field on a file entry lets you run shell commands at specific points in the file lifecycle. `on.write` supports avanti variable substitution (same rules as `exec:` and `replace:`). Side-effect hooks (`before*`, `create`, `update`) are passed to the shell verbatim — use `$AVANTI_TARGET` and `$AVANTI_IS_NEW` as environment variables.
|
|
1134
|
+
|
|
1135
|
+
| Hook | When | Content transform? |
|
|
1136
|
+
| ----------------- | ------------------------------------------------------------------------------- | ------------------ |
|
|
1137
|
+
| `on.write` | During processing, after `replace`; stdin → stdout replaces content | Yes |
|
|
1138
|
+
| `on.beforeWrite` | After user confirms, before writing — fires for every changed file | No |
|
|
1139
|
+
| `on.beforeCreate` | Same timing, but only when the file is being **created** for the first time | No |
|
|
1140
|
+
| `on.beforeUpdate` | Same timing, but only when the file already **exists** and has changed | No |
|
|
1141
|
+
| `on.create` | After the file has been successfully written — new files only | No |
|
|
1142
|
+
| `on.update` | After the file has been successfully written — existing files with changes only | No |
|
|
1143
|
+
|
|
1144
|
+
Side-effect hooks (`before*`, `create`, `update`) receive two environment variables:
|
|
1145
|
+
|
|
1146
|
+
| Variable | Value |
|
|
1147
|
+
| --------------- | ----------------------------------------------------------- |
|
|
1148
|
+
| `AVANTI_TARGET` | Absolute path of the target file |
|
|
1149
|
+
| `AVANTI_IS_NEW` | `"true"` if the file is being created; `"false"` if updated |
|
|
1150
|
+
|
|
1151
|
+
On Unix, access them as `$AVANTI_TARGET` / `$AVANTI_IS_NEW`. On Windows (PowerShell), avanti automatically injects a prelude that maps these to local variables, so `$AVANTI_TARGET` works identically — you do not need to write `$env:AVANTI_TARGET`.
|
|
1152
|
+
|
|
1153
|
+
Only hooks for files with **actual changes** are fired (`create`/`update`/`before*` are silent no-ops when content and mode are unchanged).
|
|
1154
|
+
|
|
1155
|
+
```yaml
|
|
1156
|
+
files:
|
|
1157
|
+
config/app.yml:
|
|
1158
|
+
src: https://config.example.com/app.yml
|
|
1159
|
+
on:
|
|
1160
|
+
write: sed -e 's/v3/v4/g' # transform content
|
|
1161
|
+
beforeCreate: mkdir -p "$(dirname "$AVANTI_TARGET")"
|
|
1162
|
+
create: echo "Created $AVANTI_TARGET"
|
|
1163
|
+
update: git add "$AVANTI_TARGET"
|
|
1164
|
+
```
|
|
893
1165
|
|
|
894
1166
|
### Insert Mode
|
|
895
1167
|
|
|
@@ -906,7 +1178,7 @@ files:
|
|
|
906
1178
|
**How it works:**
|
|
907
1179
|
|
|
908
1180
|
- **First run** — the fetched content is merged into the existing file (or written as-is if the file does not exist yet).
|
|
909
|
-
- **Subsequent runs (no-op)** — avanti detects that the raw source and the post-processed output (`replace`/`
|
|
1181
|
+
- **Subsequent runs (no-op)** — avanti detects that the raw source and the post-processed output (`replace`/`on.write`) are both unchanged and skips the file entirely.
|
|
910
1182
|
- **Subsequent runs (source changed)** — avanti removes the keys/text it previously contributed, then merges the updated content in.
|
|
911
1183
|
- **User edits are preserved** — keys or text the user added or modified are left untouched. If a user overrides an avanti-managed key, avanti will not remove it even if the source no longer includes it.
|
|
912
1184
|
|
|
@@ -944,13 +1216,13 @@ When both are present, both must pass. Each condition object may also include `n
|
|
|
944
1216
|
|
|
945
1217
|
#### Condition fields
|
|
946
1218
|
|
|
947
|
-
| Field | Type | Description
|
|
948
|
-
| --------------- | -------------- |
|
|
949
|
-
| `os` | string or list | Platform must match. Values: `linux`, `mac`, `windows`. List = any matches.
|
|
950
|
-
| `exists` | string | Path (file or directory) must exist. Variables are resolved.
|
|
951
|
-
| `exec` | string | Shell command must exit with code `0`.
|
|
952
|
-
| `target_exists` | boolean | `true` — pass only if target exists. `false` — pass only if target does not exist.
|
|
953
|
-
| `not` | boolean | `true` — invert the result of all checks in this condition object.
|
|
1219
|
+
| Field | Type | Description |
|
|
1220
|
+
| --------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
1221
|
+
| `os` | string or list | Platform must match. Values: `linux`, `mac`, `windows`. Aliases: `darwin` (= `mac`), `win32` (= `windows`). List = any matches. |
|
|
1222
|
+
| `exists` | string | Path (file or directory) must exist. Variables are resolved. |
|
|
1223
|
+
| `exec` | string | Shell command must exit with code `0`. |
|
|
1224
|
+
| `target_exists` | boolean | `true` — pass only if target exists. `false` — pass only if target does not exist. |
|
|
1225
|
+
| `not` | boolean | `true` — invert the result of all checks in this condition object. |
|
|
954
1226
|
|
|
955
1227
|
#### Examples
|
|
956
1228
|
|
|
@@ -1063,11 +1335,11 @@ files:
|
|
|
1063
1335
|
backup: $dirname/$filename.bkp
|
|
1064
1336
|
```
|
|
1065
1337
|
|
|
1066
|
-
Backup
|
|
1338
|
+
Backup happens when the target path currently holds a regular file or a symlink — regardless of whether the entry being written is a regular file or a symlink entry. On POSIX, if the existing target is a symlink, the symlink itself (not the file it points to) is preserved in the backup; if the symlink had a relative target, avanti rewrites it to an absolute path so the backup symlink resolves correctly from the backup directory. On Windows, symlink backups are skipped (a warning is printed) because creating a symlink backup requires elevated privileges and dereferencing the link could read files outside the working directory. **Exception:** for regular-file entries with `sudo: true`, only existing regular files are backed up — if the current target is a symlink it is not backed up. For `symlink:` entries with `sudo: true`, the existing symlink (or regular file) at the target path is backed up before being replaced. Directory targets are never backed up. If the backup path already exists it is overwritten — use the [counter pattern](#counter-pattern) or `$datetime` when you want to keep every backup.
|
|
1067
1339
|
|
|
1068
1340
|
#### Path variables
|
|
1069
1341
|
|
|
1070
|
-
All [system-injected variables](#system-injected-variables) — per-file path variables (`$path`, `$filename`, `$basename`, `$ext`, `$dirname`, `$basedir`)
|
|
1342
|
+
All [system-injected variables](#system-injected-variables) — per-file path variables (`$path`, `$filename`, `$basename`, `$ext`, `$dirname`, `$basedir`), pull-time variables (`$date`, `$datetime`), and system variables (`$os`, `$arch`, `$arch_go`) — are available in `backup:` patterns.
|
|
1071
1343
|
|
|
1072
1344
|
#### Counter pattern
|
|
1073
1345
|
|
|
@@ -1149,6 +1421,111 @@ files:
|
|
|
1149
1421
|
|
|
1150
1422
|
> **Warning:** `writeInPlace: true` is **not atomic**. Between the truncate and the completed write, a concurrent reader will see empty or partial content. Use the default atomic rename when correctness under concurrent reads matters more than inode stability.
|
|
1151
1423
|
|
|
1424
|
+
### Follow Symlink
|
|
1425
|
+
|
|
1426
|
+
When the target path is a symlink, avanti's default atomic-rename strategy replaces the symlink itself — the symlink is overwritten with the fetched file content. Set `followSymlink: true` to write through the symlink instead: avanti resolves the symlink chain to its real path and writes the content there, leaving the symlink pointer intact.
|
|
1427
|
+
|
|
1428
|
+
> **Interaction with `writeInPlace`:** `writeInPlace: true` errors when its target path is a symlink. When combined with `followSymlink: true`, avanti resolves the symlink first and passes the real path to the writer, so `writeInPlace` never sees a symlink. The result: content is written in-place to the real file (inode preserved) while the symlink itself remains intact.
|
|
1429
|
+
|
|
1430
|
+
```yaml
|
|
1431
|
+
files:
|
|
1432
|
+
config/settings.json:
|
|
1433
|
+
src:
|
|
1434
|
+
github:
|
|
1435
|
+
repo: org/shared-configs
|
|
1436
|
+
file: settings.json
|
|
1437
|
+
followSymlink: true
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
This is useful when a symlink is managed by another tool (e.g. a dotfile manager) and must not be replaced. avanti updates the real file's content while the symlink pointer remains unchanged.
|
|
1441
|
+
|
|
1442
|
+
**Safety constraints** — avanti validates the resolved target before writing:
|
|
1443
|
+
|
|
1444
|
+
- The resolved path must **not be a directory** — avanti refuses to write file content over a directory target.
|
|
1445
|
+
- The resolved path must remain **inside the working directory** — symlinks that escape (directly or through intermediate symlinked directories) are rejected.
|
|
1446
|
+
- **Circular symlinks** are detected and rejected.
|
|
1447
|
+
|
|
1448
|
+
Dangling symlinks (a symlink chain whose endpoint does not yet exist) are supported: avanti follows the chain to the non-existent endpoint and creates the file there, subject to the same directory and working-directory constraints above.
|
|
1449
|
+
|
|
1450
|
+
When the target path does not exist yet, or does not point to a symlink, `followSymlink` has no effect and the file is created or written normally.
|
|
1451
|
+
|
|
1452
|
+
### Symlink
|
|
1453
|
+
|
|
1454
|
+
Set `symlink: true` (or `symlink: "absolute"`) to create a filesystem symlink at the target path pointing to the `src` path, instead of copying the file's content. The `src` must be a single local path — remote sources (HTTP, GitHub, GitLab, exec, etc.) are not supported.
|
|
1455
|
+
|
|
1456
|
+
```yaml
|
|
1457
|
+
files:
|
|
1458
|
+
~/.config/app/config.yml:
|
|
1459
|
+
src: /opt/app/defaults/config.yml
|
|
1460
|
+
symlink: true
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
Use `symlink: "relative"` to store the symlink target as a path relative to the symlink's parent directory. This is useful when you want symlinks that remain valid after the directory tree is moved or mounted at a different location:
|
|
1464
|
+
|
|
1465
|
+
```yaml
|
|
1466
|
+
files:
|
|
1467
|
+
configs/active:
|
|
1468
|
+
src: configs/production.yml
|
|
1469
|
+
symlink: relative # symlink points to production.yml rather than an absolute path
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
**How `diff` and `pull` handle symlinks:**
|
|
1473
|
+
|
|
1474
|
+
- No symlink at the target path → create it (shown as a new entry in the diff)
|
|
1475
|
+
- Symlink already points to the correct target → no-op (no diff output, exit 0)
|
|
1476
|
+
- Symlink points to a different target → update it (diff shows old → new target)
|
|
1477
|
+
- A regular file exists at the target path → replace it with a symlink (shown in diff)
|
|
1478
|
+
|
|
1479
|
+
The symlink itself is tracked in avanti's history, so `revert` and `reset` restore the symlink (including its target path) to the state recorded at the referenced pull.
|
|
1480
|
+
|
|
1481
|
+
**Constraints** — `symlink` cannot be combined with:
|
|
1482
|
+
|
|
1483
|
+
- `replace`, `template`, `json`, `yaml`, `toml`, `ini`, `on.write`, `extract` — content processors are meaningless for a symlink
|
|
1484
|
+
- `writeInPlace`, `strategy`, `followSymlink` — incompatible write strategies
|
|
1485
|
+
- `mode` — symlinks do not have independent permission bits on POSIX
|
|
1486
|
+
- A list `src` — a symlink has exactly one target
|
|
1487
|
+
- `src.sha`, `src.filter`, `src.if`, `src.ifAny` — object-form src options that only apply to fetched content
|
|
1488
|
+
- The `$self` key — the config file itself cannot be a symlink entry
|
|
1489
|
+
|
|
1490
|
+
**POSIX only** — symlink entries are not supported on Windows. `pull` will error if a symlink entry is reached on win32. Use an `if: { os: [linux, mac] }` condition to gate symlink entries in cross-platform configs.
|
|
1491
|
+
|
|
1492
|
+
### Sudo
|
|
1493
|
+
|
|
1494
|
+
Set `sudo: true` to write a file using elevated privileges (as root). Set `sudo: "username"` to write as a specific user via `sudo -u`:
|
|
1495
|
+
|
|
1496
|
+
```yaml
|
|
1497
|
+
files:
|
|
1498
|
+
/etc/ssh/sshd_config:
|
|
1499
|
+
src:
|
|
1500
|
+
github:
|
|
1501
|
+
repo: org/system-configs
|
|
1502
|
+
file: sshd_config
|
|
1503
|
+
mode: '0600'
|
|
1504
|
+
sudo: true
|
|
1505
|
+
|
|
1506
|
+
/var/www/html/index.html:
|
|
1507
|
+
src: https://example.com/index.html
|
|
1508
|
+
sudo: 'www-data'
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
Absolute target paths (e.g. `/etc/ssh/sshd_config`) require `--working-dir /`. The map key is the target path:
|
|
1512
|
+
|
|
1513
|
+
```sh
|
|
1514
|
+
avanti pull --working-dir /
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
avanti calls `sudo -v` once per distinct identity to prime the OS credential cache, so the user is prompted for their password at most once per distinct sudo identity per pull session. Authentication happens as early as needed: if any sudo target is unreadable by the current user (e.g. a root-owned file), avanti authenticates before reading that file to compare it for the diff — so a password prompt may appear before the diff is displayed or the user is asked to confirm. For files that are already readable, authentication is deferred until just before the write phase.
|
|
1518
|
+
|
|
1519
|
+
**Stale-file cleanup** — when a `sudo` entry is removed from the config, avanti uses the stored sudo identity to restore or delete the file during the next pull.
|
|
1520
|
+
|
|
1521
|
+
**POSIX only** — `pull` errors on Windows when any file has `sudo` set. Use an `if: { os: [linux, mac] }` condition to gate `sudo` entries in cross-platform configs.
|
|
1522
|
+
|
|
1523
|
+
**Limitations:**
|
|
1524
|
+
|
|
1525
|
+
- `sudo` is honored by `pull` only. The `revert` and `reset` commands use normal file operations and will fail on root-owned paths.
|
|
1526
|
+
- `strategy: insert` cannot be combined with `sudo` — insert mode reads the existing file without privilege escalation, which silently treats an unreadable privileged file as absent. avanti rejects this combination at config parse time.
|
|
1527
|
+
- `backup` paths for `sudo` entries are resolved before privilege escalation. The backup path's parent directories must be stat-able by the current user (all ancestor directories need the execute/search bit set). A 0755 root-owned backup directory works fine; a 0700 root-owned directory does not, because `lstat` cannot traverse it to resolve `%d` counters or verify the path.
|
|
1528
|
+
|
|
1152
1529
|
### Variables
|
|
1153
1530
|
|
|
1154
1531
|
Define reusable values at the top level under `variables:`:
|
|
@@ -1174,11 +1551,11 @@ files:
|
|
|
1174
1551
|
to: $email # resolved to "you@example.com"
|
|
1175
1552
|
```
|
|
1176
1553
|
|
|
1177
|
-
Variables are resolved in every string field: target keys, `ref`, `exec` commands, HTTP URLs, local paths, `raw` content, `replace` rules (`from` and `to`), and `
|
|
1554
|
+
Variables are resolved in every string field: target keys, `ref`, `exec` commands, HTTP URLs, local paths, `raw` content, `filter` patterns, `replace` rules (`from` and `to`), and `on.write` scripts. Side-effect hooks (`on.beforeWrite`, `on.beforeCreate`, `on.beforeUpdate`, `on.create`, `on.update`) are passed to the shell verbatim — use `$AVANTI_TARGET` / `$AVANTI_IS_NEW` env vars instead of `$varname` substitutions.
|
|
1178
1555
|
|
|
1179
1556
|
For `raw:` sources, variables are resolved in the content itself. For all other source types (`http`, `local`, `github`, `gitlab`, `exec`), variables are only resolved in the fields that locate the source (URL, path, command) — not in the fetched content. Use a `replace:` rule if you need to substitute values in fetched content.
|
|
1180
1557
|
|
|
1181
|
-
**Shell safety in `exec:` and `
|
|
1558
|
+
**Shell safety in `exec:` and `on.write`** — when a variable is substituted into an `exec:` command or an `on.write` hook script, its value is automatically single-quoted. This means shell metacharacters (`;`, `&&`, `$(...)`, etc.) in the value are treated as literal data and are never executed. The surrounding command template itself is not quoted, so the static shell syntax you write is executed as usual. On Unix the script runs via `sh -c`; on Windows it runs via PowerShell (`-EncodedCommand`).
|
|
1182
1559
|
|
|
1183
1560
|
```yaml
|
|
1184
1561
|
variables:
|
|
@@ -1188,10 +1565,11 @@ files:
|
|
|
1188
1565
|
data.json:
|
|
1189
1566
|
src:
|
|
1190
1567
|
exec: curl https://example.com/api/$version/data # expands to: curl …/'1.0'/data
|
|
1191
|
-
|
|
1568
|
+
on:
|
|
1569
|
+
write: sed 's/$version/replaced/g' # expands to: sed 's/'\''1.0'\''/replaced/g'
|
|
1192
1570
|
```
|
|
1193
1571
|
|
|
1194
|
-
**Escaping a literal `$`** — use `$$` to emit a literal `$` that is not treated as a variable reference. This is useful in `exec:` and `
|
|
1572
|
+
**Escaping a literal `$`** — use `$$` to emit a literal `$` that is not treated as a variable reference. This is useful in `exec:` commands and `on.write` hook scripts that contain shell or PowerShell syntax with `$`-prefixed identifiers (e.g. PowerShell built-ins like `$$true` or `$$null`):
|
|
1195
1573
|
|
|
1196
1574
|
```yaml
|
|
1197
1575
|
files:
|
|
@@ -1199,7 +1577,8 @@ files:
|
|
|
1199
1577
|
src:
|
|
1200
1578
|
# On Windows exec: runs in PowerShell — $true is a PS built-in, needs $$
|
|
1201
1579
|
exec: "if ($$true) { Write-Output 'yes' }"
|
|
1202
|
-
|
|
1580
|
+
on:
|
|
1581
|
+
write: sed 's/$$HOME/redacted/g' # $HOME would be treated as an avanti variable
|
|
1203
1582
|
```
|
|
1204
1583
|
|
|
1205
1584
|
`$$` produces a single `$` after substitution. `$$$name` produces `$` followed by the value of `name`. `$${expr}` produces a literal `${expr}` — use this to include shell-style `${VAR}` or template placeholders verbatim in a string without avanti interpreting them.
|
|
@@ -1326,11 +1705,25 @@ variables:
|
|
|
1326
1705
|
registry_line: //$host/:_authToken=$token # both $host and $token are available
|
|
1327
1706
|
```
|
|
1328
1707
|
|
|
1329
|
-
|
|
1708
|
+
The `ref` (and `release`) field accepts four forms:
|
|
1709
|
+
|
|
1710
|
+
- **Literal** — a branch name, tag, or commit hash passed directly to the VCS (e.g. `main`, `v1.2.3`, `abc123`).
|
|
1711
|
+
- **`$latest`** — resolves to the newest **stable semver tag** (`vX.Y.Z` or `X.Y.Z`, no pre-release suffix), consistently across all providers. GitHub first checks the published "latest release" and accepts it when it is semver; all providers scan tags filtered by the semver pattern.
|
|
1712
|
+
- **`$recent`** — resolves to the most **recently created or published tag**, regardless of its name format. Use this when you want whatever was tagged last, even if it is a nightly or pre-release build. For `git:` remotes the ordering is determined by `git ls-remote` output rather than creation date (date-based ordering requires fetching tag objects and is not supported).
|
|
1713
|
+
- **`/pattern/[flags]`** — a regex pattern of the form `/body/` or `/body/flags` (e.g. `ref: /^v1\.\d+\.\d+$/`). Resolves to the first tag whose name matches the pattern, ordered newest-first on GitHub, GitLab, and Bitbucket. For `git:` remotes the match order follows `git ls-remote` output. Flags such as `i` are supported. Note: the pattern body must be non-empty — `//` is treated as a literal ref, not a match-all regex. The stateful flags `g` and `y` are silently stripped; any other unrecognised flag produces an error.
|
|
1714
|
+
|
|
1715
|
+
| Form | Meaning |
|
|
1716
|
+
| -------------- | ------------------------------------------------ |
|
|
1717
|
+
| `ref: $latest` | Newest `vX.Y.Z` / `X.Y.Z` stable tag |
|
|
1718
|
+
| `ref: $recent` | Most recently created/published tag (any format) |
|
|
1719
|
+
| `ref: /^v1\./` | Latest tag matching the regex |
|
|
1720
|
+
| `ref: main` | Literal branch / tag / commit |
|
|
1721
|
+
|
|
1722
|
+
`$latest`, `$recent`, and `$self` are reserved and cannot be used as variable names.
|
|
1330
1723
|
|
|
1331
1724
|
When `ref` is omitted, all source types (GitHub, GitLab, Bitbucket, git) resolve to the repository's default branch.
|
|
1332
1725
|
|
|
1333
|
-
`$self` is a reserved keyword that expands to the absolute path of the active config file. It is injected automatically and cannot be used as a variable name. Use it anywhere a variable is valid — `exec:` commands, `replace:` rules, `exists:` conditions, `
|
|
1726
|
+
`$self` is a reserved keyword that expands to the absolute path of the active config file. It is injected automatically and cannot be used as a variable name. Use it anywhere a variable is valid — `exec:` commands, `replace:` rules, `exists:` conditions, `on.write` scripts, or any source field:
|
|
1334
1727
|
|
|
1335
1728
|
```yaml
|
|
1336
1729
|
files:
|
|
@@ -1358,7 +1751,9 @@ When the config is specified as a remote spec (e.g. `--config github:org/repo:.a
|
|
|
1358
1751
|
|
|
1359
1752
|
In addition to `$self` and `$latest`, avanti injects several variables automatically at the start of every run. These names are reserved and cannot be used in `variables:`.
|
|
1360
1753
|
|
|
1361
|
-
**Per-file path variables**
|
|
1754
|
+
**Per-file path variables**, **pull-time variables**, and **system variables** are all available everywhere variables are resolved: source URLs, `ref:`, conditions, `replace:`, `on.write` scripts, template rendering, and `backup:` patterns. Side-effect hooks (`on.beforeWrite`, `on.beforeCreate`, `on.beforeUpdate`, `on.create`, `on.update`) do not resolve avanti variables — use `$AVANTI_TARGET` and `$AVANTI_IS_NEW` env vars instead.
|
|
1755
|
+
|
|
1756
|
+
**Per-file path variables** — avanti derives the following variables from each file entry's resolved target path.
|
|
1362
1757
|
|
|
1363
1758
|
Example with working directory `/home/user/project` and map key `configs/app.yaml`:
|
|
1364
1759
|
|
|
@@ -1371,7 +1766,7 @@ Example with working directory `/home/user/project` and map key `configs/app.yam
|
|
|
1371
1766
|
| `$dirname` | `/home/user/project/configs` |
|
|
1372
1767
|
| `$basedir` | `configs` |
|
|
1373
1768
|
|
|
1374
|
-
> **Availability in source URLs and conditions:** per-file path variables are only resolved before the fetch when the map key is a fixed (non-directory) path. They are always available in processors (`replace:`, `
|
|
1769
|
+
> **Availability in source URLs and conditions:** per-file path variables are only resolved before the fetch when the map key is a fixed (non-directory) path. They are always available in processors (`replace:`, `on.write` scripts, template rendering) and `backup:`. Side-effect hooks (`on.create`, `on.update`, etc.) do not resolve avanti variables.
|
|
1375
1770
|
|
|
1376
1771
|
```yaml
|
|
1377
1772
|
variables:
|
|
@@ -1393,13 +1788,14 @@ files:
|
|
|
1393
1788
|
if:
|
|
1394
1789
|
exists: $path
|
|
1395
1790
|
|
|
1396
|
-
# $dirname in
|
|
1791
|
+
# $dirname in on.write — transform content using the file's directory name
|
|
1397
1792
|
app/config.yaml:
|
|
1398
1793
|
src: github:org/repo/app/config.yaml
|
|
1399
|
-
|
|
1794
|
+
on:
|
|
1795
|
+
write: sed "s|__DIR__|$dirname|g"
|
|
1400
1796
|
```
|
|
1401
1797
|
|
|
1402
|
-
**Pull-time variables** — injected once at the start of every run and available everywhere (source URLs, conditions, `replace:`, `
|
|
1798
|
+
**Pull-time variables** — injected once at the start of every run and available everywhere (source URLs, conditions, `replace:`, `on.write` scripts, template rendering, `backup:`):
|
|
1403
1799
|
|
|
1404
1800
|
| Variable | Value | Example |
|
|
1405
1801
|
| ----------- | --------------------------------------- | --------------------- |
|
|
@@ -1420,6 +1816,34 @@ files:
|
|
|
1420
1816
|
to: $datetime
|
|
1421
1817
|
```
|
|
1422
1818
|
|
|
1819
|
+
**System variables** — injected once per run and reflect the machine running avanti. Useful for downloading the correct release artifact for the current OS and CPU architecture:
|
|
1820
|
+
|
|
1821
|
+
| Variable | `linux` | `darwin` | `win32` |
|
|
1822
|
+
| -------- | ------- | -------- | --------- |
|
|
1823
|
+
| `$os` | `linux` | `darwin` | `windows` |
|
|
1824
|
+
|
|
1825
|
+
| Variable | `x64` | `arm64` | `ia32` | `arm` |
|
|
1826
|
+
| ---------- | -------- | ------- | ------ | ----- |
|
|
1827
|
+
| `$arch` | `x86_64` | `arm64` | `i686` | `arm` |
|
|
1828
|
+
| `$arch_go` | `amd64` | `arm64` | `386` | `arm` |
|
|
1829
|
+
|
|
1830
|
+
`$arch` uses GNU-triple / Rust naming (`x86_64`). `$arch_go` uses Go / Docker / Kubernetes naming (`amd64`). The `arm64` value is identical in both. Unknown `process.platform` or `process.arch` values are passed through unchanged.
|
|
1831
|
+
|
|
1832
|
+
```yaml
|
|
1833
|
+
variables:
|
|
1834
|
+
rg_version: '14.1.1'
|
|
1835
|
+
kubectl_version: '1.32.0'
|
|
1836
|
+
|
|
1837
|
+
files:
|
|
1838
|
+
# Download the ripgrep tarball for the current system (Rust/GNU naming)
|
|
1839
|
+
releases/ripgrep.tar.gz:
|
|
1840
|
+
src: https://github.com/BurntSushi/ripgrep/releases/download/$rg_version/ripgrep-$rg_version-$arch-unknown-$os.tar.gz
|
|
1841
|
+
|
|
1842
|
+
# Download kubectl for the current system (Go naming)
|
|
1843
|
+
bin/kubectl:
|
|
1844
|
+
src: https://dl.k8s.io/release/v$kubectl_version/bin/$os/$arch_go/kubectl
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1423
1847
|
### $self — Self-managing Config
|
|
1424
1848
|
|
|
1425
1849
|
The special `$self` key in the `files:` map tells avanti to manage its own config file. When `$self` is present, avanti fetches the listed sources and uses the result as the active config for the rest of the run — all in a single invocation.
|
|
@@ -1462,7 +1886,7 @@ files:
|
|
|
1462
1886
|
arrays: concat
|
|
1463
1887
|
```
|
|
1464
1888
|
|
|
1465
|
-
`$self` supports all the same source types, `replace`, `
|
|
1889
|
+
`$self` supports all the same source types, `replace`, `on.write`, and YAML/JSON merge options as any other file entry. Lifecycle hooks (`on.beforeWrite`, `on.beforeCreate`, `on.beforeUpdate`, `on.create`, `on.update`) are not supported for `$self` — they require a confirmed write context that the config re-evaluation pass does not have. See [Self-managing Config](#self-managing-config) in the Use Cases section for a full worked example.
|
|
1466
1890
|
|
|
1467
1891
|
### Authentication
|
|
1468
1892
|
|