@udondan/avanti 0.13.0 → 0.15.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 (84) hide show
  1. package/README.md +303 -265
  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.d.ts.map +1 -1
  24. package/dist/diff.js +25 -15
  25. package/dist/diff.js.map +1 -1
  26. package/dist/fetch.d.ts +0 -1
  27. package/dist/fetch.d.ts.map +1 -1
  28. package/dist/fetch.js +2 -2
  29. package/dist/fetch.js.map +1 -1
  30. package/dist/history.js +17 -7
  31. package/dist/history.js.map +1 -1
  32. package/dist/processors/json.js +2 -3
  33. package/dist/processors/json.js.map +1 -1
  34. package/dist/processors/post.d.ts.map +1 -1
  35. package/dist/processors/post.js +4 -3
  36. package/dist/processors/post.js.map +1 -1
  37. package/dist/processors/replace.js +1 -2
  38. package/dist/processors/replace.js.map +1 -1
  39. package/dist/processors/yaml.js +2 -3
  40. package/dist/processors/yaml.js.map +1 -1
  41. package/dist/prompt.js +18 -9
  42. package/dist/prompt.js.map +1 -1
  43. package/dist/sha.js +18 -9
  44. package/dist/sha.js.map +1 -1
  45. package/dist/shell.d.ts +6 -0
  46. package/dist/shell.d.ts.map +1 -0
  47. package/dist/shell.js +76 -0
  48. package/dist/shell.js.map +1 -0
  49. package/dist/sources/bitbucket.js +18 -9
  50. package/dist/sources/bitbucket.js.map +1 -1
  51. package/dist/sources/exec.d.ts.map +1 -1
  52. package/dist/sources/exec.js +4 -3
  53. package/dist/sources/exec.js.map +1 -1
  54. package/dist/sources/git.js +18 -9
  55. package/dist/sources/git.js.map +1 -1
  56. package/dist/sources/github.js +18 -9
  57. package/dist/sources/github.js.map +1 -1
  58. package/dist/sources/gitlab.js +18 -9
  59. package/dist/sources/gitlab.js.map +1 -1
  60. package/dist/sources/http.d.ts +3 -0
  61. package/dist/sources/http.d.ts.map +1 -1
  62. package/dist/sources/http.js +24 -12
  63. package/dist/sources/http.js.map +1 -1
  64. package/dist/sources/index.d.ts +4 -1
  65. package/dist/sources/index.d.ts.map +1 -1
  66. package/dist/sources/index.js +152 -31
  67. package/dist/sources/index.js.map +1 -1
  68. package/dist/sources/local.d.ts +2 -1
  69. package/dist/sources/local.d.ts.map +1 -1
  70. package/dist/sources/local.js +23 -15
  71. package/dist/sources/local.js.map +1 -1
  72. package/dist/sources/s3.js +18 -9
  73. package/dist/sources/s3.js.map +1 -1
  74. package/dist/sources/vault.js +18 -9
  75. package/dist/sources/vault.js.map +1 -1
  76. package/dist/types.d.ts +15 -3
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/variables.d.ts.map +1 -1
  79. package/dist/variables.js +42 -31
  80. package/dist/variables.js.map +1 -1
  81. package/dist/writer.d.ts.map +1 -1
  82. package/dist/writer.js +37 -12
  83. package/dist/writer.js.map +1 -1
  84. 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:
384
452
 
385
453
  ```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/
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:`:
499
+
500
+ ```yaml
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,23 +685,36 @@ 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
 
616
- **Shell safety in `exec:` and `post:`** — when a variable is substituted into an `exec:` command or a `post:` script, its value is automatically wrapped in POSIX single quotes. 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.
692
+ **Shell safety in `exec:` and `post:`** — when a variable is substituted into an `exec:` command or a `post:` 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`).
617
693
 
