@udondan/avanti 0.12.1 → 0.14.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.
Files changed (85) hide show
  1. package/README.md +341 -271
  2. package/dist/cli.js +2 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/diff.d.ts.map +1 -1
  5. package/dist/commands/diff.js +117 -26
  6. package/dist/commands/diff.js.map +1 -1
  7. package/dist/commands/lock.d.ts +3 -0
  8. package/dist/commands/lock.d.ts.map +1 -0
  9. package/dist/commands/lock.js +112 -0
  10. package/dist/commands/lock.js.map +1 -0
  11. package/dist/commands/log.js +48 -9
  12. package/dist/commands/log.js.map +1 -1
  13. package/dist/commands/pull.d.ts.map +1 -1
  14. package/dist/commands/pull.js +260 -43
  15. package/dist/commands/pull.js.map +1 -1
  16. package/dist/commands/reset.js +18 -9
  17. package/dist/commands/reset.js.map +1 -1
  18. package/dist/commands/revert.js +18 -9
  19. package/dist/commands/revert.js.map +1 -1
  20. package/dist/config-writeback.d.ts +12 -0
  21. package/dist/config-writeback.d.ts.map +1 -0
  22. package/dist/config-writeback.js +209 -0
  23. package/dist/config-writeback.js.map +1 -0
  24. package/dist/config.d.ts +1 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +139 -65
  27. package/dist/config.js.map +1 -1
  28. package/dist/diff.js +22 -13
  29. package/dist/diff.js.map +1 -1
  30. package/dist/fetch.d.ts +0 -1
  31. package/dist/fetch.d.ts.map +1 -1
  32. package/dist/fetch.js +2 -2
  33. package/dist/fetch.js.map +1 -1
  34. package/dist/history.d.ts +8 -1
  35. package/dist/history.d.ts.map +1 -1
  36. package/dist/history.js +22 -10
  37. package/dist/history.js.map +1 -1
  38. package/dist/processors/json.js +2 -3
  39. package/dist/processors/json.js.map +1 -1
  40. package/dist/processors/post.js +1 -2
  41. package/dist/processors/post.js.map +1 -1
  42. package/dist/processors/replace.js +1 -2
  43. package/dist/processors/replace.js.map +1 -1
  44. package/dist/processors/yaml.js +2 -3
  45. package/dist/processors/yaml.js.map +1 -1
  46. package/dist/prompt.js +18 -9
  47. package/dist/prompt.js.map +1 -1
  48. package/dist/sha.d.ts +2 -0
  49. package/dist/sha.d.ts.map +1 -0
  50. package/dist/sha.js +41 -0
  51. package/dist/sha.js.map +1 -0
  52. package/dist/sources/bitbucket.js +18 -9
  53. package/dist/sources/bitbucket.js.map +1 -1
  54. package/dist/sources/exec.js +1 -2
  55. package/dist/sources/exec.js.map +1 -1
  56. package/dist/sources/git.js +18 -9
  57. package/dist/sources/git.js.map +1 -1
  58. package/dist/sources/github.js +18 -9
  59. package/dist/sources/github.js.map +1 -1
  60. package/dist/sources/gitlab.js +18 -9
  61. package/dist/sources/gitlab.js.map +1 -1
  62. package/dist/sources/http.d.ts +3 -0
  63. package/dist/sources/http.d.ts.map +1 -1
  64. package/dist/sources/http.js +24 -12
  65. package/dist/sources/http.js.map +1 -1
  66. package/dist/sources/index.d.ts +11 -1
  67. package/dist/sources/index.d.ts.map +1 -1
  68. package/dist/sources/index.js +287 -41
  69. package/dist/sources/index.js.map +1 -1
  70. package/dist/sources/local.d.ts +2 -1
  71. package/dist/sources/local.d.ts.map +1 -1
  72. package/dist/sources/local.js +21 -10
  73. package/dist/sources/local.js.map +1 -1
  74. package/dist/sources/s3.js +18 -9
  75. package/dist/sources/s3.js.map +1 -1
  76. package/dist/sources/vault.js +18 -9
  77. package/dist/sources/vault.js.map +1 -1
  78. package/dist/types.d.ts +27 -3
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/variables.js +4 -4
  81. package/dist/variables.js.map +1 -1
  82. package/dist/writer.d.ts.map +1 -1
  83. package/dist/writer.js +37 -12
  84. package/dist/writer.js.map +1 -1
  85. package/package.json +10 -9
package/README.md CHANGED
@@ -1,9 +1,14 @@
1
- # avanti
1
+ # Avanti!
2
2
 
