@udondan/avanti 0.13.0 → 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 (76) hide show
  1. package/README.md +289 -264
  2. package/dist/commands/diff.d.ts.map +1 -1
  3. package/dist/commands/diff.js +116 -32
  4. package/dist/commands/diff.js.map +1 -1
  5. package/dist/commands/lock.js +19 -10
  6. package/dist/commands/lock.js.map +1 -1
  7. package/dist/commands/log.js +18 -9
  8. package/dist/commands/log.js.map +1 -1
  9. package/dist/commands/pull.d.ts.map +1 -1
  10. package/dist/commands/pull.js +142 -36
  11. package/dist/commands/pull.js.map +1 -1
  12. package/dist/commands/reset.js +18 -9
  13. package/dist/commands/reset.js.map +1 -1
  14. package/dist/commands/revert.js +18 -9
  15. package/dist/commands/revert.js.map +1 -1
  16. package/dist/config-writeback.d.ts.map +1 -1
  17. package/dist/config-writeback.js +44 -18
  18. package/dist/config-writeback.js.map +1 -1
  19. package/dist/config.d.ts +1 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +96 -62
  22. package/dist/config.js.map +1 -1
  23. package/dist/diff.js +22 -13
  24. package/dist/diff.js.map +1 -1
  25. package/dist/fetch.d.ts +0 -1
  26. package/dist/fetch.d.ts.map +1 -1
  27. package/dist/fetch.js +2 -2
  28. package/dist/fetch.js.map +1 -1
  29. package/dist/history.js +17 -7
  30. package/dist/history.js.map +1 -1
  31. package/dist/processors/json.js +2 -3
  32. package/dist/processors/json.js.map +1 -1
  33. package/dist/processors/post.js +1 -2
  34. package/dist/processors/post.js.map +1 -1
  35. package/dist/processors/replace.js +1 -2
  36. package/dist/processors/replace.js.map +1 -1
  37. package/dist/processors/yaml.js +2 -3
  38. package/dist/processors/yaml.js.map +1 -1
  39. package/dist/prompt.js +18 -9
  40. package/dist/prompt.js.map +1 -1
  41. package/dist/sha.js +18 -9
  42. package/dist/sha.js.map +1 -1
  43. package/dist/sources/bitbucket.js +18 -9
  44. package/dist/sources/bitbucket.js.map +1 -1
  45. package/dist/sources/exec.js +1 -2
  46. package/dist/sources/exec.js.map +1 -1
  47. package/dist/sources/git.js +18 -9
  48. package/dist/sources/git.js.map +1 -1
  49. package/dist/sources/github.js +18 -9
  50. package/dist/sources/github.js.map +1 -1
  51. package/dist/sources/gitlab.js +18 -9
  52. package/dist/sources/gitlab.js.map +1 -1
  53. package/dist/sources/http.d.ts +3 -0
  54. package/dist/sources/http.d.ts.map +1 -1
  55. package/dist/sources/http.js +24 -12
  56. package/dist/sources/http.js.map +1 -1
  57. package/dist/sources/index.d.ts +4 -1
  58. package/dist/sources/index.d.ts.map +1 -1
  59. package/dist/sources/index.js +152 -31
  60. package/dist/sources/index.js.map +1 -1
  61. package/dist/sources/local.d.ts +2 -1
  62. package/dist/sources/local.d.ts.map +1 -1
  63. package/dist/sources/local.js +21 -10
  64. package/dist/sources/local.js.map +1 -1
  65. package/dist/sources/s3.js +18 -9
  66. package/dist/sources/s3.js.map +1 -1
  67. package/dist/sources/vault.js +18 -9
  68. package/dist/sources/vault.js.map +1 -1
  69. package/dist/types.d.ts +15 -3
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/variables.js +4 -4
  72. package/dist/variables.js.map +1 -1
  73. package/dist/writer.d.ts.map +1 -1
  74. package/dist/writer.js +37 -12
  75. package/dist/writer.js.map +1 -1
  76. package/package.json +10 -9
package/README.md CHANGED
@@ -1,9 +1,14 @@
1
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
@@ -261,49 +309,59 @@ variables:
261
309
  email: you@example.com
262
310
 
263
311
  files:
264
- - src: http://www.example.com/example.yml
265
- target: my-example.yml
312
+ my-example.yml:
313
+ src: http://www.example.com/example.yml
266
314
  replace:
267
315
  - from: '{EMAIL}'