618
694
  ```yaml
619
695
  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
 
705
+ **Escaping a literal `$`** — use `$$` to emit a literal `$` that is not treated as a variable reference. This is useful in `exec:` and `post:` scripts that contain shell or PowerShell syntax with `$`-prefixed identifiers (e.g. PowerShell built-ins like `$$true` or `$$null`):
706
+
707
+ ```yaml
708
+ files:
709
+ out.txt:
710
+ src:
711
+ # On Windows exec: runs in PowerShell — $true is a PS built-in, needs $$
712
+ exec: "if ($$true) { Write-Output 'yes' }"
713
+ post: sed 's/$$HOME/redacted/g' # $HOME would be treated as an avanti variable
714
+ ```
715
+
716
+ `$$` produces a single `$` after substitution. `$$$name` produces `$` followed by the value of `name`.
717
+
629
718
  **Environment variables** use the `$env:NAME` form:
630
719
 
631
720
  ```yaml
@@ -640,6 +729,50 @@ Referencing an undefined variable or a missing environment variable is an error.
640
729
 
641
730
  When `ref` is omitted, all source types (GitHub, GitLab, Bitbucket, git) resolve to the repository's default branch.
642
731
 
732
+ ### $self — Self-managing Config
733
+
734
+ 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.
735
+
736
+ ```yaml
737
+ files:
738
+ $self:
739
+ src:
740
+ github:
741
+ repo: myorg/dotfiles
742
+ file: avanti.yml
743
+ ref: $latest
744
+ ```
745
+
746
+ **How it works:**
747
+
748
+ 1. avanti fetches only the `$self` sources first.
749
+ 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`).
750
+ 3. The result is parsed as the new active config. If it also contains `$self`, avanti re-fetches until the content stabilizes (fixed point).
751
+ 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.
752
+
753
+ **Multi-layer config** — list multiple sources under `$self` and use `yaml:` to deep-merge them into one config:
754
+
755
+ ```yaml
756
+ files:
757
+ $self:
758
+ src:
759
+ - github:
760
+ repo: myorg/platform
761
+ file: avanti/base.yml
762
+ ref: $latest
763
+ - github:
764
+ repo: myorg/backend-team
765
+ file: avanti/team.yml
766
+ ref: main
767
+ - path: ~/avanti-personal.yml
768
+ optional: true
769
+ yaml:
770
+ conflicts: last_wins
771
+ arrays: concat
772
+ ```
773
+
774
+ `$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.
775
+
643
776
  ### Authentication
644
777
 
645
778
  Public repositories on github.com and gitlab.com work without any configuration. For private repositories or instances, supply credentials via environment variables:
@@ -675,95 +808,6 @@ GITHUB_HOST=github.mycompany.com avanti pull
675
808
 
676
809
  ## Use Cases
677
810
 
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
811
  ### Composable AI Agent Instructions (CLAUDE.md / AGENTS.md)
768
812
 
769
813
  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 +820,8 @@ variables:
776
820
  oncall_channel: '#backend-oncall'
777
821
 
778
822
  files:
779
- - src:
823
+ CLAUDE.md:
824
+ src:
780
825
  - raw: |
781
826
  # AI Assistant Guidelines
782
827
  <!-- THIS FILE IS MANAGED — run `avanti pull` to update -->
@@ -793,9 +838,12 @@ files:
793
838
  Team: $team
794
839
  Jira project: $jira_project
795
840
  Oncall: $oncall_channel
796
- target: CLAUDE.md
841
+ - path: ~/custom-claude.md # personal additions; silently skipped if absent
842
+ optional: true
797
843
  ```
798
844
 
845
+ 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.
846
+
799
847
  ### Shared Tooling Config (Renovate, ESLint, Prettier, TSConfig)
800
848
 
801
849
  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 +853,22 @@ variables:
805
853
  standards_ref: v2.4.1
806
854
 
807
855
  files:
808
- - src:
856
+ renovate.json:
857
+ src:
809
858
  github:
810
859
  repo: org/standards
811
860
  file: renovate.json
812
861
  ref: $standards_ref
813
862
 
814
- - src:
863
+ eslint.config.js:
864
+ src:
815
865
  github:
816
866
  repo: org/standards
817
867
  file: eslint.config.js
818
868
  ref: $standards_ref
819
869
 
820
- - src:
870
+ tsconfig.base.json:
871
+ src:
821
872
  github:
822
873
  repo: org/standards
823
874
  file: tsconfig.base.json
@@ -828,13 +879,13 @@ For YAML-based configs (Helm values, k8s manifests, Docker Compose overrides), u
828
879
 
829
880
  ```yaml