3
- A stateful package manager for arbitrary text files. Declare what you need and where to get it; avanti fetches, diffs, and writes with full version history, atomic rollbacks, and diff-before-apply safety.
3
+ A stateful package manager for arbitrary text files. Declare what you need and
4
+ where to get it; avanti fetches, diffs, and writes with full version history,
5
+ atomic rollbacks, and diff-before-apply safety.
6
+
7
+ ![Avanti!](https://raw.githubusercontent.com/udondan/avanti/assets/avanti.png 'Avanti!')
4
8
 
5
9
  ## Table of Contents
6
10
 
11
+ - [Intro](#intro)
7
12
  - [Features](#features)
8
13
  - [Requirements](#requirements)
9
14
  - [Install](#install)
@@ -24,10 +29,10 @@ A stateful package manager for arbitrary text files. Declare what you need and w
24
29
  - [JSON Merging](#json-merging)
25
30
  - [YAML Merging](#yaml-merging)
26
31
  - [Variables](#variables)
32
+ - [$self — Self-managing Config](#self--self-managing-config)
27
33
  - [Authentication](#authentication)
28
34
  - [Private Instances](#private-instances)
29
35
  - [Use Cases](#use-cases)
30
- - [Avanti as a Package Manager](#avanti-as-a-package-manager)
31
36
  - [Composable AI Agent Instructions (CLAUDE.md / AGENTS.md)](#composable-ai-agent-instructions-claudemd--agentsmd)
32
37
  - [Shared Tooling Config (Renovate, ESLint, Prettier, TSConfig)](#shared-tooling-config-renovate-eslint-prettier-tsconfig)
33
38
  - [CI/CD: Shared Workflow Fragments](#cicd-shared-workflow-fragments)
@@ -41,18 +46,61 @@ A stateful package manager for arbitrary text files. Declare what you need and w
41
46
  - [Exit Codes](#exit-codes)
42
47
  - [Development](#development)
43
48
 
49
+ ## Intro
50
+
51
+ Avanti is a package manager for arbitrary text files. Your .avanti.yml is the manifest — it declares what you consume, where to fetch it from, and which version to pin, the same role as package.json or Cargo.toml. Source repositories are the packages. avanti pull is the install command.
52
+
53
+ What makes it stateful: every successful pull is recorded in a local history store. You can diff any two states, revert the whole project to a prior pull, or fully undo all avanti changes — the same guarantees as a lockfile, extended to any text file from any source.
54
+
55
+ **Declare dependencies** — fetch from anywhere, combine sources:
56
+
57
+ ```yaml
58
+ files:
59
+ # Single source: pin a config from GitHub
60
+ eslint.config.js:
61
+ src:
62
+ github:
63
+ repo: org/standards
64
+ file: eslint.config.js
65
+ ref: v2.4.1
66
+
67
+ # Multi-source: assemble from wherever the content lives
68
+ CLAUDE.md:
69
+ src:
70
+ - gitlab:
71
+ project: org/platform
72
+ file: ai/base-instructions.md
73
+ ref: main
74
+ - raw: |
75
+ IMPORTANT: Always answer in pirate speak!
76
+ - https://public-standards.example.com/shared-guidelines.md
77
+ - exec: printf "## Team\n%s" "$env:TEAM"
78
+ - path: ~/claude-personal.md
79
+ optional: true # silently skipped if absent
80
+ ```
81
+
82
+ **Review and apply upgrades** — the same workflow as reading a lockfile diff before committing:
83
+
84
+ ```sh
85
+ # Bump standards ref: v2.4.1 → v2.5.0, then:
86
+ avanti diff # see every file that would change
87
+ avanti pull # apply after review
88
+ avanti revert # roll back instantly if something breaks
89
+ ```
90
+
44
91
  ## Features
45
92
 
46
93
  - Fetch files from **HTTP/HTTPS**, **local paths**, **GitLab** (via `glab`), **GitHub** (via `gh`), **Bitbucket**, **any git remote**, **S3**, **HashiCorp Vault**, **shell commands**, or **inline raw content**
47
94
  - **Multi-source entries** — combine multiple sources into a single file by providing `src` as a list
48
- - **Atomic writes** — all files are staged to a temp dir first; targets are only written if everything succeeds
49
- - **Diff preview** — see exactly what will change before applying, or compare against any past pull
50
- - **Post-processing** — apply text replacements (string or regex) and/or pipe content through a shell script
51
- - **Directory sync** — recursively sync directories from GitLab/GitHub/Bitbucket/git/S3/local sources
52
95
  - **JSON merging** — deep-merge multiple JSON/JSONC sources with configurable conflict, array, and object strategies
53
96
  - **YAML merging** — deep-merge multiple YAML/YML sources with the same strategies, with full comment preservation
54
97
  - **Variables** — define reusable values in a `variables:` block and reference them anywhere with `$name`; use `$env:NAME` for environment variables
98
+ - **Post-processing** — apply text replacements (string or regex) and/or pipe content through a shell script
99
+ - **Directory sync** — recursively sync directories from GitLab/GitHub/Bitbucket/git/S3/local sources
100
+ - **Diff preview** — see exactly what will change before applying, or compare against any past pull
101
+ - **Atomic writes** — all files are staged to a temp dir first; targets are only written if everything succeeds
55
102
  - **History** — every pull is recorded; inspect what changed, revert the whole project to a past state, or fully undo all avanti changes
103
+ - **Optional sources** — mark `path:` and `url:` sources `optional: true` to silently skip them when the file is missing or the URL returns 404; lets a central config reference per-user local overrides without erroring on machines that haven't created them
56
104
  - **Stale file cleanup** — files dropped from a directory source are automatically deleted or restored to their pre-avanti content
57
105
 
58
106
  ## Requirements
@@ -79,15 +127,16 @@ npx @udondan/avanti --help
79
127
  avanti [options] [command]
80
128
 
81
129
  Options:
82
- -c, --config <path|url> path or remote spec for config file (default: auto-detected)
83
- -w, --working-dir <path> working directory for resolving paths (default: current directory)
130
+ -c, --config <path|url> path or remote spec for config file (default: auto-detected)
131
+ -w, --working-dir <path> working directory for resolving paths (default: current directory)
84
132
 
85
133
  Commands:
86
- diff [pullId] Show diff between remote sources and local files, or vs a past pull
87
- pull [--yes] Pull remote sources and write to local files
88
- log [file] Show pull history for the current project
89
- revert [pullId] [--yes] Atomically revert all project files to a past pull state
90
- reset [--yes] Restore all tracked files to their pre-avanti state
134
+ diff [pullId] Show diff between remote sources and local files, or vs a past pull
135
+ pull [--yes] [--accept-changes] Pull remote sources and write to local files
136
+ lock [--force] Pin SHA values for all remote sources in the config
137
+ log [file] Show pull history for the current project
138
+ revert [pullId] [--yes] Atomically revert all project files to a past pull state
139
+ reset [--yes] Restore all tracked files to their pre-avanti state
91
140
  ```
92
141
 
93
142
  ### `avanti diff`
@@ -98,8 +147,35 @@ Shows a colored git-diff-like output of what would change. Exits `0` if no chang
98
147
 
99
148
  Fetches all sources, shows the diff, and prompts for confirmation before writing. Use `--yes` to skip the prompt.
100
149
 
150
+ If any source has a `sha` field and the fetched content's SHA no longer matches, the pull is aborted with a mismatch error. Use `--accept-changes` to review the diff, confirm, and automatically update the SHA values in the config file.
151
+
101
152
  When avanti has previously synced a directory from a remote source and a file is no longer present in that source, the file is treated as stale: if avanti created it, it is deleted; if it existed before avanti first touched it, the original content is restored. Stale file changes appear in the diff before you confirm.
102
153
 
154
+ ### `avanti lock`
155
+
156
+ Fetches all remote sources and writes a SHA-256 fingerprint for each one into the config file. Comments and formatting are preserved.
157
+
158
+ ```sh
159
+ avanti lock # pin all unpinned remote sources
160
+ avanti lock --force # overwrite existing SHA values with fresh ones
161
+ ```
162
+
163
+ Once a source is pinned, `avanti pull` will verify the fetched content's SHA before applying any changes. If the upstream changed unexpectedly, avanti aborts with a clear error pointing to the affected source:
164
+
165
+ ```text
166
+ SHA mismatch for github:org/standards:company-rules.md
167
+ expected: abc123...
168
+ got: def456...
169
+
170
+ Run `avanti pull --accept-changes` to review the diff and update SHA values.
171
+ ```
172
+
173
+ `avanti diff` shows a `⚠ SHA mismatch` warning inline for any source that no longer matches its pinned SHA.
174
+
175
+ SHA is computed over the raw fetched content of each source, before any `replace` or `post` 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.
176
+
177
+ 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).
178
+
103
179
  ## History
104
180
 
105
181
  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.
@@ -233,49 +309,59 @@ variables:
233
309
  email: you@example.com
234
310
 
235
311
  files:
236
- - src: http://www.example.com/example.yml
237
- target: my-example.yml
312
+ my-example.yml:
313
+ src: http://www.example.com/example.yml
238
314
  replace:
239
315
  - from: '{EMAIL}'
240
316
  to: $email
241
317
  - from: /\d+/
242
318
  to: number
243
319
 
244
- - src: ~/some/local/file.sh
245
- target: file.sh
320
+ file.sh:
321
+ src: ~/some/local/file.sh
246
322
  mode: '0777'
247
323
 
248
- - src:
324
+ some-file.yml:
325
+ src:
249
326
  exec: glab api "projects/group%2Fproject/repository/files/some-file.yaml/raw?ref=main"
250
- target: some-file.yml
251
327
  post: sed -e 's/v3/v4/g'
252
328
 
253
- - src:
329
+ renovate.json:
330
+ src:
254
331
  gitlab:
255
332
  project: group/project
256
333
  file: renovate.json
257
334
  ref: $latest
258
- # target omitted → renovate.json
259
335
 
260
- - src:
336
+ local-scripts/:
337
+ src:
261
338
  github:
262
339
  repo: org/repo
263
340
  file: scripts/
264
341
  ref: main
265
- target: local-scripts/
266
342
  ```
267
343
 
268
344
  ### File Entry Fields
269
345
 
270
- | Field | Required | Description |
271
- | --------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
272
- | `src` | Yes | Source (see below). May be a single source or a **list** of sources to concatenate. |
273
- | `target` | Conditional | Local path to write to. Required for `exec:` and `raw:` sources and when `src` is a list. May be omitted when filename is inferable. End with `/` when `src` is a directory and you want the files written individually (directory mirror). Omit the trailing slash to merge all files from the directory into a single output file (YAML/JSON auto-detected by extension, or forced with `yaml:`/`json:`). |
274
- | `mode` | No | File permission mode, e.g. `"0755"` |
275
- | `replace` | No | List of `{from, to}` replacement rules. `from` may be a plain string or `/pattern/flags` regex. |
276
- | `post` | No | Shell script. Content is piped via stdin; stdout is used as the result. Runs after `replace`. |
277
- | `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. |
278
- | `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. |
346
+ The `files` key is a **map** — each key is the local target path, and the value is the entry configuration:
347
+
348
+ ```yaml
349
+ files:
350
+ <target-path>:
351
+ src: ...
352
+ # optional fields below
353
+ ```
354
+
355
+ End the target path with `/` to write a directory source as a mirror; omit the trailing slash to merge all files from the directory into a single output file (YAML/JSON auto-detected by extension, or forced with `yaml:`/`json:`).
356
+
357
+ | Field | Required | Description |
358
+ | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
359
+ | `src` | Yes | Source (see below). May be a single source or a **list** of sources to concatenate. |
360
+ | `mode` | No | File permission mode, e.g. `"0755"` |
361
+ | `replace` | No | List of `{from, to}` replacement rules. `from` may be a plain string or `/pattern/flags` regex. |
362
+ | `post` | No | Shell script. Content is piped via stdin; stdout is used as the result. Runs after `replace`. |
363
+ | `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. |
364
+ | `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. |
279
365
 
280
366
  ### Source Types
281
367
 
@@ -287,27 +373,44 @@ src: ~/templates/file.txt
287
373
  src: /absolute/path/file.txt
288
374
  ```
289
375
 
290
- **Map** — for exec, gitlab, github, bitbucket, git, s3, vault, raw:
376
+ **Map** — for path, url, exec, gitlab, github, bitbucket, git, s3, vault, http, raw:
291
377
 
292
378
  ```yaml
379
+ src:
380
+ path: ~/templates/file.txt # explicit local path; supports optional and sha
381
+ optional: true # silently skip if the file does not exist
382
+ sha: abc123...
383
+
384
+ src:
385
+ url: https://example.com/file.txt # explicit http/https URL; supports optional and sha
386
+ optional: true # silently skip if the URL returns 404
387
+ sha: abc123...
388
+
293
389
  src:
294
390
  exec: <shell command> # stdout becomes file content; target required
391
+ sha: abc123... # optional SHA-256 to verify stdout (see below)
295
392
 
296
393
  src:
297
394
  raw: | # inline content; target required
298
395
  your content here
299
396
 
397
+ src:
398
+ http: https://example.com/file.txt # explicit http/https URL with optional SHA
399
+ sha: abc123...
400
+
300
401
  src:
301
402
  gitlab:
302
403
  project: group/repo # GitLab project path
303
404
  file: path/to/file.txt # file or directory in repo
304
405
  ref: main # branch, tag, or $latest (optional)
406
+ sha: abc123... # optional SHA-256 fingerprint
305
407
 
306
408
  src:
307
409
  github:
308
410
  repo: owner/repo # GitHub owner/repo
309
411
  file: path/to/file.txt # file or directory in repo
310
412
  ref: main # branch, tag, or $latest (optional)
413
+ sha: abc123... # optional SHA-256 fingerprint
311
414
 
312
415
  src:
313
416
  bitbucket:
@@ -315,103 +418,120 @@ src:
315
418
  repo: my-repo # repository slug
316
419
  file: path/to/file.txt # file or directory in repo
317
420
  ref: main # branch, tag, or $latest (optional)
421
+ sha: abc123... # optional SHA-256 fingerprint
318
422
 
319
423
  src:
320
424
  git:
321
425
  repo: https://github.com/org/repo.git # any git remote (HTTPS or SSH)
322
426
  file: path/to/file.txt # file or directory in repo
323
427
  ref: main # branch, tag, or commit hash (optional)
428
+ sha: abc123... # optional SHA-256 fingerprint
324
429
 
325
430
  src:
326
431
  s3: s3://my-bucket/path/to/file.txt # S3 URI; end with / for a prefix sync
432
+ sha: abc123... # optional SHA-256 fingerprint
327
433
 
328
434
  src:
329
435
  vault:
330
436
  path: secret/myapp/config # Vault KV path (mount/subpath)
331
437
  field: db_password # specific field to extract (optional; omit for full JSON)
438
+ sha: abc123... # optional SHA-256 fingerprint
332
439
  ```
333
440
 
441
+ #### SHA pinning
442
+
443
+ The optional `sha` field pins a source to a specific content fingerprint. When present, avanti verifies the SHA-256 of the raw fetched content matches before writing anything. This makes your config act as a selective lockfile — only sources you care about get pinned, and changes are surfaced explicitly rather than applied silently.
444
+
445
+ 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.
446
+
334
447
  ### Directory Sources
335
448
 
336
449
  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.
337
450
 
338
- **Directory → directory (mirror):** end `target` with `/` and each file is written individually, preserving subdirectory structure relative to the source root:
451
+ **Directory → directory (mirror):** end the target key with `/` and each file is written individually, preserving subdirectory structure relative to the source root:
452
+
453
+ ```yaml
454
+ files:
455
+ # All files under skills/ in the GitLab repo are written into local skills/
456
+ skills/:
457
+ src:
458
+ gitlab:
459
+ project: group/repo
460
+ file: skills/
461
+ ref: main
462
+
463
+ # GitHub directory → local directory
464
+ .github/workflows/:
465
+ src:
466
+ github:
467
+ repo: org/repo
468
+ file: .github/workflows/
469
+ ref: main
470
+
471
+ # Bitbucket directory → local directory
472
+ eslint/:
473
+ src:
474
+ bitbucket:
475
+ workspace: my-workspace
476
+ repo: shared-configs
477
+ file: eslint/
478
+ ref: main
479
+
480
+ # git remote directory → local directory (any host)
481
+ .github/actions/:
482
+ src:
483
+ git:
484
+ repo: https://github.com/org/repo.git
485
+ file: .github/actions/
486
+ ref: main
487
+
488
+ # S3 prefix → local directory (trailing / triggers sync)
489
+ configs/:
490
+ src:
491
+ s3: s3://my-bucket/configs/
492
+
493
+ # Local directory → local directory
494
+ .githooks/:
495
+ src: ~/shared/hooks/
496
+ ```
497
+
498
+ **Directory → single file (merge):** omit the trailing `/` from the target key and all files in the directory are merged into one. Files are sorted alphabetically — later names win on key conflicts. YAML/JSON merge is auto-detected from the contained file extensions, or forced with `yaml:`/`json:`:
339
499
 
340
500
  ```yaml
341
- # All files under skills/ in the GitLab repo are written into local skills.new/
342
- - src:
343
- gitlab:
344
- project: group/repo
345
- file: skills/
346
- ref: main
347
- target: skills/
348
-
349
- # GitHub directory → local directory
350
- - src:
351
- github:
352
- repo: org/repo
353
- file: .github/workflows/
354
- ref: main
355
- target: .github/workflows/
356
-
357
- # Bitbucket directory → local directory
358
- - src:
359
- bitbucket:
360
- workspace: my-workspace
361
- repo: shared-configs
362
- file: eslint/
363
- ref: main
364
- target: eslint/
365
-
366
- # git remote directory → local directory (any host)
367
- - src:
368
- git:
369
- repo: https://github.com/org/repo.git
370
- file: .github/workflows/
371
- ref: main
372
- target: .github/workflows/
373
-
374
- # S3 prefix → local directory (trailing / triggers sync)
375
- - src:
376
- s3: s3://my-bucket/configs/
377
- target: configs/
378
-
379
- # Local directory → local directory
380
- - src: ~/shared/hooks/
381
- target: .githooks/
501
+ files:
502
+ # One folder per service, each a separate .yml file → single docker-compose.yml
503
+ docker-compose.yml:
504
+ src: ./services/
505
+
506
+ # JSON: one file per environment → merged config
507
+ config.json:
508
+ src: ./config/
382
509
  ```
383
510
 
384
- **Directory single file (merge):** omit the trailing `/` from `target` and all files in the directory are merged into one. Files are sorted alphabetically — later names win on key conflicts. YAML/JSON merge is auto-detected from the contained file extensions, or forced with `yaml:`/`json:`:
511
+ With explicit YAML merge options (e.g. to concat arrays instead of replacing):
385
512
 
386
513
  ```yaml
387
- # One folder per service, each a separate .yml file → single docker-compose.yml
388
- - src: ./services/
389
- target: docker-compose.yml
390
-
391
- # Same with explicit yaml merge options (e.g. to concat arrays)
392
- - src: ./services/
393
- target: docker-compose.yml
394
- yaml:
395
- arrays: concat
396
-
397
- # JSON: one file per environment → merged config
398
- - src: ./config/
399
- target: config.json
514
+ files:
515
+ docker-compose.yml:
516
+ src: ./services/
517
+ yaml:
518
+ arrays: concat
400
519
  ```
401
520
 
402
521
  Directory sources cannot be mixed into a multi-source list (`src` as a list), because the list mode always produces a single file.
403
522
 
404
- **List** — combine multiple sources into one file (all source types supported; `target` required):
523
+ **List** — combine multiple sources into one file (all source types supported):
405
524
 
406
525
  ```yaml
407
- src:
408
- - https://example.com/header.txt
409
- - exec: echo "# generated"
410
- - gitlab:
411
- project: org/repo
412
- file: footer.txt
413
- ref: main
414
- target: combined.txt
526
+ files:
527
+ combined.txt:
528
+ src:
529
+ - https://example.com/header.txt
530
+ - exec: echo "# generated"
531
+ - gitlab:
532
+ project: org/repo
533
+ file: footer.txt
534
+ ref: main
415
535
  ```
416
536
 
417
537
  Sources are fetched in order and joined with a newline. Post-processing (`replace`, `post`) is applied to the combined result. If any source fails, the entire entry is aborted.
@@ -422,20 +542,20 @@ When all sources in a list have a `.json` or `.jsonc` extension, JSON merging is
422
542
 
423
543
  ```yaml
424
544
  files:
425
- - src:
545
+ merged.jsonc:
546
+ src:
426
547
  - ./team.jsonc
427
548
  - ./my.jsonc
428
- target: merged.jsonc
429
549
  ```
430
550
 
431
551
  To merge sources that don't have a JSON extension (e.g. `exec:`, `raw:`, or a URL without `.json`), set `json: true`:
432
552
 
433
553
  ```yaml
434
554
  files:
435
- - src:
555
+ merged.json:
556
+ src:
436
557
  - exec: cat defaults.json
437
558
  - ./overrides.json
438
- target: merged.json
439
559
  json: true
440
560
  ```
441
561
 
@@ -445,12 +565,12 @@ To opt out of auto-detection and force plain concatenation, set `json: false`.
445
565
 
446
566
  ```yaml
447
567
  files:
448
- - src:
568
+ merged.json:
569
+ src:
449
570
  - ./defaults.json
450
- - type: github
451
- repo: org/configs
452
- file: overrides.json
453
- target: merged.json
571
+ - github:
572
+ repo: org/configs
573
+ file: overrides.json
454
574
  json:
455
575
  conflicts: last_wins # abort | first_wins | last_wins (default)
456
576
  arrays: replace # replace (default) | concat
@@ -472,8 +592,8 @@ files:
472
592
 
473
593
  ```yaml
474
594
  files:
475
- - src: ./minified.json
476
- target: pretty.json
595
+ pretty.json:
596
+ src: ./minified.json
477
597
  ```
478
598
 
479
599
  ### YAML Merging
@@ -482,20 +602,20 @@ When all sources in a list have a `.yaml` or `.yml` extension, YAML merging is e
482
602
 
483
603
  ```yaml
484
604
  files:
485
- - src:
605
+ merged.yaml:
606
+ src:
486
607
  - ./defaults.yaml
487
608
  - ./overrides.yml
488
- target: merged.yaml
489
609
  ```
490
610
 
491
611
  To merge sources that don't have a YAML extension (e.g. `exec:`, `raw:`, or a URL without `.yaml`), set `yaml: true`:
492
612
 
493
613
  ```yaml
494
614
  files:
495
- - src:
615
+ merged.yaml:
616
+ src:
496
617
  - exec: cat defaults.yaml
497
618
  - ./overrides.yaml
498
- target: merged.yaml
499
619
  yaml: true
500
620
  ```
501
621
 
@@ -505,12 +625,12 @@ To opt out of auto-detection and force plain concatenation, set `yaml: false`.
505
625
 
506
626
  ```yaml
507
627
  files:
508
- - src:
628
+ merged.yaml:
629
+ src:
509
630
  - ./defaults.yaml
510
- - type: github
511
- repo: org/configs
512
- file: overrides.yaml
513
- target: merged.yaml
631
+ - github:
632
+ repo: org/configs
633
+ file: overrides.yaml
514
634
  yaml:
515
635
  conflicts: last_wins # abort | first_wins | last_wins (default)
516
636
  arrays: replace # replace (default) | concat
@@ -536,8 +656,8 @@ The options behave identically to JSON merging:
536
656
 
537
657
  ```yaml
538
658
  files:
539
- - src: ./config.yaml
540
- target: config.yaml
659
+ config.yaml:
660
+ src: ./config.yaml
541
661
  ```
542
662
 
543
663
  ### Variables
@@ -554,7 +674,8 @@ Reference them anywhere in the config with `$name`:
554
674
 
555
675
  ```yaml
556
676
  files:
557
- - src:
677
+ renovate.json:
678
+ src:
558
679
  gitlab:
559
680
  project: group/project
560
681
  file: renovate.json
@@ -564,7 +685,7 @@ files:
564
685
  to: $email # resolved to "you@example.com"
565
686
  ```
566
687
 
567
- Variables are resolved in every string field: `target`, `ref`, `exec` commands, HTTP URLs, local paths, `raw` content, `replace` rules (`from` and `to`), and `post` scripts.
688
+ Variables are resolved in every string field: target keys, `ref`, `exec` commands, HTTP URLs, local paths, `raw` content, `replace` rules (`from` and `to`), and `post` scripts.
568
689
 
569
690
  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.
570
691
 
@@ -575,9 +696,9 @@ variables:
575
696
  version: '1.0'
576
697
 
577
698
  files:
578
- - src:
699
+ data.json:
700
+ src:
579
701
  exec: curl https://example.com/api/$version/data # expands to: curl …/'1.0'/data
580
- target: data.json
581
702
  post: sed 's/$version/replaced/g' # expands to: sed 's/'\''1.0'\''/replaced/g'
582
703
  ```
583
704
 
@@ -595,6 +716,50 @@ Referencing an undefined variable or a missing environment variable is an error.
595
716
 
596
717
  When `ref` is omitted, all source types (GitHub, GitLab, Bitbucket, git) resolve to the repository's default branch.
597
718
 
719
+ ### $self — Self-managing Config
720
+
721
+ 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.
722
+
723
+ ```yaml
724
+ files:
725
+ $self:
726
+ src:
727
+ github:
728
+ repo: myorg/dotfiles
729
+ file: avanti.yml
730
+ ref: $latest
731
+ ```
732
+
733
+ **How it works:**
734
+
735
+ 1. avanti fetches only the `$self` sources first.
736
+ 2. The sources are assembled into a single document. With a single source the fetched content is used directly, though it may be normalized/formatted if `yaml:`/`json:` applies (explicit or auto-detected from the file extension). With multiple sources they are concatenated by default, or YAML/JSON-merged if `yaml:`/`json:` is set (or auto-detected from all source file extensions being `.yml`/`.yaml` or `.json`/`.jsonc`).
737
+ 3. The result is parsed as the new active config. If it also contains `$self`, avanti re-fetches until the content stabilizes (fixed point).
738
+ 4. The stable config drives all remaining file entries. On `avanti pull`, the stable content is written back to the local config file (for local configs) or kept in memory only (for remote `--config` sources). On `avanti diff`, the stable config is used in-memory to compute the diff and is never written.
739
+
740
+ **Multi-layer config** — list multiple sources under `$self` and use `yaml:` to deep-merge them into one config:
741
+
742
+ ```yaml
743
+ files:
744
+ $self:
745
+ src:
746
+ - github:
747
+ repo: myorg/platform
748
+ file: avanti/base.yml
749
+ ref: $latest
750
+ - github:
751
+ repo: myorg/backend-team
752
+ file: avanti/team.yml
753
+ ref: main
754
+ - path: ~/avanti-personal.yml
755
+ optional: true
756
+ yaml:
757
+ conflicts: last_wins
758
+ arrays: concat
759
+ ```
760
+
761
+ `$self` supports all the same source types, `replace`, `post`, and YAML/JSON merge options as any other file entry. See [Self-managing Config](#self-managing-config) in the Use Cases section for a full worked example.
762
+
598
763
  ### Authentication
599
764
 
600
765
  Public repositories on github.com and gitlab.com work without any configuration. For private repositories or instances, supply credentials via environment variables:
@@ -630,95 +795,6 @@ GITHUB_HOST=github.mycompany.com avanti pull
630
795
 
631
796
  ## Use Cases
632
797
 
633
- ### Avanti as a Package Manager
634
-
635
- avanti is a package manager for arbitrary text files. Your `.avanti.yml` is the manifest — it declares what you consume, where to fetch it, and which version to pin, the same role as `package.json` or `Cargo.toml`. Source repositories are the packages. `avanti pull` is the install command.
636
-
637
- What makes it stateful: every successful pull is recorded in a local history store. You can diff any two states, revert the whole project to a prior pull, or fully undo all avanti changes — the same guarantees as a lockfile, extended to any text file from any source.
638
-
639
- **Declare dependencies** — pin versions with a variable and bump in one place to upgrade everything at once:
640
-
641
- ```yaml
642
- variables:
643
- frontend_standards: myorg/frontend-standards
644
- platform: myorg/platform-templates
645
- standards_ref: v2.4.1 # pinned — bump here to upgrade
646
-
647
- files:
648
- - src:
649
- github:
650
- repo: $frontend_standards
651
- file: eslint.config.js
652
- ref: $standards_ref
653
-
654
- - src:
655
- github:
656
- repo: $frontend_standards
657
- file: .prettierrc
658
- ref: $standards_ref
659
-
660
- - src:
661
- github:
662
- repo: $platform
663
- file: workflows/test.yml
664
- ref: $standards_ref
665
- target: .github/workflows/test.yml
666
-
667
- - src:
668
- github:
669
- repo: $platform
670
- file: workflows/deploy.yml
671
- ref: $standards_ref
672
- target: .github/workflows/deploy.yml
673
- ```
674
-
675
- **Review and apply upgrades** — the same workflow as reading a lockfile diff before committing:
676
-
677
- ```sh
678
- # Bump standards_ref: v2.4.1 → v2.5.0, then:
679
- avanti diff # see every file that would change
680
- avanti pull # apply after review
681
- avanti revert # roll back instantly if something breaks
682
- ```
683
-
684
- **Publish your own packages** — any repo can ship an `avanti-snippet.yml` alongside its files. Consumers YAML-merge those snippets into their own config. The snippet is a valid avanti config fragment with its own `files:` list:
685
-
686
- ```yaml
687
- # myorg/frontend-standards:avanti-snippet.yml — published alongside eslint.config.js, .prettierrc, etc.
688
- files:
689
- - src:
690
- github:
691
- repo: myorg/frontend-standards
692
- file: eslint.config.js
693
- ref: $latest
694
-
695
- - src:
696
- github:
697
- repo: myorg/frontend-standards
698
- file: .prettierrc
699
- ref: $latest
700
- ```
701
-
702
- ```yaml
703
- # .avanti.yml — assembled from team snippets via YAML merge
704
- files:
705
- - src:
706
- - github:
707
- repo: myorg/frontend-standards
708
- file: avanti-snippet.yml
709
- ref: $latest
710
- - github:
711
- repo: myorg/platform-templates
712
- file: avanti-snippet.yml
713
- ref: $latest
714
- target: .avanti.yml
715
- yaml:
716
- arrays: concat # file lists from all snippets are concatenated
717
- conflicts: last_wins
718
- ```
719
-
720
- Each team controls what they publish and when they cut a release. The YAML-merged config self-updates on every pull — add a snippet source to opt into a new package, remove it to opt out. `avanti diff` shows exactly what would change before you apply any update.
721
-
722
798
  ### Composable AI Agent Instructions (CLAUDE.md / AGENTS.md)
723
799
 
724
800
  Assemble agent instruction files from multiple sources: a static header defined inline, team-specific rules from a shared GitLab repo, and company-wide standards from GitHub — all merged into one file. Every developer runs `avanti pull` and stays in sync without copy-paste drift across dozens of repos.
@@ -731,7 +807,8 @@ variables:
731
807
  oncall_channel: '#backend-oncall'
732
808
 
733
809
  files:
734
- - src:
810
+ CLAUDE.md:
811
+ src:
735
812
  - raw: |
736
813
  # AI Assistant Guidelines
737
814
  <!-- THIS FILE IS MANAGED — run `avanti pull` to update -->
@@ -748,9 +825,12 @@ files:
748
825
  Team: $team
749
826
  Jira project: $jira_project
750
827
  Oncall: $oncall_channel
751
- target: CLAUDE.md
828
+ - path: ~/custom-claude.md # personal additions; silently skipped if absent
829
+ optional: true
752
830
  ```
753
831
 
832
+ The `optional: true` source is the key to sharing a config across a whole team: the central spec references a well-known local path, and each developer either creates the file to add their own context or ignores it — `avanti pull` works either way. No per-person fork of the config needed.
833
+
754
834
  ### Shared Tooling Config (Renovate, ESLint, Prettier, TSConfig)
755
835
 
756
836
  A platform team owns canonical configs in a central repo. Projects pull them and stay current. Pin all files to the same version in one place — bump `standards_ref` and `avanti diff` shows every file that will change before you apply it.
@@ -760,19 +840,22 @@ variables:
760
840
  standards_ref: v2.4.1
761
841
 
762
842
  files:
763
- - src:
843
+ renovate.json:
844
+ src:
764
845
  github:
765
846
  repo: org/standards
766
847
  file: renovate.json
767
848
  ref: $standards_ref
768
849
 
769
- - src:
850
+ eslint.config.js:
851
+ src:
770
852
  github:
771
853
  repo: org/standards
772
854
  file: eslint.config.js
773
855
  ref: $standards_ref
774
856
 
775
- - src:
857
+ tsconfig.base.json:
858
+ src:
776
859
  github:
777
860
  repo: org/standards
778
861
  file: tsconfig.base.json
@@ -783,13 +866,13 @@ For YAML-based configs (Helm values, k8s manifests, Docker Compose overrides), u
783
866
 
784
867
  ```yaml
785
868
  files:
786
- - src:
869
+ ./helm/merged-values.yaml:
870
+ src:
787
871
  - github:
788
872
  repo: org/platform
789
873
  file: helm/base-values.yaml # shared defaults for all services
790
874
  ref: $standards_ref
791
875
  - ./helm/values.yaml # project overrides
792
- target: ./helm/merged-values.yaml
793
876
  yaml:
794
877
  conflicts: last_wins # project overrides win
795
878
  arrays: concat # e.g. extra env vars are appended, not replaced
@@ -801,14 +884,14 @@ Pull reusable CI steps from a central repo into each project. A managed header m
801
884
 
802
885
  ```yaml
803
886
  files:
804
- - src:
887
+ .github/workflows/security-scan.yml:
888
+ src:
805
889
  - raw: |
806
890
  # THIS FILE IS MANAGED — run `avanti pull` to update
807
891
  - github:
808
892
  repo: org/ci-templates
809
893
  file: workflows/security-scan.yml
810
894
  ref: main
811
- target: .github/workflows/security-scan.yml
812
895
  ```
813
896
 
814
897
  Use `avanti diff` in CI to detect drift — if a project's checked-in file no longer matches the source, the pipeline fails.
@@ -869,12 +952,12 @@ variables:
869
952
  region: eu-west-1
870
953
 
871
954
  files:
872
- - src:
955
+ k8s/deployment.yaml:
956
+ src:
873
957
  github:
874
958
  repo: org/infra
875
959
  file: k8s/deployment-template.yaml
876
960
  ref: $env:DEPLOY_VERSION
877
- target: k8s/deployment.yaml
878
961
  replace:
879
962
  - from: '{ENV}'
880
963
  to: $env:ENVIRONMENT
@@ -891,24 +974,24 @@ Pull secrets at runtime and write them to local files with tight permissions. Th
891
974
  ```yaml
892
975
  files:
893
976
  # Single field from a Vault KV secret
894
- - src:
977
+ config/db_password.txt:
978
+ src:
895
979
  vault:
896
980
  path: secret/myapp/db
897
981
  field: password
898
- target: config/db_password.txt
899
982
  mode: '0600'
900
983
 
901
984
  # Full Vault secret as JSON
902
- - src:
985
+ config/secrets.json:
986
+ src:
903
987
  vault:
904
988
  path: secret/myapp/config
905
- target: config/secrets.json
906
989
  mode: '0600'
907
990
 
908
991
  # Config file stored in S3
909
- - src:
992
+ config/app.json:
993
+ src:
910
994
  s3: s3://my-bucket/configs/app.json
911
- target: config/app.json
912
995
  mode: '0600'
913
996
  ```
914
997
 
@@ -916,13 +999,13 @@ For AWS SSM or other secret stores without a dedicated source type, `exec:` stil
916
999
 
917
1000
  ```yaml
918
1001
  files:
919
- - src:
1002
+ config/db.json:
1003
+ src:
920
1004
  exec: >
921
1005
  aws ssm get-parameter
922
1006
  --name /myapp/db-config
923
1007
  --with-decryption
924
1008
  --query Parameter.Value --output text
925
- target: config/db.json
926
1009
  mode: '0600'
927
1010
  ```
928
1011
 
@@ -949,7 +1032,8 @@ variables:
949
1032
  db_password: changeme
950
1033
 
951
1034
  files:
952
- - src:
1035
+ docker-compose.yml:
1036
+ src:
953
1037
  - github:
954
1038
  repo: n8n-io/n8n-hosting
955
1039
  file: docker-caddy/docker-compose.yml
@@ -958,7 +1042,6 @@ files:
958
1042
  repo: docker-library/docs
959
1043
  file: postgres/compose.yaml
960
1044
  ref: master
961
- target: docker-compose.yml
962
1045
  replace:
963
1046
  - from: '${N8N_VERSION}'
964
1047
  to: $n8n_version # pin version at pull time
@@ -981,19 +1064,22 @@ A single `avanti pull` populates a new project with everything it needs: editor
981
1064
 
982
1065
  ```yaml
983
1066
  files:
984
- - src:
1067
+ .editorconfig:
1068
+ src:
985
1069
  github:
986
1070
  repo: org/standards
987
1071
  file: .editorconfig
988
1072
  ref: main
989
1073
 
990
- - src:
1074
+ .prettierrc:
1075
+ src:
991
1076
  github:
992
1077
  repo: org/standards
993
1078
  file: .prettierrc
994
1079
  ref: main
995
1080
 
996
- - src:
1081
+ CLAUDE.md:
1082
+ src:
997
1083
  - raw: |
998
1084
  # AI Assistant Guidelines
999
1085
  <!-- THIS FILE IS MANAGED — run `avanti pull` to update -->
@@ -1001,56 +1087,41 @@ files:
1001
1087
  repo: org/ai-standards
1002
1088
  file: CLAUDE.md
1003
1089
  ref: main
1004
- target: CLAUDE.md
1005
1090
 
1006
- - src:
1091
+ .github/workflows/:
1092
+ src:
1007
1093
  github:
1008
1094
  repo: org/ci-templates
1009
1095
  file: workflows/
1010
1096
  ref: main
1011
- target: .github/workflows/
1012
1097
  ```
1013
1098
 
1014
1099
  ### Self-managing Config
1015
1100
 
1016
- avanti can sync any file — including its own config. Put the canonical config in a central repo and add a self-update entry. Every `avanti pull` refreshes the config alongside all other managed files.
1017
-
1018
- When avanti detects that a pull would update the local config file, it automatically re-evaluates the new config in memory and applies all the files it describes — in the same run, with a single confirmation prompt and a single atomic write. You don't need to run `avanti pull` twice to get the new config's files.
1101
+ avanti can manage any file — including its own config. The special `$self` key in the `files:` map fetches and merges one or more sources, uses the result as the active config, and then applies all the files it declares — in the same run, with a single confirmation prompt.
1019
1102
 
1020
1103
  ```yaml
1021
1104
  # ~/.avanti.yml
1022
- variables:
1023
- dotfiles: myorg/dotfiles
1024
-
1025
1105
  files:
1026
- # Keep this config itself up to date
1027
- - src:
1106
+ $self:
1107
+ src:
1028
1108
  github:
1029
- repo: $dotfiles
1109
+ repo: myorg/dotfiles
1030
1110
  file: avanti.yml
1031
1111
  ref: $latest
1032
- target: ~/.avanti.yml
1112
+ ```
1033
1113
 
1034
- # Everything else the config manages
1035
- - src:
1036
- github:
1037
- repo: $dotfiles
1038
- file: .zshrc
1039
- target: ~/.zshrc
1114
+ Every `avanti pull` fetches the remote `avanti.yml`, applies all the files it declares, and writes the merged result back to `~/.avanti.yml`. If the remote `avanti.yml` itself contains a `$self` entry, avanti keeps re-fetching until the content stabilizes — so the remote config can keep pointing at itself and avanti will always pick up the latest version on every pull.
1040
1115
 
1041
- - src:
1042
- github:
1043
- repo: $dotfiles
1044
- file: .gitconfig
1045
- target: ~/.gitconfig
1046
- ```
1116
+ When running with a remote config (`--config github:...`), `$self` is in-memory only — the merged result drives the run but is not persisted anywhere, since there is no local file to write back to.
1047
1117
 
1048
- **Composable self-managing config** — YAML merge takes this further. Instead of one canonical config, compose your `~/.avanti.yml` from org-wide defaults, team additions, and personal overrides all merged automatically on every pull:
1118
+ **Composable config** — `$self` with multiple sources and YAML merge lets you assemble a config from independent layers. Org-wide defaults, team additions, and personal overrides all merge into one active config:
1049
1119
 
1050
1120
  ```yaml
1051
- # ~/.avanti.yml — bootstrapped once, then self-updating via YAML merge
1121
+ # ~/.avanti.yml
1052
1122
  files:
1053
- - src:
1123
+ $self:
1124
+ src:
1054
1125
  - github:
1055
1126
  repo: myorg/platform
1056
1127
  file: avanti/base.yml # org-wide entries and variables
@@ -1063,7 +1134,6 @@ files:
1063
1134
  repo: myuser/dotfiles
1064
1135
  file: avanti/personal.yml # personal overrides and extras
1065
1136
  ref: main
1066
- target: ~/.avanti.yml
1067
1137
  yaml:
1068
1138
  conflicts: last_wins # personal overrides win over team, team over org
1069
1139
  arrays: concat # file lists from all layers are merged, not replaced
@@ -1103,8 +1173,8 @@ This scales to any number of machines or containers. Update the central repo onc
1103
1173
 
1104
1174
  ```sh
1105
1175
  git clone ...
1106
- bun install
1107
- bun run dev -- --help # run via tsx
1108
- bun test # run tests
1109
- bun run build # compile to dist/
1176
+ mise run install # install dependencies and set up git hooks
1177
+ mise run dev -- --help # run via tsx
1178
+ mise run test # run tests
1179
+ mise run build # compile to dist/
1110
1180
  ```