Compare commits
8 Commits
6427d188e6
...
4b4b1df5db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b4b1df5db | ||
| 0cfa0d9b3c | |||
| 1a3aaf2dd9 | |||
| 3a475bc25b | |||
| 039f8d808d | |||
| ca7c035ea5 | |||
| 25624c9f2b | |||
|
|
4b344621bc |
BIN
.claude/.DS_Store
vendored
BIN
.claude/.DS_Store
vendored
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
project: notfiles
|
project: notfiles
|
||||||
id: notfile
|
id: notfile
|
||||||
updated: 2026-07-07
|
updated: 2026-07-08
|
||||||
items:
|
items:
|
||||||
- id: uncommitted-work
|
- id: uncommitted-work
|
||||||
doob_uuid: null
|
doob_uuid: null
|
||||||
@@ -20,6 +20,11 @@ items:
|
|||||||
completed: null
|
completed: null
|
||||||
extra: []
|
extra: []
|
||||||
log:
|
log:
|
||||||
|
- date: 2026-07-08
|
||||||
|
summary: hexagonal architecture refactor with adapters and integration tests, notsecrets cleanup, config payload moved to notfiles-config repo
|
||||||
|
commits:
|
||||||
|
- 6427d18
|
||||||
|
- 25624c9
|
||||||
- date: '20260606.135918'
|
- date: '20260606.135918'
|
||||||
summary: done=13 running=0 pending=0 blocked=0
|
summary: done=13 running=0 pending=0 blocked=0
|
||||||
commits: []
|
commits: []
|
||||||
|
|||||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -32,6 +32,9 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- uses: hustcer/setup-nu@v3
|
||||||
|
with:
|
||||||
|
version: 0.112.2
|
||||||
- uses: taiki-e/install-action@v2
|
- uses: taiki-e/install-action@v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-nextest
|
tool: cargo-nextest
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,8 @@
|
|||||||
/target
|
/target
|
||||||
|
.DS_Store
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.venv/
|
||||||
.envrc
|
.envrc
|
||||||
.worktrees/
|
.worktrees/
|
||||||
.claude.local.md
|
.claude.local.md
|
||||||
|
|||||||
BIN
.superpowers/brainstorm/72758-1776397235/.DS_Store
vendored
BIN
.superpowers/brainstorm/72758-1776397235/.DS_Store
vendored
Binary file not shown.
449
Cargo.lock
generated
449
Cargo.lock
generated
@@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aead"
|
name = "aead"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -332,6 +338,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -479,6 +494,17 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "displaydoc"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "doc-comment"
|
name = "doc-comment"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -606,12 +632,31 @@ version = "0.4.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -745,12 +790,115 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_collections"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"potential_utf",
|
||||||
|
"utf8_iter",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_locale_core"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"litemap",
|
||||||
|
"tinystr",
|
||||||
|
"writeable",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_normalizer"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||||
|
dependencies = [
|
||||||
|
"icu_collections",
|
||||||
|
"icu_normalizer_data",
|
||||||
|
"icu_properties",
|
||||||
|
"icu_provider",
|
||||||
|
"smallvec",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_normalizer_data"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_properties"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||||
|
dependencies = [
|
||||||
|
"icu_collections",
|
||||||
|
"icu_locale_core",
|
||||||
|
"icu_properties_data",
|
||||||
|
"icu_provider",
|
||||||
|
"zerotrie",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_properties_data"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_provider"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"icu_locale_core",
|
||||||
|
"writeable",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerotrie",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "id-arena"
|
name = "id-arena"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||||
|
dependencies = [
|
||||||
|
"idna_adapter",
|
||||||
|
"smallvec",
|
||||||
|
"utf8_iter",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna_adapter"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
||||||
|
dependencies = [
|
||||||
|
"icu_normalizer",
|
||||||
|
"icu_properties",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ignore"
|
name = "ignore"
|
||||||
version = "0.4.25"
|
version = "0.4.25"
|
||||||
@@ -866,6 +1014,12 @@ version = "0.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litemap"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
@@ -878,12 +1032,44 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miette"
|
||||||
|
version = "7.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"miette-derive",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miette-derive"
|
||||||
|
version = "7.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@@ -924,6 +1110,23 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notforge"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64",
|
||||||
|
"clap",
|
||||||
|
"miette",
|
||||||
|
"notcore",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"toml",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notgraph"
|
name = "notgraph"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1148,6 +1351,12 @@ dependencies = [
|
|||||||
"base64ct",
|
"base64ct",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs1"
|
name = "pkcs1"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
@@ -1186,6 +1395,15 @@ dependencies = [
|
|||||||
"universal-hash",
|
"universal-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "potential_utf"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||||
|
dependencies = [
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -1333,6 +1551,20 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpassword"
|
name = "rpassword"
|
||||||
version = "7.4.0"
|
version = "7.4.0"
|
||||||
@@ -1397,6 +1629,41 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -1556,6 +1823,12 @@ dependencies = [
|
|||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -1578,6 +1851,12 @@ dependencies = [
|
|||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stable_deref_trait"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -1601,6 +1880,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "synstructure"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
@@ -1660,6 +1950,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinystr"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tls_codec"
|
name = "tls_codec"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -1734,6 +2034,12 @@ version = "1.0.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -1750,6 +2056,48 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "2.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"flate2",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "2.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"idna",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8_iter"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -1886,6 +2234,24 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
|
dependencies = [
|
||||||
|
"webpki-roots 1.0.8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "7.0.3"
|
version = "7.0.3"
|
||||||
@@ -2160,6 +2526,12 @@ dependencies = [
|
|||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "writeable"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x25519-dalek"
|
name = "x25519-dalek"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -2186,6 +2558,29 @@ dependencies = [
|
|||||||
"tls_codec",
|
"tls_codec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
"yoke-derive",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke-derive"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yubikey"
|
name = "yubikey"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -2239,6 +2634,27 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||||
|
dependencies = [
|
||||||
|
"zerofrom-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom-derive"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.8.2"
|
version = "1.8.2"
|
||||||
@@ -2259,6 +2675,39 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerotrie"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerovec"
|
||||||
|
version = "0.11.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||||
|
dependencies = [
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerovec-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerovec-derive"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ members = [
|
|||||||
"crates/notnet",
|
"crates/notnet",
|
||||||
"crates/notstrap",
|
"crates/notstrap",
|
||||||
"crates/notgraph",
|
"crates/notgraph",
|
||||||
|
"crates/notforge",
|
||||||
"tests/integration",
|
"tests/integration",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
base64 = "0.22"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
clap_complete = "4"
|
clap_complete = "4"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
@@ -31,6 +33,8 @@ serde_json = "1"
|
|||||||
strsim = "0.11"
|
strsim = "0.11"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
miette = { version = "7", features = ["derive"] }
|
||||||
|
ureq = { version = "2", features = ["json"] }
|
||||||
which = "7"
|
which = "7"
|
||||||
rpassword = "7"
|
rpassword = "7"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
@@ -40,6 +44,7 @@ notfiles = { path = "crates/notfiles" }
|
|||||||
notsecrets = { path = "crates/notsecrets" }
|
notsecrets = { path = "crates/notsecrets" }
|
||||||
nothooks = { path = "crates/nothooks" }
|
nothooks = { path = "crates/nothooks" }
|
||||||
notnet = { path = "crates/notnet" }
|
notnet = { path = "crates/notnet" }
|
||||||
|
notforge = { path = "crates/notforge" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -265,6 +265,23 @@ Migration is gradual — shell scripts are replaced as Rust equivalents ship:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Absorbing `pj`
|
||||||
|
|
||||||
|
`pj` (89jobrien/pj) was a standalone portable dev-environment bootstrap CLI. Its functionality overlaps directly with this workspace:
|
||||||
|
|
||||||
|
| `pj` command | `notfiles` equivalent |
|
||||||
|
|---|---|
|
||||||
|
| `pj dot` | `notfiles` (symlink engine) |
|
||||||
|
| `pj doctor` | `notstrap` prerequisites check |
|
||||||
|
| `pj sync` | `notstrap run` |
|
||||||
|
| `pj secret` | `notsecrets` |
|
||||||
|
| `pj cache` | planned `notcache` or Cargo workspace tooling |
|
||||||
|
| `pj tui` | planned TUI layer on `notstrap` |
|
||||||
|
|
||||||
|
`pj` is archived. Its remaining functionality (cache management, TUI dashboard, git-config helper) will be folded in as those areas of the workspace mature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
BIN
crates/.DS_Store
vendored
BIN
crates/.DS_Store
vendored
Binary file not shown.
@@ -10,9 +10,9 @@ categories.workspace = true
|
|||||||
description = "Shared types and config for the notfiles dotfiles manager"
|
description = "Shared types and config for the notfiles dotfiles manager"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
|
||||||
toml = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
dirs = { workspace = true }
|
|
||||||
strsim = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
dirs = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
strsim = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -85,6 +86,22 @@ impl std::fmt::Display for Method {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load and deserialize a TOML config file, mapping I/O and parse errors
|
||||||
|
/// into the caller's crate-specific error type.
|
||||||
|
pub fn load_toml_file<T, E, ReadError, ParseError>(
|
||||||
|
path: &Path,
|
||||||
|
read_error: ReadError,
|
||||||
|
parse_error: ParseError,
|
||||||
|
) -> Result<T, E>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
ReadError: FnOnce(&Path, std::io::Error) -> E,
|
||||||
|
ParseError: FnOnce(&Path, toml::de::Error) -> E,
|
||||||
|
{
|
||||||
|
let content = std::fs::read_to_string(path).map_err(|err| read_error(path, err))?;
|
||||||
|
toml::from_str(&content).map_err(|err| parse_error(path, err))
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load(dotfiles_dir: &Path) -> Result<Self, NotfilesError> {
|
pub fn load(dotfiles_dir: &Path) -> Result<Self, NotfilesError> {
|
||||||
let config_path = dotfiles_dir.join("notfiles.toml");
|
let config_path = dotfiles_dir.join("notfiles.toml");
|
||||||
@@ -204,6 +221,10 @@ ignore = [".git", ".DS_Store", "README.md", "LICENSE", "notfiles.toml", ".notfil
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn temp_config_path(name: &str) -> std::path::PathBuf {
|
||||||
|
std::env::temp_dir().join(format!("notcore-{name}-{}.toml", std::process::id()))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_default_config() {
|
fn test_default_config() {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
@@ -323,4 +344,40 @@ target = "~/bin"
|
|||||||
assert!(ssh_ignores.contains(&".git"));
|
assert!(ssh_ignores.contains(&".git"));
|
||||||
assert!(ssh_ignores.contains(&"known_hosts"));
|
assert!(ssh_ignores.contains(&"known_hosts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_toml_file_deserializes_config() {
|
||||||
|
let path = temp_config_path("load-toml-file-ok");
|
||||||
|
std::fs::write(&path, "[defaults]\ntarget = \"~/dotfiles\"\n")
|
||||||
|
.expect("test should write temporary config");
|
||||||
|
|
||||||
|
let config: Config = load_toml_file(
|
||||||
|
&path,
|
||||||
|
|path, err| format!("read {}: {err}", path.display()),
|
||||||
|
|path, err| format!("parse {}: {err}", path.display()),
|
||||||
|
)
|
||||||
|
.expect("temporary config should parse");
|
||||||
|
|
||||||
|
assert_eq!(config.defaults.target, "~/dotfiles");
|
||||||
|
|
||||||
|
std::fs::remove_file(&path).expect("test should remove temporary config");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_toml_file_maps_parse_error() {
|
||||||
|
let path = temp_config_path("load-toml-file-parse-error");
|
||||||
|
std::fs::write(&path, "[defaults\ntarget = \"~/dotfiles\"\n")
|
||||||
|
.expect("test should write invalid temporary config");
|
||||||
|
|
||||||
|
let result: Result<Config, String> = load_toml_file(
|
||||||
|
&path,
|
||||||
|
|path, err| format!("read {}: {err}", path.display()),
|
||||||
|
|path, err| format!("parse {}: {err}", path.display()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = result.expect_err("invalid TOML should return parse error");
|
||||||
|
assert!(err.contains("parse "));
|
||||||
|
|
||||||
|
std::fs::remove_file(&path).expect("test should remove temporary config");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
crates/notfiles/.DS_Store
vendored
BIN
crates/notfiles/.DS_Store
vendored
Binary file not shown.
@@ -18,17 +18,17 @@ name = "notfiles"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
notcore = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
clap_complete = { workspace = true }
|
clap_complete = { workspace = true }
|
||||||
|
dirs = { workspace = true }
|
||||||
globset = { workspace = true }
|
globset = { workspace = true }
|
||||||
chrono = { workspace = true }
|
notcore = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
|
||||||
dirs = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
|
||||||
assert_fs = { workspace = true }
|
assert_fs = { workspace = true }
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
|||||||
33
crates/notforge/Cargo.toml
Normal file
33
crates/notforge/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "notforge"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
keywords.workspace = true
|
||||||
|
categories.workspace = true
|
||||||
|
description = "Forge lifecycle and repository provisioning for notfiles"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "notforge"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "notforge"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
base64 = { workspace = true }
|
||||||
|
clap = { workspace = true }
|
||||||
|
miette = { workspace = true }
|
||||||
|
notcore = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
ureq = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = { workspace = true }
|
||||||
176
crates/notforge/src/config.rs
Normal file
176
crates/notforge/src/config.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::error::NotforgeError;
|
||||||
|
|
||||||
|
/// Top-level forge configuration.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ForgeConfig {
|
||||||
|
/// Gitea backend configuration.
|
||||||
|
pub gitea: GiteaConfig,
|
||||||
|
/// Repositories managed by this forge configuration.
|
||||||
|
#[serde(default)]
|
||||||
|
pub repositories: Vec<RepoSpec>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gitea backend configuration.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct GiteaConfig {
|
||||||
|
/// Base HTTP URL for the Gitea API.
|
||||||
|
pub base_url: String,
|
||||||
|
/// Default owner namespace for repositories.
|
||||||
|
pub owner: String,
|
||||||
|
/// Hostname used in generated SSH clone URLs.
|
||||||
|
pub ssh_host: String,
|
||||||
|
/// SSH port used in generated SSH clone URLs.
|
||||||
|
pub ssh_port: u16,
|
||||||
|
/// Backend mode used to verify or start Gitea.
|
||||||
|
pub mode: GiteaMode,
|
||||||
|
/// Authentication configuration for Gitea API calls.
|
||||||
|
#[serde(default)]
|
||||||
|
pub auth: ForgeAuthConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supported Gitea backend modes.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub enum GiteaMode {
|
||||||
|
/// Run Gitea directly as a local process.
|
||||||
|
LocalProcess(LocalProcessConfig),
|
||||||
|
/// Run Gitea via a local container runtime.
|
||||||
|
LocalContainer(LocalContainerConfig),
|
||||||
|
/// Verify or start Gitea as a systemd service on a VM.
|
||||||
|
VmSystemd(VmSystemdConfig),
|
||||||
|
/// Use an already-running Gitea instance.
|
||||||
|
Existing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for GiteaMode {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum RawMode {
|
||||||
|
Name(String),
|
||||||
|
Tagged {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: String,
|
||||||
|
#[serde(default)]
|
||||||
|
config: Option<toml::Value>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = RawMode::deserialize(deserializer)?;
|
||||||
|
match raw {
|
||||||
|
RawMode::Name(name) if name == "existing" => Ok(Self::Existing),
|
||||||
|
RawMode::Name(name) => Err(serde::de::Error::custom(format!(
|
||||||
|
"unknown gitea mode '{name}'"
|
||||||
|
))),
|
||||||
|
RawMode::Tagged { kind, config } => match kind.as_str() {
|
||||||
|
"existing" => Ok(Self::Existing),
|
||||||
|
"local-process" => Ok(Self::LocalProcess(
|
||||||
|
config
|
||||||
|
.ok_or_else(|| serde::de::Error::custom("local-process requires config"))?
|
||||||
|
.try_into()
|
||||||
|
.map_err(serde::de::Error::custom)?,
|
||||||
|
)),
|
||||||
|
"local-container" => Ok(Self::LocalContainer(
|
||||||
|
config
|
||||||
|
.ok_or_else(|| serde::de::Error::custom("local-container requires config"))?
|
||||||
|
.try_into()
|
||||||
|
.map_err(serde::de::Error::custom)?,
|
||||||
|
)),
|
||||||
|
"vm-systemd" => Ok(Self::VmSystemd(
|
||||||
|
config
|
||||||
|
.ok_or_else(|| serde::de::Error::custom("vm-systemd requires config"))?
|
||||||
|
.try_into()
|
||||||
|
.map_err(serde::de::Error::custom)?,
|
||||||
|
)),
|
||||||
|
other => Err(serde::de::Error::custom(format!(
|
||||||
|
"unknown gitea mode '{other}'"
|
||||||
|
))),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local process runner configuration.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct LocalProcessConfig {
|
||||||
|
/// Path or command name for the Gitea binary.
|
||||||
|
pub binary: String,
|
||||||
|
/// Working directory for the Gitea process.
|
||||||
|
pub work_dir: String,
|
||||||
|
/// Gitea app.ini path.
|
||||||
|
pub config_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local container runner configuration.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct LocalContainerConfig {
|
||||||
|
/// Container runtime executable, such as docker or podman.
|
||||||
|
pub runtime: String,
|
||||||
|
/// Container name to verify or start.
|
||||||
|
pub container_name: String,
|
||||||
|
/// Container image to run when missing.
|
||||||
|
pub image: String,
|
||||||
|
/// Host data directory mounted into the container.
|
||||||
|
pub data_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remote VM systemd runner configuration.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct VmSystemdConfig {
|
||||||
|
/// SSH user for the VM.
|
||||||
|
pub ssh_user: String,
|
||||||
|
/// SSH host for the VM.
|
||||||
|
pub ssh_host: String,
|
||||||
|
/// systemd service name for Gitea.
|
||||||
|
pub service_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authentication configuration for Gitea API calls.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub struct ForgeAuthConfig {
|
||||||
|
/// Token secret reference, preferred when available.
|
||||||
|
pub token: Option<ForgeSecretRef>,
|
||||||
|
/// Username secret reference for basic auth fallback.
|
||||||
|
pub username: Option<ForgeSecretRef>,
|
||||||
|
/// Password secret reference for basic auth fallback.
|
||||||
|
pub password: Option<ForgeSecretRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Secret reference used by notforge.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "source", rename_all = "lowercase")]
|
||||||
|
pub enum ForgeSecretRef {
|
||||||
|
/// Resolve from an environment variable.
|
||||||
|
Env { key: String },
|
||||||
|
/// Resolve from a 1Password URI.
|
||||||
|
Op { uri: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repository specification managed by a forge config.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct RepoSpec {
|
||||||
|
/// Repository owner namespace.
|
||||||
|
pub owner: String,
|
||||||
|
/// Repository name.
|
||||||
|
pub name: String,
|
||||||
|
/// Whether the repository should be private.
|
||||||
|
pub private: bool,
|
||||||
|
/// Optional repository description.
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// Local git remote name.
|
||||||
|
pub remote_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a forge configuration from TOML.
|
||||||
|
pub fn load_config(path: &Path) -> Result<ForgeConfig, NotforgeError> {
|
||||||
|
notcore::config::load_toml_file(
|
||||||
|
path,
|
||||||
|
|path, err| NotforgeError::Config(format!("reading {}: {err}", path.display())),
|
||||||
|
|path, err| NotforgeError::Config(format!("parsing {}: {err}", path.display())),
|
||||||
|
)
|
||||||
|
}
|
||||||
36
crates/notforge/src/error.rs
Normal file
36
crates/notforge/src/error.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use miette::Diagnostic;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Error type for forge lifecycle, API, git, and secret-resolution failures.
|
||||||
|
#[derive(Debug, Diagnostic, Error)]
|
||||||
|
pub enum NotforgeError {
|
||||||
|
/// Configuration could not be loaded or parsed.
|
||||||
|
#[error("configuration error: {0}")]
|
||||||
|
#[diagnostic(code(notforge::config))]
|
||||||
|
Config(String),
|
||||||
|
|
||||||
|
/// A required secret could not be resolved.
|
||||||
|
#[error("secret resolution error: {0}")]
|
||||||
|
#[diagnostic(code(notforge::secret))]
|
||||||
|
Secret(String),
|
||||||
|
|
||||||
|
/// Forge API communication failed.
|
||||||
|
#[error("HTTP error: {0}")]
|
||||||
|
#[diagnostic(code(notforge::http))]
|
||||||
|
Http(String),
|
||||||
|
|
||||||
|
/// A lifecycle command failed.
|
||||||
|
#[error("command error: {0}")]
|
||||||
|
#[diagnostic(code(notforge::command))]
|
||||||
|
Command(String),
|
||||||
|
|
||||||
|
/// A git operation failed.
|
||||||
|
#[error("git error: {0}")]
|
||||||
|
#[diagnostic(code(notforge::git))]
|
||||||
|
Git(String),
|
||||||
|
|
||||||
|
/// Repository lookup or provisioning failed.
|
||||||
|
#[error("repository error: {0}")]
|
||||||
|
#[diagnostic(code(notforge::repository))]
|
||||||
|
Repository(String),
|
||||||
|
}
|
||||||
302
crates/notforge/src/gitea.rs
Normal file
302
crates/notforge/src/gitea.rs
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
use base64::Engine as _;
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::config::RepoSpec;
|
||||||
|
use crate::error::NotforgeError;
|
||||||
|
use crate::ports::{ForgeApi, ForgeAuth, ForgeVersion, RemoteRepository};
|
||||||
|
|
||||||
|
/// HTTP method used by the Gitea transport boundary.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum HttpMethod {
|
||||||
|
/// HTTP GET.
|
||||||
|
Get,
|
||||||
|
/// HTTP POST.
|
||||||
|
Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HttpMethod {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Get => formatter.write_str("GET"),
|
||||||
|
Self::Post => formatter.write_str("POST"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transport-level request sent by the Gitea adapter.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct GiteaHttpRequest {
|
||||||
|
/// HTTP method.
|
||||||
|
pub method: HttpMethod,
|
||||||
|
/// Absolute URL to call.
|
||||||
|
pub url: String,
|
||||||
|
/// Optional Authorization header value.
|
||||||
|
pub authorization: Option<String>,
|
||||||
|
/// Optional JSON body.
|
||||||
|
pub body: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transport-level response returned to the Gitea adapter.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct GiteaHttpResponse {
|
||||||
|
/// HTTP status code.
|
||||||
|
pub status: u16,
|
||||||
|
/// Parsed JSON response body, or `null` for an empty body.
|
||||||
|
pub body: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP transport boundary for Gitea API requests.
|
||||||
|
pub trait GiteaTransport {
|
||||||
|
/// Send a transport-level request.
|
||||||
|
fn send(&self, request: GiteaHttpRequest) -> Result<GiteaHttpResponse, NotforgeError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gitea API adapter that implements [`ForgeApi`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GiteaHttpApi<T = UreqGiteaTransport> {
|
||||||
|
base_url: String,
|
||||||
|
transport: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GiteaHttpApi<UreqGiteaTransport> {
|
||||||
|
/// Create a Gitea API adapter backed by the default blocking HTTP transport.
|
||||||
|
pub fn new(base_url: impl Into<String>) -> Self {
|
||||||
|
Self::with_transport(base_url, UreqGiteaTransport::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GiteaHttpApi<T>
|
||||||
|
where
|
||||||
|
T: GiteaTransport,
|
||||||
|
{
|
||||||
|
/// Create a Gitea API adapter with an injected transport.
|
||||||
|
pub fn with_transport(base_url: impl Into<String>, transport: T) -> Self {
|
||||||
|
Self {
|
||||||
|
base_url: base_url.into().trim_end_matches('/').to_string(),
|
||||||
|
transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self, path: &str) -> String {
|
||||||
|
format!("{}{}", self.base_url, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, path: &str) -> Result<GiteaHttpResponse, NotforgeError> {
|
||||||
|
self.transport.send(GiteaHttpRequest {
|
||||||
|
method: HttpMethod::Get,
|
||||||
|
url: self.url(path),
|
||||||
|
authorization: None,
|
||||||
|
body: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
auth: &ForgeAuth,
|
||||||
|
body: Value,
|
||||||
|
) -> Result<GiteaHttpResponse, NotforgeError> {
|
||||||
|
self.transport.send(GiteaHttpRequest {
|
||||||
|
method: HttpMethod::Post,
|
||||||
|
url: self.url(path),
|
||||||
|
authorization: Some(authorization_header(auth)),
|
||||||
|
body: Some(body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ForgeApi for GiteaHttpApi<T>
|
||||||
|
where
|
||||||
|
T: GiteaTransport,
|
||||||
|
{
|
||||||
|
fn version(&self) -> Result<ForgeVersion, NotforgeError> {
|
||||||
|
let response = self.get("/api/v1/version")?;
|
||||||
|
require_status(response.status, &[200], "get Gitea version")?;
|
||||||
|
let payload: GiteaVersionResponse = parse_body(response.body, "Gitea version response")?;
|
||||||
|
Ok(ForgeVersion {
|
||||||
|
version: payload.version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repo(&self, owner: &str, name: &str) -> Result<Option<RemoteRepository>, NotforgeError> {
|
||||||
|
let response = self.get(&format!("/api/v1/repos/{owner}/{name}"))?;
|
||||||
|
if response.status == 404 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_status(response.status, &[200], "get Gitea repository")?;
|
||||||
|
let payload: GiteaRepositoryResponse =
|
||||||
|
parse_body(response.body, "Gitea repository response")?;
|
||||||
|
Ok(Some(payload.into_remote_repository()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_repo(
|
||||||
|
&self,
|
||||||
|
spec: &RepoSpec,
|
||||||
|
auth: &ForgeAuth,
|
||||||
|
) -> Result<RemoteRepository, NotforgeError> {
|
||||||
|
let response = self.post("/api/v1/user/repos", auth, create_repo_body(spec))?;
|
||||||
|
require_status(response.status, &[200, 201], "create Gitea repository")?;
|
||||||
|
let payload: GiteaRepositoryResponse =
|
||||||
|
parse_body(response.body, "Gitea repository response")?;
|
||||||
|
Ok(payload.into_remote_repository())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blocking HTTP transport implemented with `ureq`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UreqGiteaTransport {
|
||||||
|
agent: ureq::Agent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UreqGiteaTransport {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
agent: ureq::Agent::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UreqGiteaTransport {
|
||||||
|
/// Create a transport with a custom `ureq` agent.
|
||||||
|
pub fn new(agent: ureq::Agent) -> Self {
|
||||||
|
Self { agent }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GiteaTransport for UreqGiteaTransport {
|
||||||
|
fn send(&self, request: GiteaHttpRequest) -> Result<GiteaHttpResponse, NotforgeError> {
|
||||||
|
let method = request.method;
|
||||||
|
let url = request.url.clone();
|
||||||
|
let mut call = match method {
|
||||||
|
HttpMethod::Get => self.agent.get(&url),
|
||||||
|
HttpMethod::Post => self.agent.post(&url),
|
||||||
|
}
|
||||||
|
.set("Accept", "application/json");
|
||||||
|
|
||||||
|
if let Some(authorization) = request.authorization {
|
||||||
|
call = call.set("Authorization", &authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match method {
|
||||||
|
HttpMethod::Get => call.call(),
|
||||||
|
HttpMethod::Post => call
|
||||||
|
.set("Content-Type", "application/json")
|
||||||
|
.send_json(request.body.unwrap_or_else(|| json!({}))),
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(response) => response_from_ureq(response),
|
||||||
|
Err(ureq::Error::Status(status, response)) => response_from_status(status, response),
|
||||||
|
Err(err) => Err(NotforgeError::Http(format!(
|
||||||
|
"{method} {url} failed before response: {err}"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct GiteaVersionResponse {
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct GiteaRepositoryResponse {
|
||||||
|
owner: GiteaOwnerResponse,
|
||||||
|
name: String,
|
||||||
|
clone_url: String,
|
||||||
|
ssh_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GiteaRepositoryResponse {
|
||||||
|
fn into_remote_repository(self) -> RemoteRepository {
|
||||||
|
RemoteRepository {
|
||||||
|
owner: self.owner.login,
|
||||||
|
name: self.name,
|
||||||
|
clone_url: self.clone_url,
|
||||||
|
ssh_url: self.ssh_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct GiteaOwnerResponse {
|
||||||
|
login: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CreateRepoRequest<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
private: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
description: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_repo_body(spec: &RepoSpec) -> Value {
|
||||||
|
serde_json::to_value(CreateRepoRequest {
|
||||||
|
name: &spec.name,
|
||||||
|
private: spec.private,
|
||||||
|
description: spec.description.as_deref(),
|
||||||
|
})
|
||||||
|
.expect("CreateRepoRequest serialization should not fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorization_header(auth: &ForgeAuth) -> String {
|
||||||
|
match auth {
|
||||||
|
ForgeAuth::Token { token } => format!("token {token}"),
|
||||||
|
ForgeAuth::Basic { username, password } => {
|
||||||
|
format!(
|
||||||
|
"Basic {}",
|
||||||
|
STANDARD.encode(format!("{username}:{password}"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_status(status: u16, expected: &[u16], action: &str) -> Result<(), NotforgeError> {
|
||||||
|
if expected.contains(&status) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(NotforgeError::Http(format!(
|
||||||
|
"{action} returned unexpected status {status}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_body<T>(body: Value, context: &str) -> Result<T, NotforgeError>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
serde_json::from_value(body)
|
||||||
|
.map_err(|err| NotforgeError::Http(format!("invalid {context}: {err}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_from_status(
|
||||||
|
status: u16,
|
||||||
|
response: ureq::Response,
|
||||||
|
) -> Result<GiteaHttpResponse, NotforgeError> {
|
||||||
|
response_from_parts(status, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_from_ureq(response: ureq::Response) -> Result<GiteaHttpResponse, NotforgeError> {
|
||||||
|
response_from_parts(response.status(), response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_from_parts(
|
||||||
|
status: u16,
|
||||||
|
response: ureq::Response,
|
||||||
|
) -> Result<GiteaHttpResponse, NotforgeError> {
|
||||||
|
let body = response
|
||||||
|
.into_string()
|
||||||
|
.map_err(|err| NotforgeError::Http(format!("reading HTTP response body: {err}")))?;
|
||||||
|
let body = if body.trim().is_empty() {
|
||||||
|
Value::Null
|
||||||
|
} else {
|
||||||
|
serde_json::from_str(&body)
|
||||||
|
.map_err(|err| NotforgeError::Http(format!("parsing HTTP response JSON: {err}")))?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(GiteaHttpResponse { status, body })
|
||||||
|
}
|
||||||
19
crates/notforge/src/lib.rs
Normal file
19
crates/notforge/src/lib.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//! Forge lifecycle and repository provisioning for notfiles.
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod error;
|
||||||
|
pub mod gitea;
|
||||||
|
pub mod ports;
|
||||||
|
|
||||||
|
/// Current `notforge` crate version.
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exposes_package_version() {
|
||||||
|
assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
|
||||||
|
}
|
||||||
|
}
|
||||||
3
crates/notforge/src/main.rs
Normal file
3
crates/notforge/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("{}", notforge::VERSION);
|
||||||
|
}
|
||||||
126
crates/notforge/src/ports.rs
Normal file
126
crates/notforge/src/ports.rs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::config::{ForgeConfig, ForgeSecretRef, RepoSpec};
|
||||||
|
use crate::error::NotforgeError;
|
||||||
|
|
||||||
|
/// Authentication material resolved for forge API requests.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ForgeAuth {
|
||||||
|
/// Token-based Gitea API authentication.
|
||||||
|
Token { token: String },
|
||||||
|
/// Basic authentication fallback for bootstrap flows.
|
||||||
|
Basic { username: String, password: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remote repository metadata returned by a forge.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct RemoteRepository {
|
||||||
|
/// Repository owner namespace.
|
||||||
|
pub owner: String,
|
||||||
|
/// Repository name.
|
||||||
|
pub name: String,
|
||||||
|
/// HTTP clone URL.
|
||||||
|
pub clone_url: String,
|
||||||
|
/// SSH clone URL.
|
||||||
|
pub ssh_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forge version metadata.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ForgeVersion {
|
||||||
|
/// Server version string.
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local git repository path.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LocalRepository {
|
||||||
|
/// Filesystem path to the local repository.
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of ensuring a forge backend is available.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum LifecycleStatus {
|
||||||
|
/// The forge was already reachable.
|
||||||
|
AlreadyAvailable,
|
||||||
|
/// The forge was started by this operation.
|
||||||
|
Started,
|
||||||
|
/// The forge was verified without needing a start operation.
|
||||||
|
Verified,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of setting a local git remote.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum RemoteStatus {
|
||||||
|
/// The remote was added.
|
||||||
|
Added,
|
||||||
|
/// The remote existed and was updated.
|
||||||
|
Updated,
|
||||||
|
/// The remote already matched the desired URL.
|
||||||
|
Unchanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of pushing a local branch.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PushStatus {
|
||||||
|
/// The branch was pushed.
|
||||||
|
Pushed,
|
||||||
|
/// The branch already matched the remote.
|
||||||
|
AlreadyUpToDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Port for forge API operations.
|
||||||
|
pub trait ForgeApi {
|
||||||
|
/// Return server version metadata.
|
||||||
|
fn version(&self) -> Result<ForgeVersion, NotforgeError>;
|
||||||
|
|
||||||
|
/// Look up a repository by owner and name.
|
||||||
|
fn repo(&self, owner: &str, name: &str) -> Result<Option<RemoteRepository>, NotforgeError>;
|
||||||
|
|
||||||
|
/// Create a repository from a spec and resolved auth.
|
||||||
|
fn create_repo(
|
||||||
|
&self,
|
||||||
|
spec: &RepoSpec,
|
||||||
|
auth: &ForgeAuth,
|
||||||
|
) -> Result<RemoteRepository, NotforgeError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Port for local or remote forge lifecycle operations.
|
||||||
|
pub trait ForgeLifecycle {
|
||||||
|
/// Ensure the configured forge backend is available.
|
||||||
|
fn ensure_available(&self, config: &ForgeConfig) -> Result<LifecycleStatus, NotforgeError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Port for git remote and push operations.
|
||||||
|
pub trait GitRemoteManager {
|
||||||
|
/// Return the URL for a local remote if it exists.
|
||||||
|
fn remote_url(
|
||||||
|
&self,
|
||||||
|
repo: &LocalRepository,
|
||||||
|
remote_name: &str,
|
||||||
|
) -> Result<Option<String>, NotforgeError>;
|
||||||
|
|
||||||
|
/// Add or update a local git remote.
|
||||||
|
fn set_remote(
|
||||||
|
&self,
|
||||||
|
repo: &LocalRepository,
|
||||||
|
remote_name: &str,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<RemoteStatus, NotforgeError>;
|
||||||
|
|
||||||
|
/// Push a branch to a local remote.
|
||||||
|
fn push(
|
||||||
|
&self,
|
||||||
|
repo: &LocalRepository,
|
||||||
|
remote_name: &str,
|
||||||
|
branch: &str,
|
||||||
|
set_upstream: bool,
|
||||||
|
) -> Result<PushStatus, NotforgeError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Port for resolving forge secrets.
|
||||||
|
pub trait SecretResolverPort {
|
||||||
|
/// Resolve a secret reference into an in-memory secret value.
|
||||||
|
fn resolve(&self, secret: &ForgeSecretRef) -> Result<String, NotforgeError>;
|
||||||
|
}
|
||||||
222
crates/notforge/tests/api.rs
Normal file
222
crates/notforge/tests/api.rs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use notforge::config::{
|
||||||
|
ForgeAuthConfig, ForgeConfig, ForgeSecretRef, GiteaMode, RepoSpec, VmSystemdConfig, load_config,
|
||||||
|
};
|
||||||
|
use notforge::error::NotforgeError;
|
||||||
|
use notforge::ports::{
|
||||||
|
ForgeApi, ForgeAuth, ForgeLifecycle, ForgeVersion, GitRemoteManager, LifecycleStatus,
|
||||||
|
LocalRepository, PushStatus, RemoteRepository, RemoteStatus, SecretResolverPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn assert_diagnostic<E: miette::Diagnostic>() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn notforge_error_implements_miette_diagnostic() {
|
||||||
|
assert_diagnostic::<NotforgeError>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_parses_vm_systemd_gitea_mode() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let config_path = dir.path().join("notforge.toml");
|
||||||
|
std::fs::write(
|
||||||
|
&config_path,
|
||||||
|
r#"
|
||||||
|
[gitea]
|
||||||
|
base_url = "http://gitea.local:3000"
|
||||||
|
owner = "joe"
|
||||||
|
ssh_host = "gitea.local"
|
||||||
|
ssh_port = 2222
|
||||||
|
|
||||||
|
[gitea.mode]
|
||||||
|
type = "vm-systemd"
|
||||||
|
|
||||||
|
[gitea.mode.config]
|
||||||
|
ssh_user = "dev"
|
||||||
|
ssh_host = "gitea.local"
|
||||||
|
service_name = "gitea"
|
||||||
|
|
||||||
|
[gitea.auth.token]
|
||||||
|
source = "env"
|
||||||
|
key = "GITEA_TOKEN"
|
||||||
|
|
||||||
|
[[repositories]]
|
||||||
|
owner = "joe"
|
||||||
|
name = "notfiles-config"
|
||||||
|
private = true
|
||||||
|
description = "Personal config payload"
|
||||||
|
remote_name = "gitea"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = load_config(&config_path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(config.gitea.base_url, "http://gitea.local:3000");
|
||||||
|
assert_eq!(config.gitea.owner, "joe");
|
||||||
|
assert_eq!(config.gitea.ssh_port, 2222);
|
||||||
|
assert_eq!(
|
||||||
|
config.gitea.auth.token,
|
||||||
|
Some(ForgeSecretRef::Env {
|
||||||
|
key: "GITEA_TOKEN".to_string()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
config.gitea.mode,
|
||||||
|
GiteaMode::VmSystemd(VmSystemdConfig {
|
||||||
|
ssh_user: "dev".to_string(),
|
||||||
|
ssh_host: "gitea.local".to_string(),
|
||||||
|
service_name: "gitea".to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
config.repositories,
|
||||||
|
vec![RepoSpec {
|
||||||
|
owner: "joe".to_string(),
|
||||||
|
name: "notfiles-config".to_string(),
|
||||||
|
private: true,
|
||||||
|
description: Some("Personal config payload".to_string()),
|
||||||
|
remote_name: "gitea".to_string(),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_defaults_repositories_to_empty() {
|
||||||
|
let config: ForgeConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[gitea]
|
||||||
|
base_url = "http://localhost:3000"
|
||||||
|
owner = "joe"
|
||||||
|
ssh_host = "localhost"
|
||||||
|
ssh_port = 2222
|
||||||
|
mode = "existing"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(config.repositories.is_empty());
|
||||||
|
assert_eq!(config.gitea.auth, ForgeAuthConfig::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ports_are_object_safe_for_adapters() {
|
||||||
|
struct FakeApi;
|
||||||
|
impl ForgeApi for FakeApi {
|
||||||
|
fn version(&self) -> Result<ForgeVersion, NotforgeError> {
|
||||||
|
Ok(ForgeVersion {
|
||||||
|
version: "1.0.0".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repo(
|
||||||
|
&self,
|
||||||
|
_owner: &str,
|
||||||
|
_name: &str,
|
||||||
|
) -> Result<Option<RemoteRepository>, NotforgeError> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_repo(
|
||||||
|
&self,
|
||||||
|
spec: &RepoSpec,
|
||||||
|
_auth: &ForgeAuth,
|
||||||
|
) -> Result<RemoteRepository, NotforgeError> {
|
||||||
|
Ok(RemoteRepository {
|
||||||
|
owner: spec.owner.clone(),
|
||||||
|
name: spec.name.clone(),
|
||||||
|
clone_url: "http://example/repo.git".to_string(),
|
||||||
|
ssh_url: "ssh://git@example/joe/repo.git".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeLifecycle;
|
||||||
|
impl ForgeLifecycle for FakeLifecycle {
|
||||||
|
fn ensure_available(
|
||||||
|
&self,
|
||||||
|
_config: &ForgeConfig,
|
||||||
|
) -> Result<LifecycleStatus, NotforgeError> {
|
||||||
|
Ok(LifecycleStatus::AlreadyAvailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeGit;
|
||||||
|
impl GitRemoteManager for FakeGit {
|
||||||
|
fn remote_url(
|
||||||
|
&self,
|
||||||
|
_repo: &LocalRepository,
|
||||||
|
_remote_name: &str,
|
||||||
|
) -> Result<Option<String>, NotforgeError> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_remote(
|
||||||
|
&self,
|
||||||
|
_repo: &LocalRepository,
|
||||||
|
_remote_name: &str,
|
||||||
|
_url: &str,
|
||||||
|
) -> Result<RemoteStatus, NotforgeError> {
|
||||||
|
Ok(RemoteStatus::Added)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(
|
||||||
|
&self,
|
||||||
|
_repo: &LocalRepository,
|
||||||
|
_remote_name: &str,
|
||||||
|
_branch: &str,
|
||||||
|
_set_upstream: bool,
|
||||||
|
) -> Result<PushStatus, NotforgeError> {
|
||||||
|
Ok(PushStatus::Pushed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeSecrets;
|
||||||
|
impl SecretResolverPort for FakeSecrets {
|
||||||
|
fn resolve(&self, _secret: &ForgeSecretRef) -> Result<String, NotforgeError> {
|
||||||
|
Ok("secret".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let api: &dyn ForgeApi = &FakeApi;
|
||||||
|
let lifecycle: &dyn ForgeLifecycle = &FakeLifecycle;
|
||||||
|
let git: &dyn GitRemoteManager = &FakeGit;
|
||||||
|
let secrets: &dyn SecretResolverPort = &FakeSecrets;
|
||||||
|
|
||||||
|
assert_eq!(api.version().unwrap().version, "1.0.0");
|
||||||
|
assert_eq!(
|
||||||
|
lifecycle
|
||||||
|
.ensure_available(&ForgeConfig {
|
||||||
|
gitea: notforge::config::GiteaConfig {
|
||||||
|
base_url: "http://localhost:3000".to_string(),
|
||||||
|
owner: "joe".to_string(),
|
||||||
|
ssh_host: "localhost".to_string(),
|
||||||
|
ssh_port: 2222,
|
||||||
|
mode: GiteaMode::Existing,
|
||||||
|
auth: ForgeAuthConfig::default(),
|
||||||
|
},
|
||||||
|
repositories: Vec::new(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
LifecycleStatus::AlreadyAvailable
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
git.remote_url(
|
||||||
|
&LocalRepository {
|
||||||
|
path: PathBuf::from(".")
|
||||||
|
},
|
||||||
|
"gitea"
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
secrets
|
||||||
|
.resolve(&ForgeSecretRef::Env {
|
||||||
|
key: "GITEA_TOKEN".to_string()
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
"secret"
|
||||||
|
);
|
||||||
|
}
|
||||||
203
crates/notforge/tests/gitea.rs
Normal file
203
crates/notforge/tests/gitea.rs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use notforge::config::RepoSpec;
|
||||||
|
use notforge::error::NotforgeError;
|
||||||
|
use notforge::gitea::{
|
||||||
|
GiteaHttpApi, GiteaHttpRequest, GiteaHttpResponse, GiteaTransport, HttpMethod,
|
||||||
|
};
|
||||||
|
use notforge::ports::{ForgeApi, ForgeAuth};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct RecordedRequest {
|
||||||
|
method: HttpMethod,
|
||||||
|
url: String,
|
||||||
|
authorization: Option<String>,
|
||||||
|
body: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FakeTransport {
|
||||||
|
requests: Arc<Mutex<Vec<RecordedRequest>>>,
|
||||||
|
responses: Arc<Mutex<Vec<Result<GiteaHttpResponse, NotforgeError>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeTransport {
|
||||||
|
fn new(responses: Vec<Result<GiteaHttpResponse, NotforgeError>>) -> Self {
|
||||||
|
Self {
|
||||||
|
requests: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
responses: Arc::new(Mutex::new(responses)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requests(&self) -> Vec<RecordedRequest> {
|
||||||
|
self.requests.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GiteaTransport for FakeTransport {
|
||||||
|
fn send(&self, request: GiteaHttpRequest) -> Result<GiteaHttpResponse, NotforgeError> {
|
||||||
|
self.requests.lock().unwrap().push(RecordedRequest {
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
authorization: request.authorization,
|
||||||
|
body: request.body,
|
||||||
|
});
|
||||||
|
self.responses.lock().unwrap().remove(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_gets_gitea_server_version() {
|
||||||
|
let transport = FakeTransport::new(vec![Ok(GiteaHttpResponse {
|
||||||
|
status: 200,
|
||||||
|
body: json!({ "version": "1.22.4" }),
|
||||||
|
})]);
|
||||||
|
let api = GiteaHttpApi::with_transport("http://gitea.local:3000/", transport.clone());
|
||||||
|
|
||||||
|
let version = api.version().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(version.version, "1.22.4");
|
||||||
|
let requests = transport.requests();
|
||||||
|
assert_eq!(requests.len(), 1);
|
||||||
|
assert_eq!(requests[0].method, HttpMethod::Get);
|
||||||
|
assert_eq!(requests[0].url, "http://gitea.local:3000/api/v1/version");
|
||||||
|
assert_eq!(requests[0].authorization, None);
|
||||||
|
assert_eq!(requests[0].body, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repo_maps_found_and_not_found_responses() {
|
||||||
|
let transport = FakeTransport::new(vec![
|
||||||
|
Ok(GiteaHttpResponse {
|
||||||
|
status: 200,
|
||||||
|
body: json!({
|
||||||
|
"owner": { "login": "joe" },
|
||||||
|
"name": "notfiles-config",
|
||||||
|
"clone_url": "http://gitea.local:3000/joe/notfiles-config.git",
|
||||||
|
"ssh_url": "ssh://git@gitea.local:2222/joe/notfiles-config.git"
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Ok(GiteaHttpResponse {
|
||||||
|
status: 404,
|
||||||
|
body: json!({ "message": "not found" }),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
let api = GiteaHttpApi::with_transport("http://gitea.local:3000", transport.clone());
|
||||||
|
|
||||||
|
let found = api.repo("joe", "notfiles-config").unwrap().unwrap();
|
||||||
|
let missing = api.repo("joe", "missing").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(found.owner, "joe");
|
||||||
|
assert_eq!(found.name, "notfiles-config");
|
||||||
|
assert_eq!(
|
||||||
|
found.clone_url,
|
||||||
|
"http://gitea.local:3000/joe/notfiles-config.git"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
found.ssh_url,
|
||||||
|
"ssh://git@gitea.local:2222/joe/notfiles-config.git"
|
||||||
|
);
|
||||||
|
assert_eq!(missing, None);
|
||||||
|
|
||||||
|
let requests = transport.requests();
|
||||||
|
assert_eq!(
|
||||||
|
requests[0].url,
|
||||||
|
"http://gitea.local:3000/api/v1/repos/joe/notfiles-config"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
requests[1].url,
|
||||||
|
"http://gitea.local:3000/api/v1/repos/joe/missing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_repo_posts_json_with_token_auth() {
|
||||||
|
let transport = FakeTransport::new(vec![Ok(GiteaHttpResponse {
|
||||||
|
status: 201,
|
||||||
|
body: json!({
|
||||||
|
"owner": { "login": "joe" },
|
||||||
|
"name": "notfiles-config",
|
||||||
|
"clone_url": "http://gitea.local:3000/joe/notfiles-config.git",
|
||||||
|
"ssh_url": "ssh://git@gitea.local:2222/joe/notfiles-config.git"
|
||||||
|
}),
|
||||||
|
})]);
|
||||||
|
let api = GiteaHttpApi::with_transport("http://gitea.local:3000", transport.clone());
|
||||||
|
let spec = RepoSpec {
|
||||||
|
owner: "joe".to_string(),
|
||||||
|
name: "notfiles-config".to_string(),
|
||||||
|
private: true,
|
||||||
|
description: Some("Personal config payload".to_string()),
|
||||||
|
remote_name: "gitea".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let repo = api
|
||||||
|
.create_repo(
|
||||||
|
&spec,
|
||||||
|
&ForgeAuth::Token {
|
||||||
|
token: "redacted".to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(repo.owner, "joe");
|
||||||
|
let requests = transport.requests();
|
||||||
|
assert_eq!(requests.len(), 1);
|
||||||
|
assert_eq!(requests[0].method, HttpMethod::Post);
|
||||||
|
assert_eq!(requests[0].url, "http://gitea.local:3000/api/v1/user/repos");
|
||||||
|
assert_eq!(
|
||||||
|
requests[0].authorization,
|
||||||
|
Some("token redacted".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
requests[0].body,
|
||||||
|
Some(json!({
|
||||||
|
"name": "notfiles-config",
|
||||||
|
"private": true,
|
||||||
|
"description": "Personal config payload"
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_repo_posts_json_with_basic_auth() {
|
||||||
|
let transport = FakeTransport::new(vec![Ok(GiteaHttpResponse {
|
||||||
|
status: 201,
|
||||||
|
body: json!({
|
||||||
|
"owner": { "login": "joe" },
|
||||||
|
"name": "notfiles-config",
|
||||||
|
"clone_url": "http://gitea.local:3000/joe/notfiles-config.git",
|
||||||
|
"ssh_url": "ssh://git@gitea.local:2222/joe/notfiles-config.git"
|
||||||
|
}),
|
||||||
|
})]);
|
||||||
|
let api = GiteaHttpApi::with_transport("http://gitea.local:3000", transport.clone());
|
||||||
|
let spec = RepoSpec {
|
||||||
|
owner: "joe".to_string(),
|
||||||
|
name: "notfiles-config".to_string(),
|
||||||
|
private: true,
|
||||||
|
description: None,
|
||||||
|
remote_name: "gitea".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
api.create_repo(
|
||||||
|
&spec,
|
||||||
|
&ForgeAuth::Basic {
|
||||||
|
username: "joe".to_string(),
|
||||||
|
password: "secret".to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let requests = transport.requests();
|
||||||
|
assert_eq!(
|
||||||
|
requests[0].authorization,
|
||||||
|
Some("Basic am9lOnNlY3JldA==".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
requests[0].body,
|
||||||
|
Some(json!({
|
||||||
|
"name": "notfiles-config",
|
||||||
|
"private": true
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
crates/notgraph/.DS_Store
vendored
BIN
crates/notgraph/.DS_Store
vendored
Binary file not shown.
@@ -17,9 +17,9 @@ tempfile = { workspace = true }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
cargo_metadata = "0.18"
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
cargo_metadata = "0.18"
|
|
||||||
syn = { version = "2", features = ["full", "visit"] }
|
syn = { version = "2", features = ["full", "visit"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
|
|||||||
BIN
crates/nothooks/.DS_Store
vendored
BIN
crates/nothooks/.DS_Store
vendored
Binary file not shown.
@@ -13,11 +13,11 @@ name = "nothooks"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
notcore = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
clap = { workspace = true }
|
||||||
|
notcore = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
clap = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ description = "Tailscale network presence management for notstrap"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
|
||||||
rpassword = { workspace = true }
|
rpassword = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
yubikey = { version = "0.8", optional = true }
|
yubikey = { version = "0.8", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
BIN
crates/notsecrets/.DS_Store
vendored
BIN
crates/notsecrets/.DS_Store
vendored
Binary file not shown.
@@ -5,26 +5,26 @@ edition = "2024"
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
notcore = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
base64 = "0.22"
|
||||||
rpassword = { workspace = true }
|
bech32 = "0.11"
|
||||||
|
chacha20poly1305 = "0.10"
|
||||||
|
curve25519-dalek = "4"
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
|
ed25519-dalek = "2"
|
||||||
|
hkdf = "0.12"
|
||||||
|
hmac = "0.12"
|
||||||
|
notcore = { workspace = true }
|
||||||
|
rand = "0.8"
|
||||||
|
rpassword = { workspace = true }
|
||||||
|
scrypt = "0.11"
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
sha2 = "0.10"
|
||||||
|
thiserror = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
||||||
ed25519-dalek = "2"
|
|
||||||
sha2 = "0.10"
|
|
||||||
hkdf = "0.12"
|
|
||||||
chacha20poly1305 = "0.10"
|
|
||||||
scrypt = "0.11"
|
|
||||||
bech32 = "0.11"
|
|
||||||
base64 = "0.22"
|
|
||||||
hmac = "0.12"
|
|
||||||
rand = "0.8"
|
|
||||||
zeroize = "1"
|
|
||||||
curve25519-dalek = "4"
|
|
||||||
yubikey = { version = "0.8", optional = true }
|
yubikey = { version = "0.8", optional = true }
|
||||||
|
zeroize = "1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
@@ -79,9 +79,11 @@ pub struct SecretsConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config(path: &Path) -> Result<SecretsConfig, SecretsError> {
|
pub fn load_config(path: &Path) -> Result<SecretsConfig, SecretsError> {
|
||||||
let content = std::fs::read_to_string(path)
|
notcore::config::load_toml_file(
|
||||||
.map_err(|e| SecretsError::Config(format!("cannot read {}: {e}", path.display())))?;
|
path,
|
||||||
toml::from_str(&content).map_err(|e| SecretsError::Config(format!("parse error: {e}")))
|
|path, err| SecretsError::Config(format!("cannot read {}: {err}", path.display())),
|
||||||
|
|_path, err| SecretsError::Config(format!("parse error: {err}")),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ name = "notstrap"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
clap = { workspace = true }
|
||||||
notcore = { workspace = true }
|
notcore = { workspace = true }
|
||||||
notfiles = { workspace = true }
|
notfiles = { workspace = true }
|
||||||
notsecrets = { workspace = true }
|
|
||||||
nothooks = { workspace = true }
|
nothooks = { workspace = true }
|
||||||
notnet = { workspace = true }
|
notnet = { workspace = true }
|
||||||
clap = { workspace = true }
|
notsecrets = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
|
||||||
which = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
which = { workspace = true }
|
||||||
|
|||||||
BIN
docs/superpowers/.DS_Store
vendored
BIN
docs/superpowers/.DS_Store
vendored
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
[defaults]
|
|
||||||
target = "~"
|
|
||||||
ignore = [".git", ".DS_Store", "README.md", "LICENSE*", "notfiles.toml", ".notfiles-state.toml", ".nothooks-state.toml", "Cargo.toml", "Cargo.lock", "crates", "docs", "tests", "target", "scripts", "mise.toml", "deny.toml", "rustqual.toml", ".github", ".gitea", ".envrc", ".sccignore", ".codex", ".superpowers", ".remember", ".worktrees", ".ctx", "HANDOFF.md", "AGENTS.md", "preflight.md"]
|
|
||||||
|
|
||||||
include = ["nushell"]
|
|
||||||
|
|
||||||
[packages.nushell]
|
|
||||||
BIN
nushell/.DS_Store
vendored
BIN
nushell/.DS_Store
vendored
Binary file not shown.
BIN
nushell/.config/.DS_Store
vendored
BIN
nushell/.config/.DS_Store
vendored
Binary file not shown.
BIN
nushell/.config/nushell/.DS_Store
vendored
BIN
nushell/.config/nushell/.DS_Store
vendored
Binary file not shown.
@@ -1,108 +0,0 @@
|
|||||||
# Aliases
|
|
||||||
|
|
||||||
# ── Shell ─────────────────────────────────────────────────────────────────────
|
|
||||||
alias rr = exec nu # reload nushell (re-reads config.nu and env.nu)
|
|
||||||
|
|
||||||
# ── Files ────────────────────────────────────────────────────────────────────
|
|
||||||
alias ll = ls -l
|
|
||||||
alias la = ls -la
|
|
||||||
|
|
||||||
# ── Editor ───────────────────────────────────────────────────────────────────
|
|
||||||
alias ide = zed .
|
|
||||||
alias ocm = opencode -m ollama/gpt-mbx
|
|
||||||
|
|
||||||
# ── mise ─────────────────────────────────────────────────────────────────────
|
|
||||||
alias m = mise
|
|
||||||
alias mr = mise run
|
|
||||||
alias mi = mise install
|
|
||||||
alias mt = mise tasks ls
|
|
||||||
|
|
||||||
# ── Rust / cargo ─────────────────────────────────────────────────────────────
|
|
||||||
alias x = cargo xtask
|
|
||||||
|
|
||||||
# ── Git (gitoxide) ───────────────────────────────────────────────────────────
|
|
||||||
# gix-supported: status, branch, log, fetch, clone, diff(objects), blame, worktree
|
|
||||||
# not yet in gix: add, commit, checkout/switch/restore, push, stash, rebase, merge
|
|
||||||
def --wrapped g [...args] { gix ...$args }
|
|
||||||
def gs [] { gix status }
|
|
||||||
def --wrapped gb [...args] { gix branch list ...$args }
|
|
||||||
def --wrapped ga [...args] { git add ...$args }
|
|
||||||
def --wrapped gc [...args] { git commit ...$args }
|
|
||||||
def --wrapped gco [...args] { git checkout ...$args }
|
|
||||||
def --wrapped gd [...args] { git diff ...$args } # gix diff = object-level only
|
|
||||||
def gl [] { git pull --ff-only }
|
|
||||||
def --wrapped gp [...args] { git push ...$args }
|
|
||||||
def --wrapped gpf [...args] { git push --force-with-lease ...$args }
|
|
||||||
def gitgood [] { git stash; git pull --rebase; git stash pop; git push }
|
|
||||||
|
|
||||||
# ── GitHub CLI ───────────────────────────────────────────────────────────────
|
|
||||||
alias ghst = gh auth status
|
|
||||||
alias ghrepo = gh repo view --web
|
|
||||||
alias ghpr = gh pr create
|
|
||||||
alias ghprv = gh pr view
|
|
||||||
alias ghprw = gh pr view --web
|
|
||||||
alias ghiss = gh issue list
|
|
||||||
alias ghrun = gh run list
|
|
||||||
|
|
||||||
# ── BAML ─────────────────────────────────────────────────────────────────────
|
|
||||||
alias baml = uvx --from baml-py baml-cli
|
|
||||||
|
|
||||||
# ── Python / uv ──────────────────────────────────────────────────────────────
|
|
||||||
alias pip = uv pip
|
|
||||||
alias pip3 = uv pip
|
|
||||||
alias py = uv run python
|
|
||||||
|
|
||||||
# ── JS / Bun ─────────────────────────────────────────────────────────────────
|
|
||||||
alias npm = bun
|
|
||||||
alias npx = bunx
|
|
||||||
alias pnpm = bun
|
|
||||||
alias yarn = bun
|
|
||||||
|
|
||||||
# ── Zerobrew ─────────────────────────────────────────────────────────────────
|
|
||||||
alias zbi = zb install
|
|
||||||
alias zbl = zb list
|
|
||||||
alias zbu = zb update
|
|
||||||
|
|
||||||
# ── notfiles ─────────────────────────────────────────────────────────────────
|
|
||||||
def --wrapped nf [...args] { notfiles --dir /Users/joe/dev/notfiles ...$args }
|
|
||||||
|
|
||||||
# ── Dotfiles ─────────────────────────────────────────────────────────────────
|
|
||||||
alias dotfiles = cd ~/dotfiles
|
|
||||||
def --env dotgs [] { cd ~/dotfiles; git status -sb }
|
|
||||||
def --env dotpull [] { cd ~/dotfiles; git pull --ff-only }
|
|
||||||
def --env dotpush [] { cd ~/dotfiles; git push }
|
|
||||||
def --env dotopen [] { cd ~/dotfiles; gh repo view --web }
|
|
||||||
|
|
||||||
# ── Docker / Colima ──────────────────────────────────────────────────────────
|
|
||||||
alias dps = docker ps
|
|
||||||
alias dpsa = docker ps -a
|
|
||||||
alias di = docker images
|
|
||||||
alias dstop = docker stop
|
|
||||||
alias drm = docker rm
|
|
||||||
alias drmi = docker rmi
|
|
||||||
alias drmif = docker rmi -f
|
|
||||||
alias colima-start = colima start --profile dev --cpu 4 --memory 6 --disk 60 --runtime docker
|
|
||||||
alias colima-stop = colima stop --profile dev
|
|
||||||
alias colima-status = colima status --profile dev
|
|
||||||
|
|
||||||
# ── Kubernetes ───────────────────────────────────────────────────────────────
|
|
||||||
alias kctx = kubectl config current-context
|
|
||||||
alias kpods = kubectl get pods -A
|
|
||||||
alias kmpods = kubectl --context=gke_toptal-maestro_us-east1_main-0 -n team-maestro get pods
|
|
||||||
alias kmlogs = kubectl --context=gke_toptal-maestro_us-east1_main-0 -n team-maestro logs
|
|
||||||
alias kmexec = kubectl --context=gke_toptal-maestro_us-east1_main-0 -n team-maestro exec -it
|
|
||||||
|
|
||||||
# ── Maestro ──────────────────────────────────────────────────────────────────
|
|
||||||
alias ms = maestro start
|
|
||||||
alias mst = maestro stop
|
|
||||||
alias ml = maestro list
|
|
||||||
alias mlogs = maestro logs
|
|
||||||
alias mwork = maestro work
|
|
||||||
alias mcfg = maestro config show
|
|
||||||
alias mpurge = maestro purge
|
|
||||||
alias mauth = maestro auth login
|
|
||||||
|
|
||||||
alias maestro-attach = docker exec -it -u vscode (docker ps --filter name=maestro-maestro-dev --format "{{.ID}}" | head -1) tmux -S /tmp/tmux-shared/maestro.sock -u attach-session
|
|
||||||
|
|
||||||
# ── Secrets / obfsck ─────────────────────────────────────────────────────────
|
|
||||||
alias obfs = pj secret redact
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# Redaction audit helpers — wraps scripts/redact-audit.sh and .logs/redact-audit.jsonl
|
|
||||||
|
|
||||||
# ── Internal ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _dotfiles [] { $env.HOME | path join "dotfiles" }
|
|
||||||
def _audit_log [] { $env.HOME | path join "dotfiles/.logs/redact-audit.jsonl" }
|
|
||||||
|
|
||||||
def _read_audit_log [] {
|
|
||||||
let log = _audit_log
|
|
||||||
if not ($log | path exists) { return [] }
|
|
||||||
open $log
|
|
||||||
| lines
|
|
||||||
| where { |l| ($l | str trim) != "" }
|
|
||||||
| each { |l| $l | from json }
|
|
||||||
| update ts { |r| $r.ts | into datetime }
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── audit-log ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Read the redaction audit log as a table.
|
|
||||||
# Filter by --tier, --file, --hook, or --since (e.g. "1day", "2hr", "30min")
|
|
||||||
def audit-log [
|
|
||||||
--tier: string # Filter to a specific tier (critical, high, medium, low)
|
|
||||||
--file: string # Filter by file path substring
|
|
||||||
--hook: string # Filter by hook name (pre-commit, manual, ...)
|
|
||||||
--since: string # Only show entries newer than this duration (e.g. 1day, 2hr)
|
|
||||||
] {
|
|
||||||
mut rows = _read_audit_log
|
|
||||||
|
|
||||||
if ($tier | is-not-empty) {
|
|
||||||
$rows = ($rows | where tier == $tier)
|
|
||||||
}
|
|
||||||
if ($file | is-not-empty) {
|
|
||||||
$rows = ($rows | where { |r| $r.file | str contains $file })
|
|
||||||
}
|
|
||||||
if ($hook | is-not-empty) {
|
|
||||||
$rows = ($rows | where hook == $hook)
|
|
||||||
}
|
|
||||||
if ($since | is-not-empty) {
|
|
||||||
let cutoff = (date now) - ($since | into duration)
|
|
||||||
$rows = ($rows | where ts > $cutoff)
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows | select ts hook commit file tier group label match_count
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── audit-summary ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Summarise audit log hits grouped by tier and group.
|
|
||||||
# Pass --since to limit to a recent window (e.g. "7day").
|
|
||||||
def audit-summary [
|
|
||||||
--since: string # Limit to entries newer than this duration (e.g. 7day)
|
|
||||||
] {
|
|
||||||
mut rows = _read_audit_log
|
|
||||||
|
|
||||||
if ($since | is-not-empty) {
|
|
||||||
let cutoff = (date now) - ($since | into duration)
|
|
||||||
$rows = ($rows | where ts > $cutoff)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($rows | is-empty) {
|
|
||||||
print "No audit entries found."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows
|
|
||||||
| group-by tier
|
|
||||||
| transpose tier entries
|
|
||||||
| each { |t|
|
|
||||||
let total = ($t.entries | get match_count | math sum)
|
|
||||||
let by_group = (
|
|
||||||
$t.entries
|
|
||||||
| group-by group
|
|
||||||
| transpose group rows
|
|
||||||
| each { |g| { group: $g.group, hits: ($g.rows | get match_count | math sum) } }
|
|
||||||
| sort-by hits -r
|
|
||||||
)
|
|
||||||
{ tier: $t.tier, total_hits: $total, breakdown: $by_group }
|
|
||||||
}
|
|
||||||
| sort-by total_hits -r
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── audit-scan ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Scan files through the redaction auditor and log findings.
|
|
||||||
# With no args, scans staged git files (same as pre-commit hook).
|
|
||||||
# Pass file paths to scan specific files.
|
|
||||||
def audit-scan [
|
|
||||||
...files: string # Files to scan (default: staged git files)
|
|
||||||
--verbose (-v) # Print findings to terminal as well as logging
|
|
||||||
] {
|
|
||||||
let script = (_dotfiles | path join "scripts/redact-audit.sh")
|
|
||||||
|
|
||||||
if not ($script | path exists) {
|
|
||||||
error make { msg: $"audit script not found: ($script)" }
|
|
||||||
}
|
|
||||||
|
|
||||||
mut args = ["--hook", "manual"]
|
|
||||||
|
|
||||||
if $verbose { $args = ($args | append "--verbose") }
|
|
||||||
|
|
||||||
if ($files | is-empty) {
|
|
||||||
$args = ($args | append "--staged")
|
|
||||||
} else {
|
|
||||||
$args = ($args | append $files)
|
|
||||||
}
|
|
||||||
|
|
||||||
^bash $script ...$args
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── audit-top ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Show the most frequently hit files in the audit log.
|
|
||||||
def audit-top [
|
|
||||||
--n: int = 10 # Number of results to show
|
|
||||||
--since: string # Limit to entries newer than this duration (e.g. 7day)
|
|
||||||
] {
|
|
||||||
mut rows = _read_audit_log
|
|
||||||
|
|
||||||
if ($since | is-not-empty) {
|
|
||||||
let cutoff = (date now) - ($since | into duration)
|
|
||||||
$rows = ($rows | where ts > $cutoff)
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows
|
|
||||||
| group-by file
|
|
||||||
| transpose file entries
|
|
||||||
| each { |f| { file: $f.file, hits: ($f.entries | get match_count | math sum), scans: ($f.entries | length) } }
|
|
||||||
| sort-by hits -r
|
|
||||||
| first $n
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# did-you-mean.nu — command_not_found hook
|
|
||||||
# Source: nushell/nu_scripts (nu-hooks/command_not_found)
|
|
||||||
# Suggests 3 closest commands from PATH when a command is not found.
|
|
||||||
|
|
||||||
export def hook [] {
|
|
||||||
{|cmd|
|
|
||||||
let commands_in_path = (
|
|
||||||
if ($nu.os-info.name == windows) {
|
|
||||||
$env.Path | each {|directory|
|
|
||||||
if ($directory | path exists) {
|
|
||||||
let cmd_exts = (
|
|
||||||
$env.PATHEXT | str downcase | split row ';' | str trim --char .
|
|
||||||
)
|
|
||||||
ls $directory
|
|
||||||
| get name
|
|
||||||
| path parse
|
|
||||||
| where {|it|
|
|
||||||
$cmd_exts | any {|ext| $ext == ($it.extension | str downcase)}
|
|
||||||
}
|
|
||||||
| get stem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$env.PATH | each {|directory|
|
|
||||||
if ($directory | path exists) {
|
|
||||||
ls $directory
|
|
||||||
| get name
|
|
||||||
| path parse
|
|
||||||
| update parent ""
|
|
||||||
| path join
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| flatten
|
|
||||||
| wrap cmd
|
|
||||||
)
|
|
||||||
|
|
||||||
let closest_commands = (
|
|
||||||
$commands_in_path
|
|
||||||
| insert distance {|it| $it.cmd | str distance $cmd}
|
|
||||||
| uniq
|
|
||||||
| sort-by distance
|
|
||||||
| get cmd
|
|
||||||
| first 3
|
|
||||||
)
|
|
||||||
|
|
||||||
let pretty_commands = (
|
|
||||||
$closest_commands | each {|cmd|
|
|
||||||
$" (ansi {fg: "default" attr: "di"})($cmd)(ansi reset)"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
$"\ndid you mean?\n($pretty_commands | str join "\n")"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
# Custom functions
|
|
||||||
|
|
||||||
# ── nu_libs ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Show all commands loaded from nu_libs, grouped by domain
|
|
||||||
def libs [
|
|
||||||
--filter(-f): string = "" # filter by name substring
|
|
||||||
] {
|
|
||||||
let nu_libs_dir = "/Users/joe/dev/nu_libs/lib"
|
|
||||||
|
|
||||||
# Build name→domain map by scanning export defs in each domain's files
|
|
||||||
let domain_map = (
|
|
||||||
ls $nu_libs_dir
|
|
||||||
| where type == "dir"
|
|
||||||
| get name
|
|
||||||
| each {|dir|
|
|
||||||
let domain = $dir | path basename
|
|
||||||
let names = (
|
|
||||||
glob $"($dir)/**/*.nu"
|
|
||||||
| each {|f|
|
|
||||||
open $f | lines
|
|
||||||
| where {|l| ($l | str starts-with "export def") or ($l | str starts-with "export alias")}
|
|
||||||
| each {|l|
|
|
||||||
# extract the command name: third or fourth word depending on flags
|
|
||||||
let parts = $l | split row " " | where {|p| ($p | str length) > 0}
|
|
||||||
# skip "export", "def"/"alias", and any --flags
|
|
||||||
$parts | skip 2 | where {|p| not ($p | str starts-with "-")} | first
|
|
||||||
}
|
|
||||||
| compact
|
|
||||||
}
|
|
||||||
| flatten
|
|
||||||
| compact
|
|
||||||
| each {|n| $n | str replace --all '"' "" | str replace --all "`" "" | str trim}
|
|
||||||
)
|
|
||||||
$names | each {|n| {name: $n, domain: $domain}}
|
|
||||||
}
|
|
||||||
| flatten
|
|
||||||
| uniq-by name
|
|
||||||
| reduce -f {} {|x, acc| $acc | upsert $x.name $x.domain}
|
|
||||||
)
|
|
||||||
|
|
||||||
let nu_libs_names = $domain_map | columns
|
|
||||||
|
|
||||||
help commands
|
|
||||||
| where command_type == "custom"
|
|
||||||
| where {|cmd| $cmd.name in $nu_libs_names}
|
|
||||||
| if ($filter | is-not-empty) { where name =~ $filter } else { $in }
|
|
||||||
| select name description
|
|
||||||
| each {|cmd|
|
|
||||||
$cmd | insert domain ($domain_map | get $cmd.name)
|
|
||||||
}
|
|
||||||
| sort-by domain name
|
|
||||||
| group-by domain
|
|
||||||
| transpose domain cmds
|
|
||||||
| each {|g|
|
|
||||||
print $"(ansi cyan_bold)── ($g.domain) ──(ansi reset)"
|
|
||||||
$g.cmds | select name description | print
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Files ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Quick directory listing sorted by size
|
|
||||||
def dirsize [] {
|
|
||||||
ls | select name size type | sort-by size -r
|
|
||||||
}
|
|
||||||
|
|
||||||
# Make a directory and cd into it
|
|
||||||
def --env mkcd [dir: string] {
|
|
||||||
mkdir $dir
|
|
||||||
cd $dir
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Dotfiles ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Run a mise task from the dotfiles repo
|
|
||||||
def dfr [...args: string] {
|
|
||||||
let root = ($env.HOME | path join "dotfiles")
|
|
||||||
^mise --cd $root run ...$args
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run a just recipe from the dotfiles repo
|
|
||||||
def dfj [...args: string] {
|
|
||||||
let root = ($env.HOME | path join "dotfiles")
|
|
||||||
^just --justfile $"($root)/Justfile" ...$args
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Kubernetes ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Tail logs across all namespaces (uses stern)
|
|
||||||
def klogs [pattern: string = "."] {
|
|
||||||
^stern $pattern -A
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Secrets / redaction ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Run a command and pipe its output through obfsck redact
|
|
||||||
def obfsrun [...args: string] {
|
|
||||||
let config = ($nu.home-path | path join "dotfiles/config/obfsck-secrets.yaml")
|
|
||||||
if (which obfsck | is-not-empty) {
|
|
||||||
^$args.0 ...($args | skip 1) | ^obfsck --config $config
|
|
||||||
} else {
|
|
||||||
^$args.0 ...($args | skip 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Docker / Colima ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _colima_ensure_running [] {
|
|
||||||
if (which colima | is-empty) { return }
|
|
||||||
let profile = if ("COLIMA_PROFILE" in $env) { $env.COLIMA_PROFILE } else { "dev" }
|
|
||||||
let running = (^colima status --profile $profile | complete | get exit_code) == 0
|
|
||||||
if not $running {
|
|
||||||
print $"[colima] Starting profile '($profile)' \(4 CPU, 6GB RAM, 60GB disk\)..."
|
|
||||||
^colima start --profile $profile --cpu 4 --memory 6 --disk 60 --runtime docker
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _colima_set_socket [] {
|
|
||||||
let dev_sock = ($env.HOME | path join ".colima/dev/docker.sock")
|
|
||||||
let default_sock = ($env.HOME | path join ".config/colima/default/docker.sock")
|
|
||||||
if ($dev_sock | path exists) {
|
|
||||||
$env.DOCKER_HOST = $"unix://($dev_sock)"
|
|
||||||
} else if ($default_sock | path exists) {
|
|
||||||
$env.DOCKER_HOST = $"unix://($default_sock)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def --wrapped docker [...args: string] {
|
|
||||||
_colima_ensure_running
|
|
||||||
_colima_set_socket
|
|
||||||
^docker ...$args
|
|
||||||
}
|
|
||||||
|
|
||||||
def --wrapped docker-compose [...args: string] {
|
|
||||||
_colima_ensure_running
|
|
||||||
_colima_set_socket
|
|
||||||
^docker-compose ...$args
|
|
||||||
}
|
|
||||||
|
|
||||||
def colima-restart [] {
|
|
||||||
colima-stop
|
|
||||||
colima-start
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── JS ───────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Real npm bypass (bun alias doesn't cover maestro-ui which needs real npm)
|
|
||||||
def mnpm [...args: string] {
|
|
||||||
^npm ...$args
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Secrets helpers ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Run a command with secrets injected from ~/.secrets via op run.
|
|
||||||
# Works around $HOME not being expanded by op CLI.
|
|
||||||
def oprun [...args: string] {
|
|
||||||
let secrets = ($env.HOME | path join ".secrets")
|
|
||||||
^op run --account=my.1password.com $"--env-file=($secrets)" -- ...$args
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Git helpers ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Push via gh credential helper
|
|
||||||
def _git_gh [...args: string] {
|
|
||||||
^git -c credential.helper= -c "credential.helper=!/opt/homebrew/bin/gh auth git-credential" ...$args
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# nu_libs — load all lib/* modules on shell startup
|
|
||||||
# Managed by notfiles (nushell package). Link with: notfiles link nushell
|
|
||||||
# lib/ai is excluded from lib/mod.nu (requires runtime deps) — loaded separately below.
|
|
||||||
# lib/extensions is excluded (requires external project repos) — load manually as needed.
|
|
||||||
|
|
||||||
const NU_LIBS = "/Users/joe/dev/nu_libs/lib/mod.nu"
|
|
||||||
const NU_LIBS_AI = "/Users/joe/dev/nu_libs/lib/ai/mod.nu"
|
|
||||||
|
|
||||||
use $NU_LIBS *
|
|
||||||
use $NU_LIBS_AI *
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# nuenv.nu — secure .env.nu loader (replaces direnv for Nu-native projects)
|
|
||||||
# Source: nushell/nu_scripts (nu-hooks/nuenv), adapted for local conventions.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# source autoload/nuenv.nu
|
|
||||||
# # In config.nu hooks section:
|
|
||||||
# $env.config.hooks.env_change.PWD = (... | append (nuenv-hook))
|
|
||||||
# # Then in any project dir:
|
|
||||||
# nuenv allow # approve .env.nu (sha256 allowlist)
|
|
||||||
# nuenv disallow # revoke
|
|
||||||
|
|
||||||
const NUENV_FILE = ($nu.cache-dir | path join 'nuenv' 'allowed.txt')
|
|
||||||
|
|
||||||
def get-allowed []: [ nothing -> list<string> ] {
|
|
||||||
if ($NUENV_FILE | path exists) {
|
|
||||||
open $NUENV_FILE | lines
|
|
||||||
} else {
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Returns the PWD-change hook record for use in env_change.PWD
|
|
||||||
export def nuenv-hook []: [ nothing -> record<condition: closure, code: string> ] {
|
|
||||||
{
|
|
||||||
condition: {|_, after| $after | path join '.env.nu' | path exists }
|
|
||||||
code: $"
|
|
||||||
let allowed = if \('($NUENV_FILE)' | path exists\) {
|
|
||||||
open ($NUENV_FILE) | lines
|
|
||||||
} else {
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
|
|
||||||
if \(open .env.nu | hash sha256\) not-in $allowed {
|
|
||||||
error make --unspanned {
|
|
||||||
msg: $'\(ansi purple\)\('.env.nu' | path expand\)\(ansi reset\) is not allowed',
|
|
||||||
help: $'please run \(ansi default_dimmed\)nuenv allow\(ansi reset\) first',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print $'[\(ansi yellow_bold\)nuenv\(ansi reset\)] loading \(ansi purple\)\('.env.nu' | path expand\)\(ansi reset\)'
|
|
||||||
source .env.nu
|
|
||||||
"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _log [msg: string, color: string] {
|
|
||||||
print $'[(ansi $color)nuenv(ansi reset)] (ansi purple)(".env.nu" | path expand)(ansi reset) ($msg)'
|
|
||||||
}
|
|
||||||
|
|
||||||
def check-env-file [] {
|
|
||||||
if not (".env.nu" | path exists) {
|
|
||||||
error make --unspanned {
|
|
||||||
msg: $"(ansi red_bold)env file not found(ansi reset)",
|
|
||||||
help: $"no (ansi yellow).env.nu(ansi reset) in (ansi purple)(pwd)(ansi reset)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Adds ./.env.nu to the allowlist
|
|
||||||
export def "nuenv allow" [] {
|
|
||||||
check-env-file
|
|
||||||
let allowed = get-allowed
|
|
||||||
let hash = open .env.nu | hash sha256
|
|
||||||
if $hash in $allowed {
|
|
||||||
_log "is already allowed" "yellow_bold"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mkdir ($nu.cache-dir | path join "nuenv")
|
|
||||||
$hash | $in + "\n" out>> $NUENV_FILE
|
|
||||||
_log "has been allowed" "green_bold"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Removes ./.env.nu from the allowlist
|
|
||||||
export def "nuenv disallow" [] {
|
|
||||||
check-env-file
|
|
||||||
let allowed = get-allowed
|
|
||||||
let hash = open .env.nu | hash sha256
|
|
||||||
if $hash not-in $allowed {
|
|
||||||
_log "is already disallowed" "yellow_bold"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$allowed | find --invert $hash | to text | save --force $NUENV_FILE
|
|
||||||
_log "has been disallowed" "green_bold"
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/usr/bin/env nu
|
|
||||||
|
|
||||||
# Session Guard: Validates config/env file hashes and expected env keys at shell startup
|
|
||||||
# Runs at every nushell startup to detect configuration drift.
|
|
||||||
|
|
||||||
def validate_session [] {
|
|
||||||
let baseline_path = $"($env.HOME)/.config/nushell/.session-baseline.json"
|
|
||||||
let files_to_check = [
|
|
||||||
$"($env.HOME)/.config/nushell/env.nu"
|
|
||||||
$"($env.HOME)/.config/nushell/config.nu"
|
|
||||||
$"($env.HOME)/dev/.envrc"
|
|
||||||
$"($env.HOME)/dev/.env"
|
|
||||||
$"($env.HOME)/.mise.toml"
|
|
||||||
]
|
|
||||||
let required_env_keys = [
|
|
||||||
"ANTHROPIC_API_KEY"
|
|
||||||
"OPENAI_API_KEY"
|
|
||||||
"GITHUB_TOKEN"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Check if baseline exists
|
|
||||||
if not ($baseline_path | path exists) {
|
|
||||||
print "[session-guard] No baseline found. Run session-baseline.nu to initialize."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load baseline
|
|
||||||
let baseline = (open --raw $baseline_path | from json)
|
|
||||||
|
|
||||||
# Check file hashes
|
|
||||||
for file_path in $files_to_check {
|
|
||||||
if ($file_path | path exists) {
|
|
||||||
let current_hash = (open --raw $file_path | hash sha256)
|
|
||||||
let file_name = ($file_path | path basename)
|
|
||||||
|
|
||||||
if ($baseline.files | get -i $file_name) != $current_hash {
|
|
||||||
print $"[session-guard] DRIFT: ($file_name) hash changed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check required env vars
|
|
||||||
for key in $required_env_keys {
|
|
||||||
if ($env | get -i $key) == null {
|
|
||||||
print $"[session-guard] MISSING ENV: ($key)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run validation (silent if all OK)
|
|
||||||
validate_session
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# Shell settings
|
|
||||||
|
|
||||||
$env.config.show_banner = false
|
|
||||||
$env.config.history.file_format = "sqlite"
|
|
||||||
$env.config.history.max_size = 100_000
|
|
||||||
$env.config.completions.external.enable = true
|
|
||||||
$env.config.edit_mode = "emacs" # or "vi"
|
|
||||||
|
|
||||||
# ── Colors ───────────────────────────────────────────────────────────────────
|
|
||||||
$env.config.color_config = {
|
|
||||||
separator: default
|
|
||||||
leading_trailing_space_bg: { attr: n }
|
|
||||||
header: green_bold
|
|
||||||
empty: blue
|
|
||||||
bool: light_cyan
|
|
||||||
int: default
|
|
||||||
filesize: cyan
|
|
||||||
duration: default
|
|
||||||
datetime: purple
|
|
||||||
range: default
|
|
||||||
float: default
|
|
||||||
string: default
|
|
||||||
nothing: default
|
|
||||||
binary: default
|
|
||||||
cell-path: default
|
|
||||||
row_index: green_bold
|
|
||||||
record: default
|
|
||||||
list: default
|
|
||||||
closure: green_bold
|
|
||||||
glob: cyan_bold
|
|
||||||
block: default
|
|
||||||
hints: dark_gray
|
|
||||||
search_result: { bg: red fg: default }
|
|
||||||
shape_binary: purple_bold
|
|
||||||
shape_block: blue_bold
|
|
||||||
shape_bool: light_cyan
|
|
||||||
shape_closure: green_bold
|
|
||||||
shape_custom: green
|
|
||||||
shape_datetime: cyan_bold
|
|
||||||
shape_directory: cyan
|
|
||||||
shape_external: cyan
|
|
||||||
shape_externalarg: green_bold
|
|
||||||
shape_external_resolved: light_yellow_bold
|
|
||||||
shape_filepath: cyan
|
|
||||||
shape_flag: blue_bold
|
|
||||||
shape_float: purple_bold
|
|
||||||
shape_glob_interpolation: cyan_bold
|
|
||||||
shape_globpattern: cyan_bold
|
|
||||||
shape_int: purple_bold
|
|
||||||
shape_internalcall: cyan_bold
|
|
||||||
shape_keyword: cyan_bold
|
|
||||||
shape_list: cyan_bold
|
|
||||||
shape_literal: blue
|
|
||||||
shape_match_pattern: green
|
|
||||||
shape_matching_brackets: { attr: u }
|
|
||||||
shape_nothing: light_cyan
|
|
||||||
shape_operator: yellow
|
|
||||||
shape_pipe: purple_bold
|
|
||||||
shape_range: yellow_bold
|
|
||||||
shape_record: cyan_bold
|
|
||||||
shape_redirection: purple_bold
|
|
||||||
shape_signature: green_bold
|
|
||||||
shape_string: green
|
|
||||||
shape_string_interpolation: cyan_bold
|
|
||||||
shape_table: blue_bold
|
|
||||||
shape_variable: purple
|
|
||||||
shape_vardecl: purple
|
|
||||||
shape_raw_string: light_purple
|
|
||||||
shape_garbage: { fg: default bg: red attr: b }
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# toolkit-hook.nu — auto-load toolkit.nu overlay on cd
|
|
||||||
# Source: nushell/nu_scripts (nu-hooks/toolkit)
|
|
||||||
#
|
|
||||||
# When you cd into a directory containing toolkit.nu, it loads as an overlay
|
|
||||||
# with the prefix "tk". Define project-local commands in toolkit.nu files.
|
|
||||||
|
|
||||||
export def toolkit-hook [
|
|
||||||
--name: string = "tk",
|
|
||||||
--color: string = "yellow_bold",
|
|
||||||
]: [ nothing -> record<condition: closure, code: string> ] {
|
|
||||||
{
|
|
||||||
condition: {|_, after| $after | path join 'toolkit.nu' | path exists }
|
|
||||||
code: $"
|
|
||||||
print $'[\(ansi ($color)\)toolkit\(ansi reset\)] loading \(ansi purple\)toolkit.nu\(ansi reset\) as overlay'
|
|
||||||
overlay use --prefix toolkit.nu as ($name)
|
|
||||||
"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# config.nu — main nushell configuration
|
|
||||||
# env.nu and autoload/ are sourced automatically by nushell before this file.
|
|
||||||
|
|
||||||
# ── Vendor/autoload seeding ───────────────────────────────────────────────────
|
|
||||||
# Generates tool init scripts into $nu.data-dir/vendor/autoload/ on every
|
|
||||||
# shell startup. Nushell auto-sources everything in that directory.
|
|
||||||
|
|
||||||
let vendor = $nu.data-dir | path join "vendor/autoload"
|
|
||||||
mkdir $vendor
|
|
||||||
|
|
||||||
# Ensure nix and mise are on PATH for vendor seeding — may already be set by env.nu
|
|
||||||
$env.PATH = ($env.PATH | prepend [
|
|
||||||
($env.HOME | path join ".nix-profile/bin")
|
|
||||||
($env.HOME | path join ".local/share/mise/shims")
|
|
||||||
] | uniq)
|
|
||||||
|
|
||||||
try { starship init nu | save -f ($vendor | path join "starship.nu") }
|
|
||||||
try { zoxide init nushell | save -f ($vendor | path join "zoxide.nu") }
|
|
||||||
try { ^mise activate nu | save -f ($vendor | path join "mise.nu") }
|
|
||||||
try { atuin init nu | save -f ($vendor | path join "atuin.nu") }
|
|
||||||
try { carapace _carapace nushell | save -f ($vendor | path join "carapace.nu") }
|
|
||||||
|
|
||||||
|
|
||||||
# ── User autoload ────────────────────────────────────────────────────────────
|
|
||||||
# Sourced explicitly so they are available immediately (vendor/autoload is
|
|
||||||
# sourced before config.nu, so copying there only takes effect next session).
|
|
||||||
source autoload/settings.nu
|
|
||||||
source autoload/aliases.nu
|
|
||||||
source autoload/functions.nu
|
|
||||||
source autoload/audit.nu
|
|
||||||
source autoload/nuenv.nu
|
|
||||||
source autoload/did-you-mean.nu
|
|
||||||
source autoload/toolkit-hook.nu
|
|
||||||
source autoload/nu_libs.nu
|
|
||||||
|
|
||||||
# ── Keybindings ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
$env.config.keybindings = ($env.config.keybindings? | default [] | append [
|
|
||||||
# Ctrl+F — fzf file picker, inserts selected path at cursor
|
|
||||||
{
|
|
||||||
name: fzf_file_picker
|
|
||||||
modifier: control
|
|
||||||
keycode: char_f
|
|
||||||
mode: [emacs, vi_insert]
|
|
||||||
event: {
|
|
||||||
send: executehostcommand
|
|
||||||
cmd: "commandline edit --insert (fd --type f | fzf | str trim)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
# ── Hooks ─────────────────────────────────────────────────────────────────────
|
|
||||||
# Prevent accumulation from repeated config.nu sourcing by tracking registration
|
|
||||||
|
|
||||||
let pwd_hooks = [
|
|
||||||
(nuenv-hook)
|
|
||||||
(toolkit-hook)
|
|
||||||
{|before, after|
|
|
||||||
let cache = ($env.HOME | path join ".cache/doob/status.json")
|
|
||||||
if not ($cache | path exists) { return }
|
|
||||||
let data = (open $cache | from json)
|
|
||||||
let overdue = ($data.overdue_total? | default 0)
|
|
||||||
if $overdue == 0 { return }
|
|
||||||
let repo = ($after | path basename)
|
|
||||||
let repo_count = ($data.overdue_by_repo? | default {} | get -o $repo | default 0)
|
|
||||||
if $repo_count > 0 {
|
|
||||||
print $"(ansi yellow)doob: ($repo_count) overdue in ($repo)(ansi reset)"
|
|
||||||
} else {
|
|
||||||
print $"(ansi dim)doob: ($overdue) overdue total(ansi reset)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Guard against accumulation from repeated sourcing (e.g., user runs 'source config.nu')
|
|
||||||
if ($env.NUSHELL_CONFIG_SOURCED? == "true") {
|
|
||||||
# Already sourced in this session, skip hook registration
|
|
||||||
} else {
|
|
||||||
$env.config.hooks.env_change.PWD = $pwd_hooks
|
|
||||||
$env.NUSHELL_CONFIG_SOURCED = "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
# display_output: expand table columns on wide terminals
|
|
||||||
$env.config.hooks.display_output = {||
|
|
||||||
if (term size).columns >= 100 { table -e } else { table }
|
|
||||||
}
|
|
||||||
|
|
||||||
# command_not_found: fuzzy-match 3 closest commands from PATH
|
|
||||||
$env.config.hooks.command_not_found = (use autoload/did-you-mean.nu; hook)
|
|
||||||
|
|
||||||
# go wrapper: install to $HOME/go/bin by default
|
|
||||||
def go [...args] { with-env { GOBIN: $"($env.HOME)/go/bin" } { ^go ...$args } }
|
|
||||||
|
|
||||||
# naptrace: run from source checkout so prompts/ is found
|
|
||||||
def --wrapped naptrace [...args] { cd ~/dev/naptrace; ^naptrace ...$args }
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
# Environment variables
|
|
||||||
|
|
||||||
# Guard: reset PWD to $HOME if inherited value is not an absolute path.
|
|
||||||
# if not ($env.PWD | str starts-with "/") {
|
|
||||||
# $env.PWD = $env.HOME
|
|
||||||
# }
|
|
||||||
|
|
||||||
# ── PATH ────────────────────────────────────────────────────────────────────
|
|
||||||
$env.PATH = (
|
|
||||||
$env.PATH
|
|
||||||
| prepend ($env.HOME | path join ".local/bin")
|
|
||||||
| prepend ($env.HOME | path join ".local/share/mise/shims")
|
|
||||||
| prepend ($env.HOME | path join ".bun/bin")
|
|
||||||
| prepend "/opt/zerobrew/bin"
|
|
||||||
| prepend ($env.HOME | path join ".zerobrew/bin")
|
|
||||||
| prepend ($env.HOME | path join ".nix-profile/bin")
|
|
||||||
| prepend "/nix/var/nix/profiles/default/bin"
|
|
||||||
| prepend ($env.HOME | path join ".cargo/bin")
|
|
||||||
| prepend "/opt/homebrew/bin"
|
|
||||||
| prepend "/opt/homebrew/sbin"
|
|
||||||
| prepend "/opt/homebrew/opt/openjdk/bin"
|
|
||||||
| prepend "/opt/homebrew/share/google-cloud-sdk/bin"
|
|
||||||
| uniq
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── ENV_CONVERSIONS ──────────────────────────────────────────────────────────
|
|
||||||
# Teach nushell to handle colon-separated vars from external tools
|
|
||||||
$env.ENV_CONVERSIONS = {
|
|
||||||
PATH: {
|
|
||||||
from_string: { |s| $s | split row (char esep) | path expand -n }
|
|
||||||
to_string: { |v| $v | str join (char esep) }
|
|
||||||
}
|
|
||||||
XDG_DATA_DIRS: {
|
|
||||||
from_string: { |s| $s | split row (char esep) }
|
|
||||||
to_string: { |v| $v | str join (char esep) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Core env ─────────────────────────────────────────────────────────────────
|
|
||||||
$env.SHLVL = 1
|
|
||||||
$env.EDITOR = "vim"
|
|
||||||
$env.VISUAL = "zed --wait"
|
|
||||||
$env.BUN_INSTALL = ($env.HOME | path join ".bun")
|
|
||||||
$env.JAVA_HOME = "/opt/homebrew/opt/openjdk"
|
|
||||||
$env.RTK_HOOK_AUDIT = "1"
|
|
||||||
|
|
||||||
# ── mise ─────────────────────────────────────────────────────────────────────
|
|
||||||
# Shims are on PATH above. Full activation (hooks, cd triggers) requires
|
|
||||||
# sourcing `mise activate nu` in config.nu — see settings.nu note.
|
|
||||||
$env.MISE_SHELL = "nu"
|
|
||||||
|
|
||||||
# ── Secrets / SOPS ──────────────────────────────────────────────────────────
|
|
||||||
let age_key = ($env.HOME | path join ".config/sops/age/keys.txt")
|
|
||||||
# Populate keys.txt from 1Password if it's missing or contains only a placeholder
|
|
||||||
if (which op | is-not-empty) {
|
|
||||||
let needs_refresh = (
|
|
||||||
not ($age_key | path exists) or
|
|
||||||
(open $age_key | str trim) == "AGE-SECRET-KEY-1TESTKEY"
|
|
||||||
)
|
|
||||||
if $needs_refresh {
|
|
||||||
try {
|
|
||||||
op item get 6meypnchchq3tsb32mdnzxtlia --fields notesPlain
|
|
||||||
| str replace --all '"' ''
|
|
||||||
| lines
|
|
||||||
| where { |l| $l | str starts-with "AGE-SECRET-KEY" }
|
|
||||||
| str join "\n"
|
|
||||||
| save --force $age_key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($age_key | path exists) {
|
|
||||||
$env.SOPS_AGE_KEY_FILE = $age_key
|
|
||||||
$env.MISE_SOPS_AGE_KEY_FILE = $age_key
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bootstrap secrets (dotenv format, no op:// refs)
|
|
||||||
let bootstrap_secrets = ($env.HOME | path join ".config/dev-bootstrap/secrets.env")
|
|
||||||
if ($bootstrap_secrets | path exists) {
|
|
||||||
open $bootstrap_secrets
|
|
||||||
| lines
|
|
||||||
| where { |l| not ($l | str starts-with "#") and ($l | str trim | str length) > 0 }
|
|
||||||
| each { |l| $l | parse "{key}={value}" | first }
|
|
||||||
| each { |kv| load-env {($kv.key): $kv.value} }
|
|
||||||
| ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cache dotenvx private key so nuenv .env.nu never re-prompts 1Password
|
|
||||||
if (which op | is-not-empty) and ($env.DOTENV_PRIVATE_KEY? | default "" | is-empty) {
|
|
||||||
try {
|
|
||||||
$env.DOTENV_PRIVATE_KEY = (
|
|
||||||
op read "op://Personal/nihl7o2bojy53zy4aqtr7txyqi/password"
|
|
||||||
--account=my.1password.com
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Colima / Docker ──────────────────────────────────────────────────────────
|
|
||||||
let colima_dev_sock = ($env.HOME | path join ".colima/dev/docker.sock")
|
|
||||||
let colima_default_sock = ($env.HOME | path join ".config/colima/default/docker.sock")
|
|
||||||
if ($colima_dev_sock | path exists) {
|
|
||||||
$env.DOCKER_HOST = $"unix://($colima_dev_sock)"
|
|
||||||
} else if ($colima_default_sock | path exists) {
|
|
||||||
$env.DOCKER_HOST = $"unix://($colima_default_sock)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Maestro ──────────────────────────────────────────────────────────────────
|
|
||||||
$env.MAESTRO_API_URL = "https://api.maestro-staging.toptal.net"
|
|
||||||
$env.MAESTRO_RESOURCE_PROFILE = "development"
|
|
||||||
|
|
||||||
|
|
||||||
# ── Homebrew ──────────────────────────────────────────────────────────────────
|
|
||||||
$env.HOMEBREW_PREFIX = "/opt/homebrew"
|
|
||||||
$env.HOMEBREW_CELLAR = "/opt/homebrew/Cellar"
|
|
||||||
$env.HOMEBREW_REPOSITORY = "/opt/homebrew"
|
|
||||||
|
|
||||||
# ── Handon banner ────────────────────────────────────────────────────────────
|
|
||||||
# Show top handoff items for current repo on shell start.
|
|
||||||
if (which handoff-detect | is-not-empty) {
|
|
||||||
let _result = (do { run-external "handoff-detect" $env.PWD } | complete)
|
|
||||||
if $_result.exit_code == 0 and ($_result.stdout | str trim | is-not-empty) {
|
|
||||||
print $"(ansi cyan)--- handoff ---"
|
|
||||||
print $_result.stdout
|
|
||||||
print (ansi reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── SSH agent ────────────────────────────────────────────────────────────────
|
|
||||||
# Prefer gpg-agent (YubiKey/OpenPGP), fall back to native macOS launchd agent.
|
|
||||||
$env.SSH_AUTH_SOCK = (
|
|
||||||
[
|
|
||||||
(^gpgconf --list-dirs agent-ssh-socket | str trim)
|
|
||||||
(glob "/var/run/com.apple.launchd.*/Listeners" | first | default "")
|
|
||||||
]
|
|
||||||
| where { |it| $it | path exists }
|
|
||||||
| first
|
|
||||||
| default ""
|
|
||||||
)
|
|
||||||
$env.PATH = ($env.PATH | split row (char esep) | where { $in != "/Users/joe/Library/Application Support/carapace/bin" } | prepend "/Users/joe/Library/Application Support/carapace/bin")
|
|
||||||
|
|
||||||
def --env get-env [name] { $env | get $name }
|
|
||||||
def --env set-env [name, value] { load-env { $name: $value } }
|
|
||||||
def --env unset-env [name] { hide-env $name }
|
|
||||||
|
|
||||||
let carapace_completer = {|spans|
|
|
||||||
load-env {
|
|
||||||
CARAPACE_SHELL_BUILTINS: (help commands | where category != "" | get name | each { split row " " | first } | uniq | str join "\n")
|
|
||||||
CARAPACE_SHELL_FUNCTIONS: (help commands | where category == "" | get name | each { split row " " | first } | uniq | str join "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
# if the current command is an alias, get it's expansion
|
|
||||||
let expanded_alias = (scope aliases | where name == $spans.0 | $in.0?.expansion?)
|
|
||||||
|
|
||||||
# overwrite
|
|
||||||
let spans = (if $expanded_alias != null {
|
|
||||||
# put the first word of the expanded alias first in the span
|
|
||||||
$spans | skip 1 | prepend ($expanded_alias | split row " " | take 1)
|
|
||||||
} else {
|
|
||||||
$spans | skip 1 | prepend ($spans.0)
|
|
||||||
})
|
|
||||||
|
|
||||||
carapace $spans.0 nushell ...$spans
|
|
||||||
| from json
|
|
||||||
}
|
|
||||||
|
|
||||||
mut current = (($env | default {} config).config | default {} completions)
|
|
||||||
$current.completions = ($current.completions | default {} external)
|
|
||||||
$current.completions.external = ($current.completions.external
|
|
||||||
| default true enable
|
|
||||||
# backwards compatible workaround for default, see nushell #15654
|
|
||||||
| upsert completer { if $in == null { $carapace_completer } else { $in } })
|
|
||||||
|
|
||||||
$env.config = $current
|
|
||||||
|
|
||||||
@@ -14,11 +14,11 @@ name = "cross_crate"
|
|||||||
path = "tests/cross_crate.rs"
|
path = "tests/cross_crate.rs"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
notstrap = { path = "../../crates/notstrap" }
|
anyhow = { workspace = true }
|
||||||
|
assert_fs = { workspace = true }
|
||||||
|
notcore = { path = "../../crates/notcore" }
|
||||||
notfiles = { path = "../../crates/notfiles" }
|
notfiles = { path = "../../crates/notfiles" }
|
||||||
nothooks = { path = "../../crates/nothooks" }
|
nothooks = { path = "../../crates/nothooks" }
|
||||||
notsecrets = { path = "../../crates/notsecrets" }
|
notsecrets = { path = "../../crates/notsecrets" }
|
||||||
notcore = { path = "../../crates/notcore" }
|
notstrap = { path = "../../crates/notstrap" }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
assert_fs = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user