830
881
  files:
831
- - src:
882
+ ./helm/merged-values.yaml:
883
+ src:
832
884
  - github:
833
885
  repo: org/platform
834
886
  file: helm/base-values.yaml # shared defaults for all services
835
887
  ref: $standards_ref
836
888
  - ./helm/values.yaml # project overrides
837
- target: ./helm/merged-values.yaml
838
889
  yaml:
839
890
  conflicts: last_wins # project overrides win
840
891
  arrays: concat # e.g. extra env vars are appended, not replaced
@@ -846,14 +897,14 @@ Pull reusable CI steps from a central repo into each project. A managed header m
846
897
 
847
898
  ```yaml
848
899
  files:
849
- - src:
900
+ .github/workflows/security-scan.yml:
901
+ src:
850
902
  - raw: |
851
903
  # THIS FILE IS MANAGED — run `avanti pull` to update
852
904
  - github:
853
905
  repo: org/ci-templates
854
906
  file: workflows/security-scan.yml
855
907
  ref: main
856
- target: .github/workflows/security-scan.yml
857
908
  ```
858
909
 
859
910
  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 +965,12 @@ variables:
914
965
  region: eu-west-1
915
966
 
916
967
  files:
917
- - src:
968
+ k8s/deployment.yaml:
969
+ src:
918
970
  github:
919
971
  repo: org/infra
920
972
  file: k8s/deployment-template.yaml
921
973
  ref: $env:DEPLOY_VERSION
922
- target: k8s/deployment.yaml
923
974
  replace:
924
975
  - from: '{ENV}'
925
976
  to: $env:ENVIRONMENT
@@ -936,24 +987,24 @@ Pull secrets at runtime and write them to local files with tight permissions. Th
936
987
  ```yaml
937
988
  files:
938
989
  # Single field from a Vault KV secret
939
- - src:
990
+ config/db_password.txt:
991
+ src:
940
992
  vault:
941
993
  path: secret/myapp/db
942
994
  field: password
943
- target: config/db_password.txt
944
995
  mode: '0600'
945
996
 
946
997
  # Full Vault secret as JSON
947
- - src:
998
+ config/secrets.json:
999
+ src:
948
1000
  vault:
949
1001
  path: secret/myapp/config
950
- target: config/secrets.json
951
1002
  mode: '0600'
952
1003
 
953
1004
  # Config file stored in S3
954
- - src:
1005
+ config/app.json:
1006
+ src:
955
1007
  s3: s3://my-bucket/configs/app.json
956
- target: config/app.json
957
1008
  mode: '0600'
958
1009
  ```
959
1010
 
@@ -961,13 +1012,13 @@ For AWS SSM or other secret stores without a dedicated source type, `exec:` stil
961
1012
 
962
1013
  ```yaml
963
1014
  files:
964
- - src:
1015
+ config/db.json:
1016
+ src:
965
1017
  exec: >
966
1018
  aws ssm get-parameter
967
1019
  --name /myapp/db-config
968
1020
  --with-decryption
969
1021
  --query Parameter.Value --output text
970
- target: config/db.json
971
1022
  mode: '0600'
972
1023
  ```
973
1024
 
@@ -994,7 +1045,8 @@ variables:
994
1045
  db_password: changeme
995
1046
 
996
1047
  files:
997
- - src:
1048
+ docker-compose.yml:
1049
+ src:
998
1050
  - github:
999
1051
  repo: n8n-io/n8n-hosting
1000
1052
  file: docker-caddy/docker-compose.yml
@@ -1003,7 +1055,6 @@ files:
1003
1055
  repo: docker-library/docs
