docs: update HANDOFF.md — age-native redesign progress (tasks 1–7 complete)

This commit is contained in:
Joseph O'Brien
2026-04-01 20:58:09 -04:00
parent 545a45b3d3
commit b4cba74ea3
24 changed files with 1069 additions and 99 deletions

562
Cargo.lock generated
View File

@@ -2,6 +2,16 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -97,12 +107,39 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bech32"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.12.1"
@@ -135,6 +172,30 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -146,6 +207,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]]
name = "clap"
version = "4.6.0"
@@ -192,12 +264,27 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@@ -223,12 +310,73 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
[[package]]
name = "dirs"
version = "6.0.0"
@@ -256,6 +404,30 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9"
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "either"
version = "1.15.0"
@@ -290,6 +462,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -302,6 +480,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -371,6 +559,24 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -429,6 +635,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "integration"
version = "0.1.0"
@@ -465,6 +680,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -477,6 +701,12 @@ version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.15"
@@ -548,12 +778,24 @@ name = "notsecrets"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"bech32",
"chacha20poly1305",
"curve25519-dalek",
"dirs",
"ed25519-dalek",
"hkdf",
"hmac",
"notcore",
"rand",
"rpassword",
"rsa",
"scrypt",
"sha2",
"tempfile",
"thiserror",
"which",
"x25519-dalek",
"zeroize",
]
[[package]]
@@ -571,6 +813,42 @@ dependencies = [
"which",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand",
"smallvec",
"zeroize",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -578,6 +856,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@@ -592,12 +871,89 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "predicates"
version = "3.1.4"
@@ -659,6 +1015,36 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "redox_users"
version = "0.5.2"
@@ -698,6 +1084,27 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"sha2",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rtoolbox"
version = "0.0.3"
@@ -708,6 +1115,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
@@ -727,6 +1143,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "same-file"
version = "1.0.6"
@@ -736,6 +1161,18 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"password-hash",
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "semver"
version = "1.0.27"
@@ -794,18 +1231,67 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
@@ -897,6 +1383,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -909,12 +1401,28 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -1302,6 +1810,58 @@ dependencies = [
"wasmparser",
]
[[package]]
name = "x25519-dalek"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek",
"rand_core",
"serde",
"zeroize",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"

View File

@@ -1,9 +1,11 @@
# HANDOFF.md
State of the `notfiles` repo as of 2026-03-31.
State of the `notfiles` repo as of 2026-04-01.
## What Was Done
### Previous session (2026-03-31)
Restructured from a flat `src/` layout into a Cargo workspace of 5 crates:
- `notcore` — shared library (types, config, paths, errors)
- `notfiles` — dotfiles linker, migrated from `src/`
@@ -11,27 +13,61 @@ Restructured from a flat `src/` layout into a Cargo workspace of 5 crates:
- `nothooks` — Nushell hook runner with phase/state tracking
- `notstrap` — new-machine bootstrap orchestrator
39 tests passing, 0 clippy warnings. Committed to `main`, not yet pushed to `gitea/main`.
### This session (2026-04-01)
## Pending Issues
Began age-native redesign of `notsecrets` — replacing sops shell-out and Bitwarden CLI dependency with a self-contained age encryption/decryption library implemented in pure Rust (modeled after rage, no dependency on it).
**Spec and plan written:**
- `docs/superpowers/specs/2026-04-01-notsecrets-age-redesign.md`
- `docs/superpowers/plans/2026-04-01-notsecrets-age-redesign.md`
**Implementation progress (Tasks 17 of 12 complete):**
| Task | Status | Commit |
|------|--------|--------|
| 1: Scaffold (Cargo.toml, error.rs, domain types, legacy shim) | ✅ | `34d9e3d` |
| 2: format.rs (age wire format parser/serializer) | ✅ | `2476daa` |
| 3: X25519 identity + recipient | ✅ | `6202c5d` |
| 4: Scrypt identity + recipient | ✅ | `b899198` |
| 5: SSH Ed25519 identity + recipient | ✅ | `566501c` |
| 6: SSH RSA identity + recipient | ✅ | `a0fe6f7` |
| 7: EncryptedIdentity (stub, real impl needs Decryptor) | ✅ | `545a45b` |
| 8: Encryptor | ⏳ not started | |
| 9: Decryptor + wire EncryptedIdentity | ⏳ not started | |
| 10: sources/ migration (IdentitySource trait) | ⏳ not started | |
| 11: resolve_identities() public API | ⏳ not started | |
| 12: notstrap migration + cargo check --workspace | ⏳ not started | |
**Current state:**
- `notsecrets` compiles and passes all existing tests
- Legacy API (`AgeKeySource`, `resolve_age_key`, `install_age_key`, `decrypt_sops`) preserved in `_legacy.rs``notstrap` still works
- `EncryptedIdentity::unwrap_file_key` is a stub returning an error until `Decryptor` is wired in Task 9
## Resuming
To continue the age-native redesign, pick up at Task 8 (Encryptor). The plan is at:
`docs/superpowers/plans/2026-04-01-notsecrets-age-redesign.md`
Tasks 812 need to be executed in order (each depends on the previous). Use the subagent-driven-development skill to dispatch implementer subagents per task.
Key context for resuming:
- `EncryptedIdentity` stub in `src/identities/encrypted.rs` must have its `unwrap_file_key` body replaced after `Decryptor` exists (end of Task 9)
- The `EnvInjector` type in `notstrap` changes from `Box<dyn Fn>` to `Box<dyn FnOnce>` in Task 12
- `_legacy.rs` is deleted in Task 10
## Pending Issues (carried from previous session)
### 1. Push to remote
```bash
git push
```
8 commits ahead of `gitea/main`. Nothing blocking this.
Several commits ahead of `gitea/main`. Push when ready.
### 2. notstrap has an empty lib.rs
`crates/notstrap/src/lib.rs` is empty — created as a stub. `notstrap` is binary-only so this is harmless, but it's dead weight. Either delete `[lib]` from `notstrap/Cargo.toml` (if there is one) or remove the file.
`crates/notstrap/src/lib.rs` is empty — created as a stub. Either delete `[lib]` from `notstrap/Cargo.toml` or remove the file.
### 3. notstrap config not documented
`notstrap.toml` format is defined inline in `notstrap/src/main.rs` via serde structs but never written down. A sample `notstrap.toml` should be created and documented.
`notstrap.toml` format is defined inline via serde structs but never written down. A sample config would help.
### 4. notsecrets has no binary
`notsecrets` is library-only. A small `notsecrets get-key` CLI binary could be useful for manual debugging/testing of the key retrieval chain, but not blocking.
### 4. Integration test coverage for nothooks assumes nu in PATH
`nothooks` integration tests call `nu` directly. CI needs `nu` installed or tests need `#[ignore]` + feature flag.
### 5. Integration test coverage for nothooks assumes nu in PATH
`nothooks` integration tests call `nu` directly. If `nu` is not on PATH (e.g. on CI), the success/failure tests will fail for the wrong reason. CI should either install `nu` or the tests should be marked `#[ignore]` with a feature flag.
### 6. notstrap sops_file path is always joined to dotfiles_dir
If `sops_file` in `notstrap.toml` is an absolute path, the join will silently override the base. This is a minor footgun in `repo.clone_if_missing` usage — low priority.
### 5. notstrap sops_file path footgun
If `sops_file` in `notstrap.toml` is an absolute path, the `join` will silently override the base. Low priority.

View File

@@ -77,10 +77,12 @@ impl Config {
if !config_path.exists() {
return Ok(Config::default());
}
let content = std::fs::read_to_string(&config_path)
.map_err(|e| NotfilesError::Config(format!("reading {}: {e}", config_path.display())))?;
let config: Config = toml::from_str(&content)
.map_err(|e| NotfilesError::Config(format!("parsing {}: {e}", config_path.display())))?;
let content = std::fs::read_to_string(&config_path).map_err(|e| {
NotfilesError::Config(format!("reading {}: {e}", config_path.display()))
})?;
let config: Config = toml::from_str(&content).map_err(|e| {
NotfilesError::Config(format!("parsing {}: {e}", config_path.display()))
})?;
Ok(config)
}

View File

@@ -48,7 +48,10 @@ pub struct Report {
impl Report {
pub fn add(&mut self, name: impl Into<String>, status: StepStatus) {
self.steps.push(Step { name: name.into(), status });
self.steps.push(Step {
name: name.into(),
status,
});
}
pub fn print(&self) {
@@ -67,7 +70,9 @@ impl Report {
}
pub fn has_failures(&self) -> bool {
self.steps.iter().any(|s| matches!(s.status, StepStatus::Failed(_)))
self.steps
.iter()
.any(|s| matches!(s.status, StepStatus::Failed(_)))
}
}

View File

@@ -14,7 +14,9 @@ impl IgnoreMatcher {
// Match the pattern as a filename component and also as a path suffix.
let glob = Glob::new(pattern)
.or_else(|_| Glob::new(&format!("**/{pattern}")))
.map_err(|e| NotfilesError::Other(format!("invalid ignore pattern '{pattern}': {e}")))?;
.map_err(|e| {
NotfilesError::Other(format!("invalid ignore pattern '{pattern}': {e}"))
})?;
builder.add(glob);
// Also add a recursive variant so "foo" matches "a/foo" etc.
#[allow(clippy::collapsible_if)]

View File

@@ -10,11 +10,7 @@ use std::path::Path;
pub use linker::{LinkOptions, State};
pub use package::resolve_packages;
pub fn link(
dotfiles_dir: &Path,
packages: &[String],
opts: &LinkOptions,
) -> Result<State> {
pub fn link(dotfiles_dir: &Path, packages: &[String], opts: &LinkOptions) -> Result<State> {
let config = notcore::Config::load(dotfiles_dir)?;
let mut state = State::load(dotfiles_dir)?;
let pkgs = resolve_packages(dotfiles_dir, packages)?;

View File

@@ -4,8 +4,8 @@ use std::path::{Path, PathBuf};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use notcore::{Config, Method, NotfilesError, expand_tilde};
use crate::package::collect_files;
use notcore::{Config, Method, NotfilesError, expand_tilde};
const STATE_FILE: &str = ".notfiles-state.toml";
@@ -46,7 +46,10 @@ impl State {
}
pub fn entries_for_package(&self, package: &str) -> Vec<&StateEntry> {
self.entries.iter().filter(|e| e.package == package).collect()
self.entries
.iter()
.filter(|e| e.package == package)
.collect()
}
pub fn remove_package(&mut self, package: &str) {
@@ -55,7 +58,8 @@ impl State {
pub fn add_entry(&mut self, entry: StateEntry) {
// Remove existing entry for same source+target, then add new
self.entries.retain(|e| !(e.source == entry.source && e.target == entry.target));
self.entries
.retain(|e| !(e.source == entry.source && e.target == entry.target));
self.entries.push(entry);
}
}
@@ -113,10 +117,18 @@ pub fn link_package(
if !opts.no_backup {
let backup = backup_path(&target);
if opts.dry_run {
println!(" \x1b[33mwould backup\x1b[0m {} -> {}", target.display(), backup.display());
println!(
" \x1b[33mwould backup\x1b[0m {} -> {}",
target.display(),
backup.display()
);
} else {
if opts.verbose {
println!(" \x1b[33mbackup\x1b[0m {} -> {}", target.display(), backup.display());
println!(
" \x1b[33mbackup\x1b[0m {} -> {}",
target.display(),
backup.display()
);
}
fs::rename(&target, &backup)?;
}
@@ -147,7 +159,10 @@ pub fn link_package(
};
if opts.dry_run {
println!(" \x1b[36mwould {action_word}\x1b[0m {source_display} -> {}", target.display());
println!(
" \x1b[36mwould {action_word}\x1b[0m {source_display} -> {}",
target.display()
);
} else {
match method {
Method::Symlink => {
@@ -161,7 +176,10 @@ pub fn link_package(
}
}
if opts.verbose {
println!(" \x1b[32m{action_word}\x1b[0m {source_display} -> {}", target.display());
println!(
" \x1b[32m{action_word}\x1b[0m {source_display} -> {}",
target.display()
);
}
state.add_entry(StateEntry {
@@ -183,7 +201,11 @@ pub fn unlink_package(
package: &str,
opts: &LinkOptions,
) -> Result<(), NotfilesError> {
let entries: Vec<StateEntry> = state.entries_for_package(package).into_iter().cloned().collect();
let entries: Vec<StateEntry> = state
.entries_for_package(package)
.into_iter()
.cloned()
.collect();
if entries.is_empty() {
if opts.verbose {
@@ -278,7 +300,10 @@ fn cleanup_empty_parents(path: &Path) {
if Some(parent.to_path_buf()) == dirs::home_dir() || parent == Path::new("/") {
break;
}
if fs::read_dir(parent).map(|mut d| d.next().is_none()).unwrap_or(false) {
if fs::read_dir(parent)
.map(|mut d| d.next().is_none())
.unwrap_or(false)
{
let _ = fs::remove_dir(parent);
dir = parent.parent();
} else {

View File

@@ -2,11 +2,11 @@ use anyhow::{Context, Result};
use clap::Parser;
use std::fs;
use notcore::Config;
use notfiles::cli::{Cli, Command};
use notfiles::linker::{LinkOptions, State};
use notfiles::package::resolve_packages;
use notfiles::{linker, status};
use notcore::Config;
fn main() -> Result<()> {
let cli = Cli::parse();
@@ -46,7 +46,10 @@ fn main() -> Result<()> {
if !cli.dry_run {
state.save(&dotfiles_dir)?;
let count: usize = pkgs.iter().map(|p| state.entries_for_package(p).len()).sum();
let count: usize = pkgs
.iter()
.map(|p| state.entries_for_package(p).len())
.sum();
println!(
"\x1b[32mLinked {count} file{} across {} package{}.\x1b[0m",
if count == 1 { "" } else { "s" },

View File

@@ -1,8 +1,8 @@
use std::fs;
use std::path::{Path, PathBuf};
use notcore::{Config, NotfilesError};
use crate::ignore::IgnoreMatcher;
use notcore::{Config, NotfilesError};
/// Discover available packages (subdirectories of the dotfiles dir).
pub fn discover_packages(dotfiles_dir: &Path) -> Result<Vec<String>, NotfilesError> {

View File

@@ -1,9 +1,9 @@
use std::fs;
use std::path::{Path, PathBuf};
use notcore::{Config, Method, expand_tilde};
use crate::linker::State;
use crate::package::collect_files;
use notcore::{Config, Method, expand_tilde};
#[derive(Debug, PartialEq)]
pub enum FileStatus {
@@ -69,8 +69,7 @@ pub fn package_status(
}
Method::Copy => {
let has_state = state.entries.iter().any(|e| {
e.package == package
&& e.target == target.to_string_lossy().as_ref()
e.package == package && e.target == target.to_string_lossy().as_ref()
});
if has_state && target.exists() {
FileStatus::Copied
@@ -122,6 +121,11 @@ pub fn print_status(package: &str, entries: &[StatusEntry]) {
}
println!(" \x1b[1m{package}\x1b[0m:");
for entry in entries {
println!(" {} {} -> {}", entry.status, entry.source_display, entry.target.display());
println!(
" {} {} -> {}",
entry.status,
entry.source_display,
entry.target.display()
);
}
}

View File

@@ -94,7 +94,13 @@ fn test_link_and_status() {
let gitconfig = target.join(".gitconfig");
assert!(gitconfig.exists());
assert!(gitconfig.symlink_metadata().unwrap().file_type().is_symlink());
assert!(
gitconfig
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink()
);
// State file should exist
assert!(dotfiles.join(".notfiles-state.toml").exists());
@@ -199,7 +205,14 @@ fn test_force_with_backup() {
assert!(ok);
// The link should now exist
assert!(target.join(".gitconfig").symlink_metadata().unwrap().file_type().is_symlink());
assert!(
target
.join(".gitconfig")
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink()
);
// A backup should exist
let backups: Vec<_> = fs::read_dir(&target)
@@ -232,11 +245,7 @@ fn test_force_no_backup() {
let backups: Vec<_> = fs::read_dir(&target)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.contains("notfiles-backup")
})
.filter(|e| e.file_name().to_string_lossy().contains("notfiles-backup"))
.collect();
assert_eq!(backups.len(), 0);
}
@@ -272,8 +281,17 @@ method = "copy"
let ssh_config = target.join(".ssh/config");
assert!(ssh_config.exists());
// Should NOT be a symlink
assert!(!ssh_config.symlink_metadata().unwrap().file_type().is_symlink());
assert_eq!(fs::read_to_string(&ssh_config).unwrap(), "Host *\n AddKeysToAgent yes");
assert!(
!ssh_config
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink()
);
assert_eq!(
fs::read_to_string(&ssh_config).unwrap(),
"Host *\n AddKeysToAgent yes"
);
// Status should show "copied"
let (stdout, _, _) = run(&dotfiles, &["status", "ssh"]);

121
crates/nothooks/README.md Normal file
View File

@@ -0,0 +1,121 @@
# nothooks
Nushell hook runner for the notfiles workspace. Executes `.nu` scripts in two
distinct lifecycle phases — **dot** (always) and **setup** (once) — with
state persistence so setup hooks are not re-run across invocations.
## Concepts
### HookPhase
Each hook belongs to exactly one phase:
| Phase | When it runs | Typical use |
|-------|-------------|-------------|
| `dot` | Every time nothooks is invoked | Apply shell config, set env vars, refresh symlinks |
| `setup` | Once per machine (tracked in state) | Install packages, create dirs, first-time configuration |
### HookSpec
A hook is described by three fields (defined in `notcore`):
```toml
[[hooks]]
name = "install-starship"
script = "hooks/setup/install-starship.nu"
phase = "setup"
[[hooks]]
name = "set-default-shell"
script = "hooks/dot/shell.nu"
phase = "dot"
```
### State tracking
Setup hooks are tracked in `.nothooks-state.toml` in the state directory
(typically the dotfiles root). Once a setup hook completes successfully its
name is recorded and it will be **skipped** on all future runs.
```toml
# .nothooks-state.toml (auto-generated, do not edit manually)
completed_setup_hooks = ["install-starship", "configure-git"]
```
Dot hooks are **never** recorded in state — they run unconditionally on every
invocation.
### --force re-run
To re-run setup hooks (e.g. after a machine rebuild or to apply changes),
construct the runner with `HookRunner::with_force`. This bypasses the state
check so all setup hooks execute regardless of prior completion.
The notstrap CLI exposes this as `--force`.
## Hook script lifecycle
```
notstrap / nothooks invoked
├── dot phase
│ └── for each hook where phase == "dot"
│ └── nu <script> # always runs
└── setup phase
└── for each hook where phase == "setup"
├── state.is_done(name)?
│ ├── yes (and !force) → skip
│ └── no (or force) → nu <script>
│ └── success → state.mark_done(name)
└── report collected
```
## Writing a hook script
Hook scripts are plain Nushell (`.nu`) files. They receive no arguments and
communicate success/failure via exit code (0 = success, non-zero = failure).
```nushell
#!/usr/bin/env nu
# hooks/setup/install-starship.nu
# Installs starship prompt if not already present.
if (which starship | is-empty) {
print "Installing starship..."
sh -c "curl -sS https://starship.rs/install.sh | sh -s -- -y"
} else {
print "starship already installed, skipping"
}
```
```nushell
#!/usr/bin/env nu
# hooks/dot/xdg-dirs.nu
# Ensure XDG base directories exist on every login.
mkdir ~/.config ~/.cache ~/.local/share ~/.local/state
```
## Usage from Rust
```rust
use nothooks::{HookRunner, run_phase};
use notcore::{HookPhase, HookSpec};
use std::path::PathBuf;
let hooks: Vec<HookSpec> = /* load from notfiles.toml */;
let state_dir = PathBuf::from("/path/to/dotfiles");
// Normal run
let runner = HookRunner::new(state_dir.clone());
// Force re-run of setup hooks
let runner = HookRunner::with_force(state_dir.clone());
let dot_report = run_phase(&hooks, &HookPhase::Dot, &runner);
let setup_report = run_phase(&hooks, &HookPhase::Setup, &runner);
```
`run_phase` returns a `notcore::Report` containing a `StepStatus` (`Ok`,
`Skipped`, or `Failed(msg)`) for each hook in that phase.

View File

@@ -1,8 +1,8 @@
pub mod runner;
pub mod state;
pub use runner::HookRunner;
use notcore::{HookPhase, HookSpec, Report, StepStatus};
pub use runner::HookRunner;
#[derive(Debug, PartialEq)]
pub enum HookResult {
@@ -12,11 +12,7 @@ pub enum HookResult {
}
/// Run all hooks matching `phase` and collect into a `Report`.
pub fn run_phase(
hooks: &[HookSpec],
phase: &HookPhase,
runner: &HookRunner,
) -> Report {
pub fn run_phase(hooks: &[HookSpec], phase: &HookPhase, runner: &HookRunner) -> Report {
let mut report = Report::default();
for hook in hooks.iter().filter(|h| &h.phase == phase) {
let result = runner.run_hook(hook);

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use nothooks::{run_phase, HookRunner};
use notcore::{HookPhase, HookSpec};
use nothooks::{HookRunner, run_phase};
use std::path::PathBuf;
#[derive(Parser)]

View File

@@ -1,8 +1,8 @@
use std::path::PathBuf;
use std::process::Command;
use notcore::{HookPhase, HookSpec};
use crate::HookResult;
use crate::state::HookState;
use notcore::{HookPhase, HookSpec};
use std::path::PathBuf;
use std::process::Command;
pub struct HookRunner {
state_dir: PathBuf,
@@ -11,11 +11,17 @@ pub struct HookRunner {
impl HookRunner {
pub fn new(state_dir: PathBuf) -> Self {
Self { state_dir, force: false }
Self {
state_dir,
force: false,
}
}
pub fn with_force(state_dir: PathBuf) -> Self {
Self { state_dir, force: true }
Self {
state_dir,
force: true,
}
}
pub fn run_hook(&self, spec: &HookSpec) -> HookResult {
@@ -25,9 +31,8 @@ impl HookRunner {
return HookResult::Skipped;
}
let result = Command::new("nu")
.arg(&spec.script)
.status();
// TODO: support other shells
let result = Command::new("nu").arg(&spec.script).status();
match result {
Ok(status) if status.success() => {

View File

@@ -1,7 +1,7 @@
use std::collections::HashSet;
use std::path::Path;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
const STATE_FILE: &str = ".nothooks-state.toml";

View File

@@ -1,7 +1,7 @@
use notcore::{HookPhase, HookSpec};
use nothooks::{HookResult, HookRunner};
use std::fs;
use tempfile::TempDir;
use nothooks::{HookResult, HookRunner};
use notcore::{HookPhase, HookSpec};
fn make_hook_script(dir: &TempDir, name: &str, content: &str) -> HookSpec {
let path = dir.path().join(format!("{name}.nu"));

View File

@@ -1,8 +1,8 @@
use anyhow::{Context, Result};
use notcore::{HookPhase, Report, StepStatus};
use notfiles::{link, LinkOptions};
use nothooks::{run_phase, HookRunner};
use notsecrets::{install_age_key, resolve_age_key, BitwardenSource, FileSource, PromptSource};
use notfiles::{LinkOptions, link};
use nothooks::{HookRunner, run_phase};
use notsecrets::{BitwardenSource, FileSource, PromptSource, install_age_key, resolve_age_key};
use serde::Deserialize;
use std::path::{Path, PathBuf};
@@ -28,8 +28,12 @@ pub struct BootstrapSection {
pub sops_file: String,
}
pub(crate) fn default_bw_item() -> String { "age-key-dotfiles".to_string() }
pub(crate) fn default_sops_file() -> String { "secrets/bootstrap.sops.env".to_string() }
pub(crate) fn default_bw_item() -> String {
"age-key-dotfiles".to_string()
}
pub(crate) fn default_sops_file() -> String {
"secrets/bootstrap.sops.env".to_string()
}
pub struct BootstrapOptions {
pub config: PathBuf,
@@ -48,7 +52,9 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
// 1. Prerequisites
if let Some(check) = opts.check_prereqs {
match check() {
Ok(_) => { report.add("prerequisites", StepStatus::Ok); }
Ok(_) => {
report.add("prerequisites", StepStatus::Ok);
}
Err(e) => {
report.add("prerequisites", StepStatus::Failed(e.to_string()));
return Ok(report);
@@ -63,14 +69,19 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
let dotfiles_dir = match opts.dotfiles {
Some(d) => d,
None => notcore::expand_tilde(&cfg.bootstrap.dotfiles_dir)
.with_context(|| format!("cannot expand dotfiles_dir: {}", cfg.bootstrap.dotfiles_dir))?,
None => notcore::expand_tilde(&cfg.bootstrap.dotfiles_dir).with_context(|| {
format!("cannot expand dotfiles_dir: {}", cfg.bootstrap.dotfiles_dir)
})?,
};
// 3. Clone dotfiles if missing
match repo::clone_if_missing(&cfg.bootstrap.dotfiles_repo, &dotfiles_dir) {
Ok(true) => { report.add("clone dotfiles", StepStatus::Ok); }
Ok(false) => { report.add("clone dotfiles", StepStatus::Skipped); }
Ok(true) => {
report.add("clone dotfiles", StepStatus::Ok);
}
Ok(false) => {
report.add("clone dotfiles", StepStatus::Skipped);
}
Err(e) => {
report.add("clone dotfiles", StepStatus::Failed(e.to_string()));
return Ok(report);
@@ -110,7 +121,9 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
// threads exist. Subsequent Command::spawn calls (git, hooks) will
// inherit these vars — keys are validated by parse_env_line before
// reaching this point (null bytes and dangerous keys are rejected).
unsafe { std::env::set_var(&k, &v); }
unsafe {
std::env::set_var(&k, &v);
}
}
Ok(None) => {}
Err(e) => {
@@ -129,7 +142,12 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
}
// 6. Link dotfiles
let link_opts = LinkOptions { force: opts.force, no_backup: false, dry_run: false, verbose: false };
let link_opts = LinkOptions {
force: opts.force,
no_backup: false,
dry_run: false,
verbose: false,
};
match link(&dotfiles_dir, &[], &link_opts) {
Ok(state) => {
let count = state.entries.len();
@@ -147,9 +165,14 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
HookRunner::new(dotfiles_dir.clone())
};
for (phase, label) in [(HookPhase::Dot, "dot hooks"), (HookPhase::Setup, "setup hooks")] {
for (phase, label) in [
(HookPhase::Dot, "dot hooks"),
(HookPhase::Setup, "setup hooks"),
] {
let phase_report = run_phase(&cfg.hooks, &phase, &runner);
let failed = phase_report.steps.iter()
let failed = phase_report
.steps
.iter()
.filter(|s| matches!(s.status, notcore::StepStatus::Failed(_)))
.count();
let summary = if failed > 0 {
@@ -223,7 +246,12 @@ mod tests {
fn parse_env_line_blocks_ld_preload() {
let result = parse_env_line("LD_PRELOAD=/evil/lib.so");
assert!(result.is_err(), "expected error for LD_PRELOAD");
assert!(result.unwrap_err().to_string().contains("sensitive env key"));
assert!(
result
.unwrap_err()
.to_string()
.contains("sensitive env key")
);
}
/// Issue #5: PATH must be blocked.

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use clap::Parser;
use notstrap::{prereqs, run, BootstrapOptions};
use notstrap::{BootstrapOptions, prereqs, run};
#[derive(Parser)]
#[command(name = "notstrap", about = "Bootstrap a new machine from dotfiles")]
@@ -33,7 +33,12 @@ enum Cmd {
fn main() -> Result<()> {
let cli = Cli::parse();
let Cmd::Run { config, force, key_file, dotfiles } = cli.command;
let Cmd::Run {
config,
force,
key_file,
dotfiles,
} = cli.command;
let opts = BootstrapOptions {
config,
force,

View File

@@ -7,9 +7,18 @@ pub struct Prereq {
}
const PREREQS: &[Prereq] = &[
Prereq { cmd: "nu", install_hint: "brew install nushell OR nix-env -iA nixpkgs.nushell" },
Prereq { cmd: "sops", install_hint: "brew install sops OR nix-env -iA nixpkgs.sops" },
Prereq { cmd: "age", install_hint: "brew install age OR nix-env -iA nixpkgs.age" },
Prereq {
cmd: "nu",
install_hint: "brew install nushell OR nix-env -iA nixpkgs.nushell",
},
Prereq {
cmd: "sops",
install_hint: "brew install sops OR nix-env -iA nixpkgs.sops",
},
Prereq {
cmd: "age",
install_hint: "brew install age OR nix-env -iA nixpkgs.age",
},
];
pub fn check_prerequisites() -> Result<()> {

94
mise.toml Normal file
View File

@@ -0,0 +1,94 @@
[tools]
rust = "1.91.1"
[tasks]
# ── Quality Gates ──────────────────────────────────────────────────────────────
[tasks."all:check"]
description = "cargo check across workspace"
run = "cargo check --workspace"
[tasks."all:fmt"]
description = "Format all code"
run = "cargo fmt"
[tasks."all:fmt-check"]
description = "Check formatting (no changes)"
run = "cargo fmt --check"
[tasks."all:clippy"]
description = "Clippy with warnings as errors"
run = "cargo clippy --workspace -- -D warnings"
[tasks."all:test"]
description = "Run all tests via nextest"
run = "cargo nextest run --workspace --cargo-profile release"
[tasks."all:test-unit"]
description = "Run unit tests only (exclude integration)"
run = "cargo nextest run --workspace --lib --cargo-profile release"
[tasks."all:test-integration"]
description = "Run integration tests"
run = "cargo nextest run -p integration --cargo-profile release"
[tasks."all:ci"]
description = "Full CI gate: fmt-check + clippy + nextest"
run = """
#!/usr/bin/env bash
set -euo pipefail
echo " fmt "
cargo fmt --check
echo " clippy "
cargo clippy --workspace -- -D warnings
echo " dep-boundaries "
python3 scripts/check-dep-boundaries.py
echo " test "
cargo nextest run --workspace --cargo-profile release
echo " all gates passed"
"""
[tasks."all:ci-setup"]
description = "One-time setup: install cargo-nextest, cargo-deny"
run = """
#!/usr/bin/env bash
set -e
echo "Installing cargo-nextest..."
command -v cargo-nextest >/dev/null 2>&1 || ~/.local/bin/mise exec -- cargo install cargo-nextest --locked
echo "Installing cargo-deny..."
command -v cargo-deny >/dev/null 2>&1 || ~/.local/bin/mise exec -- cargo install cargo-deny --locked
echo " CI setup complete"
"""
[tasks."all:dep-boundaries"]
description = "Enforce crate dependency boundary rules"
run = "python3 scripts/check-dep-boundaries.py"
[tasks."all:deny"]
description = "Run cargo-deny license/security checks"
run = "cargo deny check"
# ── Dev Workflow ───────────────────────────────────────────────────────────────
[tasks."all:build"]
description = "Build all workspace crates"
run = "cargo build"
[tasks."all:build-release"]
description = "Build release binaries"
run = "cargo build --release"
[tasks."all:commit"]
description = "Stage all + commit. Usage: mise run all:commit -- 'message'"
run = """
#!/usr/bin/env bash
set -e
MSG="${1:?usage: mise run all:commit -- 'message'}"
git add -A
git commit -m "$MSG"
"""
[tasks."all:watch"]
description = "Watch for changes and run check + nextest"
run = "cargo watch -x 'check --workspace' -x 'nextest run --workspace --cargo-profile release'"

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""
Enforce crate dependency boundaries for the notfiles workspace:
- notcore must remain a leaf crate (no deps on other notfiles crates)
- notfiles, notsecrets, nothooks may depend on notcore but NOT on each other
- notstrap may depend on notcore + feature crates (it's the orchestrator)
Exits 0 if boundaries are clean, 1 if violations found.
"""
import json
import subprocess
import sys
LEAF = "notcore"
FEATURE_CRATES = {"notfiles", "notsecrets", "nothooks"}
ORCHESTRATOR = "notstrap"
WORKSPACE_CRATES = FEATURE_CRATES | {LEAF, ORCHESTRATOR}
result = subprocess.run(
["cargo", "metadata", "--no-deps", "--format-version", "1"],
capture_output=True, text=True, check=True,
)
meta = json.loads(result.stdout)
# Build name -> deps map for workspace members only
crate_deps: dict[str, set[str]] = {}
for pkg in meta["packages"]:
name = pkg["name"]
if name not in WORKSPACE_CRATES:
continue
deps = {d["name"] for d in pkg["dependencies"] if d["name"] in WORKSPACE_CRATES}
crate_deps[name] = deps
violations = []
# notcore must have no workspace deps
if crate_deps.get(LEAF):
violations.append(f"{LEAF} depends on workspace crates: {crate_deps[LEAF]}")
# feature crates must not depend on each other
for crate in FEATURE_CRATES:
forbidden = crate_deps.get(crate, set()) & FEATURE_CRATES
if forbidden:
violations.append(f"{crate} depends on sibling feature crates: {forbidden}")
if violations:
print("FAIL: crate dependency boundary violations detected:")
for v in violations:
print(f" - {v}")
sys.exit(1)
print("OK: crate dependency boundaries are clean")
print(f" {LEAF}: no workspace deps (leaf)")
for c in sorted(FEATURE_CRATES):
print(f" {c}: depends only on {LEAF}")
print(f" {ORCHESTRATOR}: orchestrator (unrestricted)")

View File

@@ -1,6 +1,6 @@
use notcore::{HookPhase, HookSpec, StepStatus};
use nothooks::{run_phase, HookRunner};
use notstrap::{run, BootstrapOptions};
use nothooks::{HookRunner, run_phase};
use notstrap::{BootstrapOptions, run};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;

View File

@@ -3,9 +3,9 @@ use std::path::Path;
use tempfile::TempDir;
use notcore::{HookPhase, HookSpec};
use notfiles::{LinkOptions, link};
use nothooks::{HookResult, HookRunner};
use notsecrets::{resolve_age_key, AgeKeySource, FileSource};
use notfiles::{link, LinkOptions};
use notsecrets::{AgeKeySource, FileSource, resolve_age_key};
/// Test that notsecrets and nothooks can each be used independently
/// in the same integration boundary — resolving an age key from a file
@@ -69,7 +69,12 @@ fn test_notfiles_respects_default_ignore() {
fs::write(pkg.join(".notfiles-state.toml"), "# state\n").unwrap();
fs::write(pkg.join(".nothooks-state.toml"), "# state\n").unwrap();
let opts = LinkOptions { force: false, no_backup: false, dry_run: false, verbose: false };
let opts = LinkOptions {
force: false,
no_backup: false,
dry_run: false,
verbose: false,
};
let state = link(d, &[], &opts).expect("link() failed");
// foo.txt linked