diff --git a/flake.lock b/flake.lock index 863dc62..7710151 100644 --- a/flake.lock +++ b/flake.lock @@ -82,9 +82,25 @@ "inputs": { "nix-systems": "nix-systems", "nixpkgs": "nixpkgs", + "tack": "tack", "unf": "unf" } }, + "tack": { + "locked": { + "lastModified": 1779981755, + "narHash": "sha256-sdHSW55s2NCd3t2ezu5oV98ZQPjXHE2nxn0IixFaxwU=", + "owner": "manic-systems", + "repo": "tack", + "rev": "6e62423ffe85ebe0db1a8e538aa3ff281f690b3f", + "type": "github" + }, + "original": { + "owner": "manic-systems", + "repo": "tack", + "type": "github" + } + }, "unf": { "inputs": { "ndg": "ndg", diff --git a/flake.nix b/flake.nix index a3cb7c8..090ab5e 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nix-systems.url = "github:nix-systems/default-linux"; unf.url = "git+https://git.atagen.co/atagen/unf"; + tack.url = "github:manic-systems/tack"; }; outputs = @@ -15,6 +16,7 @@ nixpkgs.lib.genAttrs (import nix-systems) ( system: function nixpkgs.legacyPackages.${system} system ); + tackFor = system: tack.packages.${system}.default; in { devShells = forEachSystem ( @@ -28,9 +30,10 @@ ); packages = forEachSystem ( - pkgs: _: { + pkgs: system: { default = pkgs.callPackage ./nix/default.nix { inherit version; + tack = tackFor system; }; docs = pkgs.callPackage unf.lib.pak-chooie { inherit self; @@ -55,9 +58,11 @@ in { imports = [ ./nix/module.nix ]; + programs.meat.tack = lib.mkDefault (tackFor pkgs.stdenv.hostPlatform.system); programs.meat.package = self.packages.${pkgs.stdenv.hostPlatform.system}.default.override { differ = cfg.differ; monitor = cfg.monitor; + tack = cfg.tack; }; }; diff --git a/meat.nu b/meat.nu old mode 100644 new mode 100755 index a03a4bc..cacf97a --- a/meat.nu +++ b/meat.nu @@ -121,86 +121,14 @@ def cmd-trade [] { } } -def pins-path [] { $"($env.MEATS)/pins/pins.toml" } -def lock-path [] { $"($env.MEATS)/pins/pins.lock.json" } - -def load-pins [] { - let p = pins-path - if not ($p | path exists) { error make { msg: $"no pins file at ($p)" } } - open --raw $p | from toml -} +def lock-path [] { $"($env.MEATS)/.tack/pins.lock.json" } def load-lock [] { let p = lock-path if ($p | path exists) { open --raw $p | from json } else { {} } } -# Sort keys alphabetically and write atomically. -def write-lock [lock: record] { - let p = lock-path - let sorted = $lock | columns | sort | reduce -f {} { |k, acc| $acc | insert $k ($lock | get $k) } - let tmp = $"($p).tmp" - $sorted | to json --indent 2 | save -f $tmp - ^mv $tmp $p -} - -# scheme:rest → expansion via {path} template, or pass-through. -def expand-shorturl [url: string, shorturls: record] { - let parts = $url | split row ":" -n 2 - if (($parts | length) < 2) { return $url } - let scheme = $parts | get 0 - if not ($scheme in ($shorturls | columns)) { return $url } - ($shorturls | get $scheme) | str replace --regex '\{path\}' ($parts | get 1) -} - -# Decompose to {git_url, ref?} for cheap ls-remote queries (used by `look`). -def parse-git-target [url: string] { - if ($url | str starts-with "github:") { - let body = $url | str substring 7.. - let path_query = $body | split row "?" -n 2 - let segs = ($path_query | get 0) | split row "/" - let owner = $segs | get 0 - let repo = $segs | get 1 - let ref = if (($segs | length) > 2) { $segs | skip 2 | str join "/" } else { null } - return { git_url: $"https://github.com/($owner)/($repo).git", ref: $ref } - } - if ($url | str starts-with "git+") { - let stripped = $url | str substring 4.. - let parts = $stripped | split row "?" -n 2 - let base = $parts | get 0 - let ref = if (($parts | length) > 1) { - ($parts | get 1) | split row "&" | each { |kv| - let kvp = $kv | split row "=" -n 2 - { k: ($kvp | get 0), v: ($kvp | get 1?) } - } | where k == "ref" | get -o 0 | get -o v - } else { null } - return { git_url: $base, ref: $ref } - } - error make { msg: $"unsupported url scheme for ls-remote: ($url)" } -} - -# Cheap "is upstream ahead?" check via git ls-remote. Returns the rev string. -def ls-remote-head [url: string] { - let tgt = parse-git-target $url - let target_ref = if ($tgt.ref? | is-not-empty) { $"refs/heads/($tgt.ref)" } else { "HEAD" } - let r = ^git ls-remote $tgt.git_url $target_ref | complete - if $r.exit_code != 0 { error make { msg: $"ls-remote failed: ($r.stderr)" } } - let first = $r.stdout | str trim | lines | get -o 0 - if ($first | is-empty) { error make { msg: "no refs returned" } } - $first | split row "\t" | get 0 -} - -# Real prefetch — caches in /nix/store and returns the full locked attrset. -# Output JSON: { hash: SRI, locked: {...}, original: {...}, storePath: PATH } -# --refresh bypasses the ref→rev cache (otherwise stale within tarball-ttl). -def prefetch-pin [url: string] { - let r = ^nix flake prefetch --refresh --json $url | complete - if $r.exit_code != 0 { error make { msg: $"prefetch failed for ($url): ($r.stderr)" } } - let j = $r.stdout | from json - $j.locked | insert narHash $j.hash -} - -# Fetch one locked input into the store, mirroring lib/inputs.nix's +# Fetch one locked input into the store, mirroring the tack resolver's # `builtins.fetchTree lock.`. Returns the name on failure, else null. def warm-pin [name: string, node: record] { let tmp = ^mktemp -t "meat-pin.XXXXXX.json" | str trim @@ -228,97 +156,14 @@ def warm-pins [] { def cmd-fresh [...names: string] { with-frame { meat-print "HUNTING FRESH MEATS.." - let pins = load-pins - let shorturls = $pins.shorturls? | default {} - let inputs = $pins.inputs - let lock = load-lock - - let requested = if ($names | is-empty) { $inputs | columns } else { $names } - - # Report unknown names up front and keep only real targets. - let targets = $requested | each { |name| - if not ($name in ($inputs | columns)) { - meat-print $"NO MEAT CALLED ($name | str upcase).." - null - } else { $name } - } | compact - - # Prefetch every target in parallel — this is the network-bound work. - # No lock writes happen here; results are collected and applied below. - let results = $targets | par-each { |name| - let old_rev = ($lock | get -o $name) | default {} | get -o rev - try { - let pin = $inputs | get $name - let expanded = expand-shorturl $pin.url $shorturls - let entry = prefetch-pin $expanded - { name: $name, ok: true, entry: $entry, old_rev: $old_rev, new_rev: $entry.rev } - } catch { |e| - { name: $name, ok: false, err: $e.msg } - } - } - - # Apply results sequentially in target order so the lock file is never - # written concurrently and reporting stays deterministic. - mut lock = $lock - for name in $targets { - let r = $results | where name == $name | first - meat-print $"PROCESSING ($name | str upcase).." - if not $r.ok { - meat-print $" NO FIND ($name | str upcase): ($r.err)" - continue - } - if $r.old_rev == $r.new_rev { - meat-print $" ($name | str upcase) STILL FRESH" - continue - } - $lock = $lock | upsert $name $r.entry - write-lock $lock - let from = if ($r.old_rev | is-empty) { "NEW" } else { $r.old_rev | str substring 0..8 } - meat-print $" ($name | str upcase): ($from) -> ($r.new_rev | str substring 0..8)" - } + do { cd $env.MEATS; ^$env.TACK update ...$names } } - print "" } -def cmd-look [] { +def cmd-look [...names: string] { with-frame { meat-print "LOOK FOR NEW MEATS.." - let pins = load-pins - let shorturls = $pins.shorturls? | default {} - let inputs = $pins.inputs - let lock = load-lock - - let rows = $inputs | transpose name pin - - # Query every upstream head in parallel; no printing happens here. - let results = $rows | par-each { |row| - let old_rev = ($lock | get -o $row.name) | default {} | get -o rev - try { - let expanded = expand-shorturl $row.pin.url $shorturls - let new_rev = ls-remote-head $expanded - { name: $row.name, ok: true, stale: ($old_rev != $new_rev), old_rev: $old_rev, new_rev: $new_rev } - } catch { - { name: $row.name, ok: false } - } - } - - # Report in input order so output is deterministic and never races. - mut stale = [] - for row in $rows { - let r = $results | where name == $row.name | first - if not $r.ok { - meat-print $" NO FIND ($row.name | str upcase).." - continue - } - if $r.stale { - let from = if ($r.old_rev | is-empty) { "NEW" } else { $r.old_rev | str substring 0..8 } - meat-print $" ($row.name | str upcase): ($from) -> ($r.new_rev | str substring 0..8)" - $stale = ($stale | append $row.name) - } - } - if ($stale | is-empty) { - meat-print "NO MEAT FRESHER" - } + do { cd $env.MEATS; ^$env.TACK look ...$names } } } @@ -429,7 +274,7 @@ def main [...args: string] { "poke" => { cmd-poke ...$rest } "gut" => { cmd-gut ...$rest } "trade" => { cmd-trade } - "look" => { cmd-look } + "look" => { cmd-look ...$rest } "fresh" => { cmd-fresh ...$rest } "hunt" => { cmd-hunt ...$rest } "ritual" => { cmd-ritual } diff --git a/nix/default.nix b/nix/default.nix index 4b5d5ff..fc7a3ae 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -5,6 +5,7 @@ nushell, makeBinaryWrapper, version, + tack, differ ? pkgs.dix, monitor ? pkgs.nix-output-monitor, ... @@ -26,7 +27,8 @@ stdenvNoCC.mkDerivation { makeBinaryWrapper ${nushell}/bin/nu $out/bin/meat \ --add-flags "$out/share/meat/meat.nu" \ --set DIFFER ${lib.getExe differ} \ - --set MONITOR ${lib.getExe monitor} + --set MONITOR ${lib.getExe monitor} \ + --set TACK ${lib.getExe tack} runHook postInstall ''; } diff --git a/nix/module.nix b/nix/module.nix index b2d6079..68be629 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -29,6 +29,10 @@ in description = "nix monitoring tool to use"; default = pkgs.nix-output-monitor; }; + tack = mkOption { + type = types.package; + description = "tack pin manager"; + }; }; config = lib.mkIf cfg.enable { environment.sessionVariables.MEATS = cfg.flake;