268
316
  to: $email
269
317
  - from: /\d+/
270
318
  to: number
271
319
 
272
- - src: ~/some/local/file.sh
273
- target: file.sh
320
+ file.sh:
321
+ src: ~/some/local/file.sh
274
322
  mode: '0777'
275
323
 
276
- - src:
324
+ some-file.yml:
325
+ src:
277
326
  exec: glab api "projects/group%2Fproject/repository/files/some-file.yaml/raw?ref=main"
278
- target: some-file.yml
279
327
  post: sed -e 's/v3/v4/g'
280
328
 
281
- - src:
329
+ renovate.json:
330
+ src:
282
331
  gitlab:
283
332
  project: group/project
284
333
  file: renovate.json
285
334
  ref: $latest
286
- # target omitted → renovate.json
287
335
 
288
- - src:
336
+ local-scripts/:
337
+ src:
289
338
  github:
290
339
  repo: org/repo
291
340
  file: scripts/
292
341
  ref: main
293
- target: local-scripts/
294
342
  ```
295
343
 
296
344
  ### File Entry Fields
297
345
 
298
- | Field | Required | Description |
299
- | --------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
300
- | `src` | Yes | Source (see below). May be a single source or a **list** of sources to concatenate. |
301
- | `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:`). |
302
- | `mode` | No | File permission mode, e.g. `"0755"` |
303
- | `replace` | No | List of `{from, to}` replacement rules. `from` may be a plain string or `/pattern/flags` regex. |
304
- | `post` | No | Shell script. Content is piped via stdin; stdout is used as the result. Runs after `replace`. |
305
- | `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. |
306
- | `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. |
307
365
 
308
366
  ### Source Types
309
367
 
@@ -315,9 +373,19 @@ src: ~/templates/file.txt
315
373
  src: /absolute/path/file.txt
316
374
  ```
317
375
 
318
- **Map** — for exec, gitlab, github, bitbucket, git, s3, vault, http, raw:
376
+ **Map** — for path, url, exec, gitlab, github, bitbucket, git, s3, vault, http, raw:
319
377
 
320
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
+
321
389
  src:
322
390
  exec: <shell command> # stdout becomes file content; target required
323
391
  sha: abc123... # optional SHA-256 to verify stdout (see below)
@@ -374,89 +442,96 @@ src:
374
442
 
375
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.
376
444
 
377
- Use `avanti lock` to compute and write SHA values automatically. Use `avanti pull --accept-changes` to review a mismatch and update the pinned SHA. Local paths and `raw:` sources do not support `sha` (changes to those are inherently visible).
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.
378
446
 
379
447
  ### Directory Sources
380
448
 
381
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.
382
450
 
383
- **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:`:
384
499
 
385
500
  ```yaml
386
- # All files under skills/ in the GitLab repo are written into local skills.new/
387
- - src:
388
- gitlab:
389
- project: group/repo
390
- file: skills/
391
- ref: main
392
- target: skills/
393
-
394
- # GitHub directory → local directory
395
- - src:
396
- github:
397
- repo: org/repo
398
- file: .github/workflows/
399
- ref: main
400
- target: .github/workflows/
401
-
402
- # Bitbucket directory → local directory
403
- - src:
404
- bitbucket:
405
- workspace: my-workspace
406
- repo: shared-configs
407
- file: eslint/
408
- ref: main
409
- target: eslint/
410
-
411
- # git remote directory → local directory (any host)
412
- - src:
413
- git:
414
- repo: https://github.com/org/repo.git
415
- file: .github/workflows/
416
- ref: main
417
- target: .github/workflows/
418
-
419
- # S3 prefix → local directory (trailing / triggers sync)
420
- - src:
421
- s3: s3://my-bucket/configs/
422
- target: configs/
423
-
424
- # Local directory → local directory
425
- - src: ~/shared/hooks/
426
- 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/
427
509
  ```
428
510
 
429
- **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):
430
512
 
431
513
  ```yaml
432
- # One folder per service, each a separate .yml file → single docker-compose.yml
433
- - src: ./services/
434
- target: docker-compose.yml
435
-
436
- # Same with explicit yaml merge options (e.g. to concat arrays)
437
- - src: ./services/
438
- target: docker-compose.yml
439
- yaml:
440
- arrays: concat
441
-
442
- # JSON: one file per environment → merged config
443
- - src: ./config/
444
- target: config.json
514
+ files:
515
+ docker-compose.yml:
516
+ src: ./services/
517
+ yaml:
518
+ arrays: concat
445
519
  ```
446
520
 
447
521
  Directory sources cannot be mixed into a multi-source list (`src` as a list), because the list mode always produces a single file.
448
522
 
449
- **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):
450
524
 
451
525
  ```yaml