1004
1056
  file: postgres/compose.yaml
1005
1057
  ref: master
1006
- target: docker-compose.yml
1007
1058
  replace:
1008
1059
  - from: '${N8N_VERSION}'
1009
1060
  to: $n8n_version # pin version at pull time
@@ -1026,19 +1077,22 @@ A single `avanti pull` populates a new project with everything it needs: editor
1026
1077
 
1027
1078
  ```yaml
1028
1079
  files:
1029
- - src:
1080
+ .editorconfig:
1081
+ src:
1030
1082
  github:
1031
1083
  repo: org/standards
1032
1084
  file: .editorconfig
1033
1085
  ref: main
1034
1086
 
1035
- - src:
1087
+ .prettierrc:
1088
+ src:
1036
1089
  github:
1037
1090
  repo: org/standards
1038
1091
  file: .prettierrc
1039
1092
  ref: main
1040
1093
 
1041
- - src:
1094
+ CLAUDE.md:
1095
+ src:
1042
1096
  - raw: |
1043
1097
  # AI Assistant Guidelines
1044
1098
  <!-- THIS FILE IS MANAGED — run `avanti pull` to update -->
@@ -1046,56 +1100,41 @@ files:
1046
1100
  repo: org/ai-standards
1047
1101
  file: CLAUDE.md
1048
1102
  ref: main
1049
- target: CLAUDE.md
1050
1103
 
1051
- - src:
1104
+ .github/workflows/:
1105
+ src:
1052
1106
  github:
1053
1107
  repo: org/ci-templates
1054
1108
  file: workflows/
1055
1109
  ref: main
1056
- target: .github/workflows/
1057
1110
  ```
1058
1111
 
1059
1112
  ### Self-managing Config
1060
1113
 
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.
1114
+ 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
1115
 
1065
1116
  ```yaml
1066
1117
  # ~/.avanti.yml
1067
- variables:
1068
- dotfiles: myorg/dotfiles
1069
-
1070
1118
  files:
1071
- # Keep this config itself up to date
1072
- - src:
1119
+ $self:
1120
+ src:
1073
1121
  github:
1074
- repo: $dotfiles
1122
+ repo: myorg/dotfiles
1075
1123
  file: avanti.yml
1076
1124
  ref: $latest
1077
- target: ~/.avanti.yml
1125
+ ```
1078
1126
 
1079
- # Everything else the config manages
1080
- - src:
1081
- github:
1082
- repo: $dotfiles
1083
- file: .zshrc
1084
- target: ~/.zshrc
1127
+ 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
1128
 
1086
- - src:
1087
- github:
1088
- repo: $dotfiles
1089
- file: .gitconfig
1090
- target: ~/.gitconfig
1091
- ```
1129
+ 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
1130
 
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:
1131
+ **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
1132
 
1095
1133
  ```yaml
1096
- # ~/.avanti.yml — bootstrapped once, then self-updating via YAML merge
1134
+ # ~/.avanti.yml
1097
1135
  files:
1098
- - src:
1136
+ $self:
1137
+ src:
1099
1138
  - github:
1100
1139
  repo: myorg/platform
1101
1140
  file: avanti/base.yml # org-wide entries and variables
@@ -1108,7 +1147,6 @@ files:
1108
1147
  repo: myuser/dotfiles
1109
1148
  file: avanti/personal.yml # personal overrides and extras
1110
1149
  ref: main
1111
- target: ~/.avanti.yml
1112
1150
  yaml:
1113
1151
  conflicts: last_wins # personal overrides win over team, team over org
1114
1152
  arrays: concat # file lists from all layers are merged, not replaced
@@ -1148,8 +1186,8 @@ This scales to any number of machines or containers. Update the central repo onc
1148
1186
 
1149
1187
  ```sh
1150
1188
  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/
1189
+ mise run install # install dependencies and set up git hooks
1190
+ mise run dev -- --help # run via tsx
1191
+ mise run test # run tests
1192
+ mise run build # compile to dist/
1155
1193
  ```