Compare commits
3 Commits
6427d188e6
...
039f8d808d
| Author | SHA1 | Date | |
|---|---|---|---|
| 039f8d808d | |||
| ca7c035ea5 | |||
| 25624c9f2b |
@@ -1,6 +1,6 @@
|
||||
project: notfiles
|
||||
id: notfile
|
||||
updated: 2026-07-07
|
||||
updated: 2026-07-08
|
||||
items:
|
||||
- id: uncommitted-work
|
||||
doob_uuid: null
|
||||
@@ -20,6 +20,11 @@ items:
|
||||
completed: null
|
||||
extra: []
|
||||
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'
|
||||
summary: done=13 running=0 pending=0 blocked=0
|
||||
commits: []
|
||||
|
||||
@@ -10,9 +10,9 @@ categories.workspace = true
|
||||
description = "Shared types and config for the notfiles dotfiles manager"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
strsim = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
strsim = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
||||
@@ -18,17 +18,17 @@ name = "notfiles"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
notcore = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
clap_complete = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
notcore = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
assert_fs = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
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
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -17,9 +17,9 @@ tempfile = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
cargo_metadata = "0.18"
|
||||
clap = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = "1"
|
||||
cargo_metadata = "0.18"
|
||||
syn = { version = "2", features = ["full", "visit"] }
|
||||
walkdir = "2"
|
||||
|
||||
@@ -13,11 +13,11 @@ name = "nothooks"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
notcore = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
notcore = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -7,9 +7,9 @@ description = "Tailscale network presence management for notstrap"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rpassword = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
yubikey = { version = "0.8", optional = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -5,26 +5,26 @@ edition = "2024"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
notcore = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rpassword = { workspace = true }
|
||||
base64 = "0.22"
|
||||
bech32 = "0.11"
|
||||
chacha20poly1305 = "0.10"
|
||||
curve25519-dalek = "4"
|
||||
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 }
|
||||
sha2 = "0.10"
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
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 }
|
||||
zeroize = "1"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -14,13 +14,13 @@ name = "notstrap"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
notcore = { workspace = true }
|
||||
notfiles = { workspace = true }
|
||||
notsecrets = { workspace = true }
|
||||
nothooks = { workspace = true }
|
||||
notnet = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
which = { workspace = true }
|
||||
notsecrets = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
which = { workspace = true }
|
||||
|
||||
@@ -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"
|
||||
|
||||
[dev-dependencies]
|
||||
notstrap = { path = "../../crates/notstrap" }
|
||||
anyhow = { workspace = true }
|
||||
assert_fs = { workspace = true }
|
||||
notcore = { path = "../../crates/notcore" }
|
||||
notfiles = { path = "../../crates/notfiles" }
|
||||
nothooks = { path = "../../crates/nothooks" }
|
||||
notsecrets = { path = "../../crates/notsecrets" }
|
||||
notcore = { path = "../../crates/notcore" }
|
||||
notstrap = { path = "../../crates/notstrap" }
|
||||
tempfile = { workspace = true }
|
||||
assert_fs = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
Reference in New Issue
Block a user