452
- src:
453
- - https://example.com/header.txt
454
- - exec: echo "# generated"
455
- - gitlab:
456
- project: org/repo
457
- file: footer.txt
458
- ref: main
459
- 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
460
535
  ```
461
536
 
462
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.
@@ -467,20 +542,20 @@ When all sources in a list have a `.json` or `.jsonc` extension, JSON merging is
467
542
 
468
543
  ```yaml
469
544
  files:
470
- - src:
545
+ merged.jsonc:
546
+ src:
471
547
  - ./team.jsonc
472
548
  - ./my.jsonc
473
- target: merged.jsonc
474
549
  ```
475
550
 
476
551
  To merge sources that don't have a JSON extension (e.g. `exec:`, `raw:`, or a URL without `.json`), set `json: true`:
477
552
 
478
553
  ```yaml
479
554
  files:
480
- - src:
555
+ merged.json:
556
+ src:
481
557
  - exec: cat defaults.json
482
558
  - ./overrides.json
483
- target: merged.json
484
559
  json: true
485
560
  ```
486
561
 
@@ -490,12 +565,12 @@ To opt out of auto-detection and force plain concatenation, set `json: false`.
490
565
 
491
566
  ```yaml
492
567
  files:
493
- - src:
568
+ merged.json:
569
+ src:
494
570
  - ./defaults.json
495
- - type: github
496
- repo: org/configs
497
- file: overrides.json
498
- target: merged.json
571
+ - github:
572
+ repo: org/configs
573
+ file: overrides.json
499
574
  json:
500
575
  conflicts: last_wins # abort | first_wins | last_wins (default)
501
576
  arrays: replace # replace (default) | concat
@@ -517,8 +592,8 @@ files:
517
592
 
518
593
  ```yaml
519
594
  files:
520
- - src: ./minified.json
521
- target: pretty.json
595
+ pretty.json:
596
+ src: ./minified.json
522
597
  ```
523
598
 
524
599
  ### YAML Merging
@@ -527,20 +602,20 @@ When all sources in a list have a `.yaml` or `.yml` extension, YAML merging is e
527
602
 
528
603
  ```yaml
529
604
  files:
530
- - src:
605
+ merged.yaml:
606
+ src:
531
607
  - ./defaults.yaml
532
608
  - ./overrides.yml
533
- target: merged.yaml
534
609
  ```
535
610
 
536
611
  To merge sources that don't have a YAML extension (e.g. `exec:`, `raw:`, or a URL without `.yaml`), set `yaml: true`:
537
612
 
538
613
  ```yaml
539
614
  files:
540
- - src:
615
+ merged.yaml:
616
+ src:
541
617
  - exec: cat defaults.yaml
542
618
  - ./overrides.yaml
543
- target: merged.yaml
544
619
  yaml: true
545
620
  ```
546
621
 
@@ -550,12 +625,12 @@ To opt out of auto-detection and force plain concatenation, set `yaml: false`.
550
625
 
551
626
  ```yaml
552
627
  files:
553
- - src:
628
+ merged.yaml:
629
+ src:
554
630
  - ./defaults.yaml
555
- - type: github
556
- repo: org/configs
557
- file: overrides.yaml
558
- target: merged.yaml
631
+ - github:
632
+ repo: org/configs
633
+ file: overrides.yaml
559
634
  yaml:
560
635
  conflicts: last_wins # abort | first_wins | last_wins (default)
561
636
  arrays: replace # replace (default) | concat
@@ -581,8 +656,8 @@ The options behave identically to JSON merging:
581
656
 
582
657
  ```yaml
583
658
  files:
584
- - src: ./config.yaml
585
- target: config.yaml
659
+ config.yaml:
660
+ src: ./config.yaml
586
661
  ```
587
662
 
588
663
  ### Variables
@@ -599,7 +674,8 @@ Reference them anywhere in the config with `$name`:
599
674
 
600
675
  ```yaml
601
676
  files:
602
- - src:
677
+ renovate.json:
678
+ src:
603
679
  gitlab:
604
680
  project: group/project
605
681
  file: renovate.json
@@ -609,7 +685,7 @@ files:
609
685
  to: $email # resolved to "you@example.com"
610
686
  ```
611
687
 
612
- 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.
613
689
 
614
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.
615
691
 
@@ -620,9 +696,9 @@ variables:
620
696
  version: '1.0'
621
697
 
622
698
  files:
623
- - src:
699
+ data.json:
700
+ src:
624
701
  exec: curl https://example.com/api/$version/data # expands to: curl …/'1.0'/data
625
- target: data.json
626
702
  post: sed 's/$version/replaced/g' # expands to: sed 's/'\''1.0'\''/replaced/g'
627
703
  ```
628
704
 
@@ -640,6 +716,50 @@ Referencing an undefined variable or a missing environment variable is an error.
640
716
 
641
717
  When `ref` is omitted, all source types (GitHub, GitLab, Bitbucket, git) resolve to the repository's default branch.
642
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
+
643
763
  ### Authentication
644
764
 
645
765
  Public repositories on github.com and gitlab.com work without any configuration. For private repositories or instances, supply credentials via environment variables:
@@ -675,95 +795,6 @@ GITHUB_HOST=github.mycompany.com avanti pull
675
795
 
676
796
  ## Use Cases
677
797
 
678
- ### Avanti as a Package Manager
679
-
680
- 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.
681
-
682
- 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.
683
-
684
- **Declare dependencies** — pin versions with a variable and bump in one place to upgrade everything at once:
685
-
686
- ```yaml
687
- variables:
688
- frontend_standards: myorg/frontend-standards
689
- platform: myorg/platform-templates
690
- standards_ref: v2.4.1 # pinned — bump here to upgrade
691
-
692
- files:
693
- - src:
694
- github:
695
- repo: $frontend_standards
696
- file: eslint.config.js
697
- ref: $standards_ref
698
-
699
- - src:
700
- github:
701
- repo: $frontend_standards
702
- file: .prettierrc
703
- ref: $standards_ref
704
-
705
- - src:
706
- github:
707
- repo: $platform
708
- file: workflows/test.yml
709
- ref: $standards_ref
710
- target: .github/workflows/test.yml
711
-
712
- - src:
713
- github:
714
- repo: $platform
715
- file: workflows/deploy.yml
716
- ref: $standards_ref
717
- target: .github/workflows/deploy.yml
718
- ```
719
-
720
- **Review and apply upgrades** — the same workflow as reading a lockfile diff before committing:
721
-
722
- ```sh
723
- # Bump standards_ref: v2.4.1 → v2.5.0, then:
724
- avanti diff # see every file that would change
725
- avanti pull # apply after review
726
- avanti revert # roll back instantly if something breaks
727
- ```
728
-
729
- **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:
730
-
731
- ```yaml
732
- # myorg/frontend-standards:avanti-snippet.yml — published alongside eslint.config.js, .prettierrc, etc.
733
- files:
734
- - src:
735
- github:
736
- repo: myorg/frontend-standards
737
- file: eslint.config.js
738
- ref: $latest
739
-
740
- - src:
741
- github:
742
- repo: myorg/frontend-standards
743
- file: .prettierrc
744
- ref: $latest
745
- ```
746
-
747
- ```yaml
748
- # .avanti.yml — assembled from team snippets via YAML merge
749
- files:
750
- - src:
751
- - github:
752
- repo: myorg/frontend-standards
753
- file: avanti-snippet.yml
754
- ref: $latest
755
- - github:
756
- repo: myorg/platform-templates
757
- file: avanti-snippet.yml
758
- ref: $latest
759
- target: .avanti.yml
760
- yaml:
761
- arrays: concat # file lists from all snippets are concatenated
762
- conflicts: last_wins
763
- ```
764
-
765
- 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.
766
-
767
798
  ### Composable AI Agent Instructions (CLAUDE.md / AGENTS.md)
768
799
 
769
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.
@@ -776,7 +807,8 @@ variables:
776
807
  oncall_channel: '#backend-oncall'
777
808
 
778
809
  files:
779
- - src:
810
+ CLAUDE.md:
811
+ src:
780
812
  - raw: |
781
813
  # AI Assistant Guidelines
782
814
  <!-- THIS FILE IS MANAGED — run `avanti pull` to update -->
@@ -793,9 +825,12 @@ files:
793
825
  Team: $team
794
826
  Jira project: $jira_project
795
827
  Oncall: $oncall_channel
796
- target: CLAUDE.md
828
+ - path: ~/custom-claude.md # personal additions; silently skipped if absent
829
+ optional: true
797
830
  ```
798
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
+
799
834
  ### Shared Tooling Config (Renovate, ESLint, Prettier, TSConfig)
800
835
 
801
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.
@@ -805,19 +840,22 @@ variables:
805
840
  standards_ref: v2.4.1
806
841
 
807
842
  files:
808
- - src:
843
+ renovate.json:
844
+ src:
809
845
  github:
810
846
  repo: org/standards
811
847
  file: renovate.json
812
848
  ref: $standards_ref
813
849
 
814
- - src:
850
+ eslint.config.js:
851
+ src:
815
852
  github:
816
853
  repo: org/standards
817
854
  file: eslint.config.js
818
855
  ref: $standards_ref
819
856
 
820
- - src:
857
+ tsconfig.base.json:
858
+ src:
821
859
  github:
822
860
  repo: org/standards
823
861
  file: tsconfig.base.json
@@ -828,13 +866,13 @@ For YAML-based configs (Helm values, k8s manifests, Docker Compose overrides), u
828
866
 
829
867
  ```yaml
830
868
  files:
831
- - src:
869
+ ./helm/merged-values.yaml:
870
+ src:
832
871
  - github:
833
872
  repo: org/platform
834
873
  file: helm/base-values.yaml # shared defaults for all services
835
874
  ref: $standards_ref
836
875
  - ./helm/values.yaml # project overrides
837
- target: ./helm/merged-values.yaml
838
876
  yaml:
839
877
  conflicts: last_wins # project overrides win
840
878
  arrays: concat # e.g. extra env vars are appended, not replaced
@@ -846,14 +884,14 @@ Pull reusable CI steps from a central repo into each project. A managed header m
846
884
 
847
885
  ```yaml
848
886
  files:
849
- - src:
887
+ .github/workflows/security-scan.yml:
888
+ src:
850
889
  - raw: |
851
890
  # THIS FILE IS MANAGED — run `avanti pull` to update
852
891
  - github:
853
892
  repo: org/ci-templates
854
893
  file: workflows/security-scan.yml
855
894
  ref: main
856
- target: .github/workflows/security-scan.yml
857
895
  ```
858
896
 
859
897
  Use `avanti diff` in CI to detect drift — if a project's checked-in file no longer matches the source, the pipeline fails.
@@ -914,12 +952,12 @@ variables:
914
952
  region: eu-west-1
915
953
 
916
954
  files:
917
- - src:
955
+ k8s/deployment.yaml:
956
+ src:
918
957
  github:
919
958
  repo: org/infra
920
959
  file: k8s/deployment-template.yaml
921
960
  ref: $env:DEPLOY_VERSION
922
- target: k8s/deployment.yaml
923
961
  replace:
924
962
  - from: '{ENV}'
925
963
  to: $env:ENVIRONMENT
@@ -936,24 +974,24 @@ Pull secrets at runtime and write them to local files with tight permissions. Th
936
974
  ```yaml
937
975
  files:
938
976
  # Single field from a Vault KV secret
939
- - src:
977
+ config/db_password.txt:
978
+ src:
940
979
  vault:
941
980
  path: secret/myapp/db
942
981
  field: password
943
- target: config/db_password.txt
944
982
  mode: '0600'
945
983
 
946
984
  # Full Vault secret as JSON
947
- - src:
985
+ config/secrets.json:
986
+ src:
948
987
  vault:
949
988
  path: secret/myapp/config
950
- target: config/secrets.json
951
989
  mode: '0600'
952
990
 
953
991
  # Config file stored in S3
954
- - src:
992
+ config/app.json:
993
+ src:
955
994
  s3: s3://my-bucket/configs/app.json
956
- target: config/app.json
957
995
  mode: '0600'
958
996
  ```
959
997
 
@@ -961,13 +999,13 @@ For AWS SSM or other secret stores without a dedicated source type, `exec:` stil
961
999
 
962
1000
  ```yaml
963
1001
  files:
964
- - src:
1002
+ config/db.json:
1003
+ src:
965
1004
  exec: >
966
1005
  aws ssm get-parameter
967
1006
  --name /myapp/db-config
968
1007
  --with-decryption
969
1008
  --query Parameter.Value --output text
970
- target: config/db.json
971
1009
  mode: '0600'
972
1010
  ```
973
1011
 
@@ -994,7 +1032,8 @@ variables:
994
1032
  db_password: changeme
995
1033
 
996
1034
  files:
997
- - src:
1035
+ docker-compose.yml:
1036
+ src:
998
1037
  - github:
999
1038
  repo: n8n-io/n8n-hosting
1000
1039
  file: docker-caddy/docker-compose.yml
@@ -1003,7 +1042,6 @@ files:
1003
1042
  repo: docker-library/docs
1004
1043
  file: postgres/compose.yaml
1005
1044
  ref: master
1006
- target: docker-compose.yml
1007
1045
  replace:
1008
1046
  - from: '${N8N_VERSION}'
1009
1047
  to: $n8n_version # pin version at pull time
@@ -1026,19 +1064,22 @@ A single `avanti pull` populates a new project with everything it needs: editor
1026
1064
 
1027
1065
  ```yaml
1028
1066
  files:
1029
- - src:
1067
+ .editorconfig:
1068
+ src:
1030
1069
  github:
1031
1070
  repo: org/standards
1032
1071
  file: .editorconfig
1033
1072
  ref: main
1034
1073
 
1035
- - src:
1074
+ .prettierrc:
1075
+ src:
1036
1076
  github:
1037
1077
  repo: org/standards
1038
1078
  file: .prettierrc
1039
1079
  ref: main
1040
1080
 
1041
- - src:
1081
+ CLAUDE.md:
1082
+ src:
1042
1083
  - raw: |
1043
1084
  # AI Assistant Guidelines
1044
1085
  <!-- THIS FILE IS MANAGED — run `avanti pull` to update -->
@@ -1046,56 +1087,41 @@ files:
1046
1087
  repo: org/ai-standards
1047
1088
  file: CLAUDE.md
1048
1089
  ref: main
1049
- target: CLAUDE.md
1050
1090
 
1051
- - src:
1091
+ .github/workflows/:
1092
+ src:
1052
1093
  github:
1053
1094
  repo: org/ci-templates
1054
1095
  file: workflows/
1055
1096
  ref: main
1056
- target: .github/workflows/
1057
1097
  ```
1058
1098
 
1059
1099
  ### Self-managing Config
1060
1100
 
1061
- 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.
1062
-
1063
- 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.
1064
1102
 
1065
1103
  ```yaml
1066
1104
  # ~/.avanti.yml
1067
- variables:
1068
- dotfiles: myorg/dotfiles
1069
-
1070
1105
  files:
1071
- # Keep this config itself up to date
1072
- - src:
1106
+ $self:
1107
+ src:
1073
1108
  github:
1074
- repo: $dotfiles
1109
+ repo: myorg/dotfiles
1075
1110
  file: avanti.yml
1076
1111
  ref: $latest
1077
- target: ~/.avanti.yml
1112
+ ```
1078
1113
 
1079
- # Everything else the config manages
1080
- - src:
1081
- github:
1082
- repo: $dotfiles
1083
- file: .zshrc
1084
- 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.
1085
1115
 
1086
- - src:
1087
- github:
1088
- repo: $dotfiles
1089
- file: .gitconfig
1090
- target: ~/.gitconfig
1091
- ```
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.
1092
1117
 
1093
- **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:
1094
1119
 
1095
1120
  ```yaml
1096
- # ~/.avanti.yml — bootstrapped once, then self-updating via YAML merge
1121
+ # ~/.avanti.yml
1097
1122
  files:
1098
- - src:
1123
+ $self:
1124
+ src:
1099
1125
  - github:
1100
1126
  repo: myorg/platform
1101
1127
  file: avanti/base.yml # org-wide entries and variables
@@ -1108,7 +1134,6 @@ files:
1108
1134
  repo: myuser/dotfiles
1109
1135
  file: avanti/personal.yml # personal overrides and extras
1110
1136
  ref: main
1111
- target: ~/.avanti.yml
1112
1137
  yaml:
1113
1138
  conflicts: last_wins # personal overrides win over team, team over org
1114
1139
  arrays: concat # file lists from all layers are merged, not replaced
@@ -1148,8 +1173,8 @@ This scales to any number of machines or containers. Update the central repo onc
1148
1173
 
1149
1174
  ```sh
1150
1175
  git clone ...
1151
- bun install
1152
- bun run dev -- --help # run via tsx
1153
- bun test # run tests
1154
- 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/
1155
1180
  ```