From 4a39e1f2daa9af71d88d8346a5d65f6901cc6d13 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 26 Sep 2025 13:41:42 -0700 Subject: [PATCH 01/10] update packages, add todoist --- flake.lock | 83 ++++++++++++-------------- users/natalie/home.nix | 12 ++++ users/natalie/laptop/configuration.nix | 1 + users/natalie/packages.nix | 2 - users/natalie/vim/languages.nix | 2 + users/natalie/vim/latex.nix | 4 +- 6 files changed, 57 insertions(+), 47 deletions(-) diff --git a/flake.lock b/flake.lock index 732953dd0e5b25b7b3b6f22321f7098e4c4bd061..8c35bf3a02d8f914e4d32e9fa7f28ef8afb5667c 100644 --- a/flake.lock +++ b/flake.lock @@ -96,17 +96,17 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "type": "github" + "lastModified": 1751685974, + "narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=", + "ref": "refs/heads/main", + "rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1", + "revCount": 92, + "type": "git", + "url": "https://git.lix.systems/lix-project/flake-compat.git" }, "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" + "type": "git", + "url": "https://git.lix.systems/lix-project/flake-compat.git" } }, "flake-compat_2": { @@ -125,6 +125,22 @@ "type": "github" } }, + "flake-compat_3": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -133,11 +149,11 @@ ] }, "locked": { - "lastModified": 1753121425, - "narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=", + "lastModified": 1754487366, + "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "644e0fc48951a860279da645ba77fe4a6e814c5e", + "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", "type": "github" }, "original": { @@ -201,27 +217,6 @@ } }, "flake-utils_4": { - "inputs": { - "systems": [ - "nvf", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_5": { "inputs": { "systems": "systems_5" }, @@ -239,7 +234,7 @@ "type": "github" } }, - "flake-utils_6": { + "flake-utils_5": { "inputs": { "systems": "systems_6" }, @@ -257,7 +252,7 @@ "type": "github" } }, - "flake-utils_7": { + "flake-utils_6": { "inputs": { "systems": "systems_7" }, @@ -486,8 +481,8 @@ }, "nvf": { "inputs": { + "flake-compat": "flake-compat", "flake-parts": "flake-parts", - "flake-utils": "flake-utils_4", "mnw": "mnw", "nixpkgs": [ "nixpkgs" @@ -495,11 +490,11 @@ "systems": "systems_4" }, "locked": { - "lastModified": 1754043154, - "narHash": "sha256-Q47BVxwocPZ1GEDs6gKGH00eso74i0gTEPNuF8Kj0T8=", + "lastModified": 1755843058, + "narHash": "sha256-oAajTGcyTzuND7AAlaRXO+wF7ytAMOnrE4sYUjRonB8=", "owner": "notashelf", "repo": "nvf", - "rev": "69659e078b6531698255deda280c9407b34e29ea", + "rev": "d36996c8ba5c8d68b5def1b660e06d281ef0a3a8", "type": "github" }, "original": { @@ -769,8 +764,8 @@ }, "zig": { "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils_5", + "flake-compat": "flake-compat_2", + "flake-utils": "flake-utils_4", "nixpkgs": "nixpkgs_3" }, "locked": { @@ -789,8 +784,8 @@ }, "zig-overlay": { "inputs": { - "flake-compat": "flake-compat_2", - "flake-utils": "flake-utils_7", + "flake-compat": "flake-compat_3", + "flake-utils": "flake-utils_6", "nixpkgs": [ "zls", "nixpkgs" @@ -812,7 +807,7 @@ }, "zls": { "inputs": { - "flake-utils": "flake-utils_6", + "flake-utils": "flake-utils_5", "gitignore": "gitignore_2", "langref": "langref", "nixpkgs": "nixpkgs_4", diff --git a/users/natalie/home.nix b/users/natalie/home.nix index eba325355da35fbd28c3c131b199f88746ae78da..b902b0c2d02b64fff482326c68dda270acc85ab4 100644 --- a/users/natalie/home.nix +++ b/users/natalie/home.nix @@ -16,6 +16,18 @@ man.generateCaches = false; lsd.enable = true; android-sdk.enable = true; + ghostty = { + enable = true; + package = null; + settings = { + theme = "light:catppuccin-latte,dark:catppuccin-macchiato"; + font-family = "AT Name Mono"; + adjust-cursor-thickness = 1; + minimum-contrast = 1.1; + background-opacity = 0.9; + background-blur = true; + }; + }; # sort-lines:end }; diff --git a/users/natalie/laptop/configuration.nix b/users/natalie/laptop/configuration.nix index 7e83946932a2f1cccc3d0c1a4e1e9b670eb7182b..c124b21ea2b115f101a54a6271f2750b7e70344d 100644 --- a/users/natalie/laptop/configuration.nix +++ b/users/natalie/laptop/configuration.nix @@ -46,6 +46,7 @@ "zen" "supertuxkart" "sf-symbols" + "todoist" "mediosz/tap/swipeaerospace" ]; diff --git a/users/natalie/packages.nix b/users/natalie/packages.nix index d10b03e4466f2d4cd97cce17651f866daaa0cff0..5a16c867d2b398df4e52266913e6669be2743dc0 100644 --- a/users/natalie/packages.nix +++ b/users/natalie/packages.nix @@ -19,8 +19,6 @@ with pkgs; deno #ides - zed-editor - emacs #virtualization qemu diff --git a/users/natalie/vim/languages.nix b/users/natalie/vim/languages.nix index 9c447ed006f378b5e3750dda65e37e0c70bd4654..dda6dfd6ea01e5de9cc422a1b0d147c961534577 100644 --- a/users/natalie/vim/languages.nix +++ b/users/natalie/vim/languages.nix @@ -105,6 +105,8 @@ scrollDocsDown = null; scrollDocsUp = null; }; + sourcePlugins = { + }; setupOpts = { keymap = { diff --git a/users/natalie/vim/latex.nix b/users/natalie/vim/latex.nix index dd256a544add51076952ca5837f0ebc5631f0a06..a0c8ddc47750df4a6509640f0afa39cb5d5c88d2 100644 --- a/users/natalie/vim/latex.nix +++ b/users/natalie/vim/latex.nix @@ -57,7 +57,9 @@ }; setupOpts = { sources = { - default = [ "vimtex" ]; + default = [ + "vimtex" + ]; providers = { vimtex = { name = "vimtex"; -- 2.54.0 From 4ecab4b9c4140b5ce5738b4fac2261c0bbc324ad Mon Sep 17 00:00:00 2001 From: Natalie Date: Wed, 1 Oct 2025 17:21:21 -0700 Subject: [PATCH 02/10] ghostty shader default type --- modules/home/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/home/default.nix b/modules/home/default.nix index bc3a5ebfd3c15f9d93ab6737cf29b830661a7fa6..997ad2406a696cae358364b543c027167279e82d 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -19,7 +19,7 @@ in options = { programs.ghostty.shader = lib.mkOption { type = lib.types.str; - default = { }; + default = ""; description = "set the ghostty shader, relative to 'files/ghostty'"; }; programs.android-sdk.enable = lib.mkOption { -- 2.54.0 From 6188561b16c8653053cf504aeae3e9411eb3050e Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 2 Feb 2026 17:51:27 -0800 Subject: [PATCH 03/10] disable android sdk --- users/natalie/home.nix | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/users/natalie/home.nix b/users/natalie/home.nix index 295169243fda67476144879e74254928b7153da4..f5999757d79db11a7c1160fa9a88551a997f1f81 100644 --- a/users/natalie/home.nix +++ b/users/natalie/home.nix @@ -1,4 +1,9 @@ -{ lib, host, ... }@args: +{ + lib, + host, + pkgs, + ... +}@args: { programs = { # sort-lines:start @@ -9,7 +14,7 @@ fish.enable = true; man.generateCaches = false; lsd.enable = true; - android-sdk.enable = true; + android-sdk.enable = false; ghostty = { enable = true; package = null; @@ -33,5 +38,5 @@ }; }; - home.packages = import ./packages.nix args; + home.packages = import ./packages.nix pkgs; } -- 2.54.0 From 26468a201675fa97bc68e8ae64408e0321ba85c0 Mon Sep 17 00:00:00 2001 From: Natalie Date: Tue, 28 Apr 2026 12:50:16 -0700 Subject: [PATCH 04/10] fix: formatting --- flake.nix | 11 ++++------- modules/macos/tiling/aerospace.nix | 2 +- users/natalie/home.nix | 4 ---- users/natalie/home/vicinae.nix | 10 ++++++++-- users/natalie/laptop/configuration.nix | 4 ++-- users/natalie/laptop/home.nix | 3 +++ 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/flake.nix b/flake.nix index a9317dbb4bc952f69ece364e28d9b22184513ae5..46386565e2b359f5630120d51a68f30d2d343549 100644 --- a/flake.nix +++ b/flake.nix @@ -67,13 +67,9 @@ }; }); }) - # https://github.com/NixOS/nixpkgs/issues/476669#issuecomment-3707850520 + # https://github.com/NixOS/nixpkgs/issues/484618#issuecomment-3827163603 (_: prev: { vesktop = prev.vesktop.overrideAttrs (_: { - preBuild = '' - cp -r ${prev.electron.dist} electron-dist - chmod -R u+w electron-dist - ''; buildPhase = '' runHook preBuild @@ -81,8 +77,9 @@ pnpm exec electron-builder \ --dir \ -c.asarUnpack="**/*.node" \ - -c.electronDist="electron-dist" \ - -c.electronVersion=${prev.electron.version} + -c.electronDist=${if prev.stdenv.hostPlatform.isDarwin then "." else "electron-dist"} \ + -c.electronVersion=${prev.electron.version} \ + ${prev.lib.optionalString prev.stdenv.hostPlatform.isDarwin "-c.mac.identity=null"} runHook postBuild ''; diff --git a/modules/macos/tiling/aerospace.nix b/modules/macos/tiling/aerospace.nix index b3acf3cd90601a1302a9a4a592abd653215d10e0..a18b269632c85ffed1cb14406c4c56406bd65a84 100644 --- a/modules/macos/tiling/aerospace.nix +++ b/modules/macos/tiling/aerospace.nix @@ -34,7 +34,7 @@ outer = { left = 0; bottom = 0; - top = 40; + top = 10; right = 0; }; }; diff --git a/users/natalie/home.nix b/users/natalie/home.nix index 1001ee382ad204cee304754fa57b626df11e4f0b..c59a8a0392a563ed6331e1976b54be3917bcfc32 100644 --- a/users/natalie/home.nix +++ b/users/natalie/home.nix @@ -31,10 +31,6 @@ }; }; # sort-lines:end - ghostty = { - enable = true; - settings.theme = "Catppuccin Mocha"; - }; fish = { shellAliases = diff --git a/users/natalie/home/vicinae.nix b/users/natalie/home/vicinae.nix index 132ed839a7ca552bb1923bc5f6dbbe537c05cf3e..6dc91ed649bd1df50ebf4148fb369670b2623108 100644 --- a/users/natalie/home/vicinae.nix +++ b/users/natalie/home/vicinae.nix @@ -1,7 +1,13 @@ -{ inputs, pkgs, ... }: +{ + inputs, + pkgs, + lib, + host, + ... +}: { services.vicinae = { - enable = true; + enable = host.linux; systemd = { enable = true; autoStart = true; diff --git a/users/natalie/laptop/configuration.nix b/users/natalie/laptop/configuration.nix index c124b21ea2b115f101a54a6271f2750b7e70344d..6ad204a544a8351c5c28abedb90b896f59f8f803 100644 --- a/users/natalie/laptop/configuration.nix +++ b/users/natalie/laptop/configuration.nix @@ -7,7 +7,7 @@ # Custom configuration modules in "modules" are shared between users, # and can be configured in this "shared" namespace shared.darwin = { - macAppStoreApps = [ "wireguard" ]; +# macAppStoreApps = [ "wireguard" ]; }; system.defaults.NSGlobalDomain."com.apple.trackpad.scaling" = 1.0; @@ -40,7 +40,7 @@ casks = [ "battle-net" "stremio" - "alt-tab" +# "alt-tab" "legcord" "zulip" "zen" diff --git a/users/natalie/laptop/home.nix b/users/natalie/laptop/home.nix index 6974967997ae41ad67f2d9f53fe434226411dae3..8b2dd291b777b872af4efea6573f247a89a1efb6 100644 --- a/users/natalie/laptop/home.nix +++ b/users/natalie/laptop/home.nix @@ -8,6 +8,9 @@ ... }@args: { + programs.atuin = { + settings.data_dir = "/Users/${user.username}/.local/share/atuin"; + }; home = { stateVersion = "23.05"; # Please read the comment before changing. # shell = pkgs.fish; -- 2.54.0 From 6e5196e82473dd69fa10d81abb9b7998c749a3e2 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Mon, 26 Jan 2026 14:50:57 -0800 Subject: [PATCH 05/10] clover: work stuff --- users/clover/home.nix | 7 +++++-- users/clover/work/home.nix | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/users/clover/home.nix b/users/clover/home.nix index 673e3b5cb39ed98b2ce92fea51331f2069b90d03..d587dc0700fc88f4ea885677ed228a6236780788 100644 --- a/users/clover/home.nix +++ b/users/clover/home.nix @@ -38,6 +38,9 @@ in ]; in all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop); + sessionPath = [ + "$HOME/.local/bin" + ]; }; programs = { # sort-lines:start @@ -72,8 +75,8 @@ in jf = "jj git fetch"; e = "jj edit"; d = "jj desc"; - bup = "jj b m --from 'heads(::@- & bookmarks())' --to '@'"; - moveself = "jj rebase -r @ -d"; + bup = "jj b m --from 'heads(::@- & bookmarks()) ~ (master | bookmarks(glob:\"deployment*\"))' --to '@'"; + moveself = "jj rebase -s @ -d"; logall = "jj log -r 'all()'"; cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit"; }; diff --git a/users/clover/work/home.nix b/users/clover/work/home.nix index a6687ddcafbae66923c46d6cd449de1514f6eccc..f9a2d9515898b3a742f55de6574a7ddeb452cc4a 100644 --- a/users/clover/work/home.nix +++ b/users/clover/work/home.nix @@ -24,4 +24,7 @@ ssm-session-manager-plugin awscli ]; + home.sessionPath = [ + "$HOME/.opencode/bin" + ]; } -- 2.54.0 From 08f4390f3321397ef6b7c0e57619ca6a4d39573e Mon Sep 17 00:00:00 2001 From: clover caruso Date: Tue, 24 Feb 2026 12:08:47 -0800 Subject: [PATCH 06/10] clover: more work stuff --- flake.nix | 2 + packages/autofmt/autofmt.js | 12 +- packages/autofmt/default.nix | 18 +- packages/jj-import-merge/default.nix | 9 + packages/jj-import-merge/jj-import-merge.js | 871 ++++++++++++++++++++ users/clover/home.nix | 90 +- users/clover/paperback/home.nix | 13 +- users/clover/sandwich/home.nix | 7 + users/clover/work/home.nix | 13 +- 9 files changed, 999 insertions(+), 36 deletions(-) create mode 100644 packages/jj-import-merge/default.nix create mode 100644 packages/jj-import-merge/jj-import-merge.js create mode 100644 users/clover/sandwich/home.nix diff --git a/flake.nix b/flake.nix index a9317dbb4bc952f69ece364e28d9b22184513ae5..c65f15040704d23f4d7730710ef047c5f65bf5ba 100644 --- a/flake.nix +++ b/flake.nix @@ -92,6 +92,7 @@ # custom packages (_: pkgs: { autofmt = pkgs.callPackage ./packages/autofmt { }; + "jj-import-merge" = pkgs.callPackage ./packages/jj-import-merge { }; exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { }; awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { }; }) @@ -125,6 +126,7 @@ nvim-natalie = mkNeovim "natalie" pkgs; inherit (pkgs) autofmt; + "jj-import-merge" = pkgs."jj-import-merge"; } // lib.optionalAttrs (system == "aarch64-darwin") { # "nix run .#darwin-rebuild" diff --git a/packages/autofmt/autofmt.js b/packages/autofmt/autofmt.js index 7211f0b32c6e745f2c6807356e8d38b48cb79425..5bb0a0037307e72927401fa4f3db4859f9e45b26 100755 --- a/packages/autofmt/autofmt.js +++ b/packages/autofmt/autofmt.js @@ -100,12 +100,14 @@ const formatters = { }, prettier: { languages: ["javascript", "markdown", "json", "yaml", "mdx", "html", "css"], + mustMatchFile: true, files: ["node_modules/.bin/prettier"], cmd: ["node_modules/.bin/prettier", "--write", "--", "$files"], stdin: (file) => ["node_modules/.bin/prettier", "--stdin-filepath", file], }, biome: { languages: ["javascript", "json", "css"], + mustMatchFile: true, files: ["node_modules/.bin/biome"], cmd: ["node_modules/.bin/biome", "format", "--", "$files"], stdin: (file) => [ @@ -542,20 +544,22 @@ function pickFormatter(file) { : Object.entries(formatters[fmt].files) .map(([base, check]) => ({ base, check })); for (const { base, check } of filesToCheck) { + let fileMatches; if (base.includes("/")) { - matches = readDir(path.join(x, path.dirname(base))) // + fileMatches = readDir(path.join(x, path.dirname(base))) // .includes(path.basename(base)); } else { - matches = children.includes(base); + fileMatches = children.includes(base); } - if (matches && typeof check === "function") { + if (fileMatches && typeof check === "function") { let text; - matches = !!check({ + fileMatches = !!check({ get text() { return text ??= fs.readFileSync(path.join(x, base), "utf-8"); }, }); } + matches ||= fileMatches; } } if (matches) { diff --git a/packages/autofmt/default.nix b/packages/autofmt/default.nix index 45db7d83280d053850fcd1f77fc1a757e338ed38..82a0fe2a6a1e1133498d93c2d118e070cb25ee70 100644 --- a/packages/autofmt/default.nix +++ b/packages/autofmt/default.nix @@ -1,6 +1,5 @@ { pkgs, ... }: -pkgs.writeShellApplication { - name = "autofmt"; +let runtimeInputs = with pkgs; [ # include only a couple of formatters by default clang-tools @@ -11,5 +10,18 @@ pkgs.writeShellApplication { rustfmt zig ]; - text = ''exec ${pkgs.nodejs}/bin/node ${./autofmt.js} "$@"''; +in +pkgs.symlinkJoin { + name = "autofmt"; + paths = [ + (pkgs.writeTextFile { + name = "autofmt"; + destination = "/bin/autofmt"; + executable = true; + text = builtins.replaceStrings + [ "#!/usr/bin/env node" ] + [ "#!${pkgs.nodejs}/bin/node" ] + (builtins.readFile ./autofmt.js); + }) + ] ++ runtimeInputs; } diff --git a/packages/jj-import-merge/default.nix b/packages/jj-import-merge/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..281bac05ddff1e3ccdab9d2dfc39e9144749f80f --- /dev/null +++ b/packages/jj-import-merge/default.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: +pkgs.writeShellApplication { + name = "jj-import-merge"; + runtimeInputs = with pkgs; [ + autofmt + nodejs + ]; + text = ''exec ${pkgs.nodejs}/bin/node ${./jj-import-merge.js} "$@"''; +} diff --git a/packages/jj-import-merge/jj-import-merge.js b/packages/jj-import-merge/jj-import-merge.js new file mode 100644 index 0000000000000000000000000000000000000000..df56d84e771df5b1f29c3de95638a1c4eb6c84f4 --- /dev/null +++ b/packages/jj-import-merge/jj-import-merge.js @@ -0,0 +1,871 @@ +#!/usr/bin/env node +// jj-import-merge +// Merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts. + +import * as child_process from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import process from "node:process"; + +const SUPPORTED_EXTENSIONS = new Set([ + ".cjs", + ".cts", + ".js", + ".jsx", + ".mjs", + ".mts", + ".ts", + ".tsx", +]); + +const CONFLICT_START_RE = /^<<<<<<< conflict \d+ of \d+/m; +const CONFLICT_END_RE = /^>>>>>>> conflict \d+ of \d+ ends$/m; + +main(); + +function main() { + const options = parseArgs(process.argv.slice(2)); + let files = options.files.map((file) => path.resolve(file)); + + if (options.allJj) { + files.push(...listJjConflictFiles().map((file) => path.resolve(file))); + } + + files = [...new Set(files)]; + if (files.length === 0) { + usage(1); + } + + let failed = false; + for (const file of files) { + try { + const result = processFile(file); + if (result.kind === "formatted") { + console.info(`formatted ${path.relative(process.cwd(), file)}`); + } else if (result.kind === "rewritten") { + console.info(`rewrote ${path.relative(process.cwd(), file)}`); + } + } catch (error) { + failed = true; + const message = error instanceof Error ? error.message : String(error); + console.error(`error: ${message}`); + } + } + + process.exit(failed ? 1 : 0); +} + +function parseArgs(argv) { + const options = { + allJj: false, + files: [], + }; + + while (argv.length > 0) { + const arg = argv.shift(); + if (arg === "-h" || arg === "--help") { + usage(0); + } else if (arg === "--all-jj") { + options.allJj = true; + } else if (arg === "--") { + options.files.push(...argv); + break; + } else if (arg.startsWith("-")) { + console.error(`error: unknown option ${JSON.stringify(arg)}`); + usage(1); + } else { + options.files.push(arg); + } + } + + return options; +} + +function usage(code) { + const exe = "jj-import-merge"; + const stream = code === 0 ? process.stdout : process.stderr; + stream.write(`usage: ${exe} [--all-jj] [--] ...\n`); + stream.write(`\n`); + stream.write(`merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts\n`); + stream.write(`\n`); + stream.write(`options:\n`); + stream.write(` --all-jj resolve all conflicted JS/TS files from 'jj resolve --list'\n`); + stream.write(` -h show help\n`); + process.exit(code); +} + +function listJjConflictFiles() { + const result = child_process.spawnSync( + "jj", + ["--color=never", "--quiet", "resolve", "--list"], + { encoding: "utf8" }, + ); + + if (result.error) { + throw new Error(`failed to run jj: ${result.error.message}`); + } + if (result.status !== 0) { + throw new Error(result.stderr.trim() || "jj resolve --list failed"); + } + + return result.stdout + .split(/\r?\n/) + .map((line) => line.trimEnd()) + .filter(Boolean) + .map((line) => line.match(/^(.*?)\s{2,}.+conflict$/)) + .filter(Boolean) + .map((match) => match[1]); +} + +function processFile(file) { + if (!SUPPORTED_EXTENSIONS.has(path.extname(file))) { + return { kind: "noop" }; + } + + let original; + try { + original = fs.readFileSync(file, "utf8"); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`cannot read ${file}: ${message}`); + } + + if (!CONFLICT_START_RE.test(original)) { + return { kind: "noop" }; + } + + const parsed = parseConflictFile(original); + if (!parsed.supported) { + return { kind: "noop" }; + } + + const leftText = parsed.parts.map((part) => part.a).join(""); + const rightText = parsed.parts.map((part) => part.b).join(""); + const leftImports = extractTopImportBlock(leftText); + const rightImports = extractTopImportBlock(rightText); + if (!leftImports || !rightImports) { + return { kind: "noop" }; + } + + const leftPrelude = leftText.slice(0, leftImports.start); + const rightPrelude = rightText.slice(0, rightImports.start); + if (leftPrelude !== rightPrelude) { + return { kind: "noop" }; + } + + const start = projectBoundaryToOriginal( + parsed.parts, + leftImports.start, + rightImports.start, + ); + const end = projectBoundaryToOriginal(parsed.parts, leftImports.end, rightImports.end); + if (start == null || end == null || end < start) { + return { kind: "noop" }; + } + + const mergedImports = mergeImportStatements( + leftImports.statements, + rightImports.statements, + detectNewline(original), + ); + const updated = original.slice(0, start) + mergedImports + original.slice(end); + + if (updated === original) { + return { kind: "noop" }; + } + + try { + fs.writeFileSync(file, updated, "utf8"); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`cannot write ${file}: ${message}`); + } + + if (hasConflictMarkers(updated)) { + return { kind: "rewritten" }; + } + + const formatResult = child_process.spawnSync("autofmt", [file], { + encoding: "utf8", + stdio: "inherit", + }); + + if (formatResult.error || formatResult.status !== 0) { + fs.writeFileSync(file, original, "utf8"); + if (formatResult.error) { + throw new Error(`autofmt failed for ${file}: ${formatResult.error.message}`); + } + throw new Error(`autofmt failed for ${file}`); + } + + return { kind: "formatted" }; +} + +function parseConflictFile(text) { + const lines = splitLines(text); + const parts = []; + + let index = 0; + while (index < lines.length) { + const line = lines[index].text; + if (!line.startsWith("<<<<<<< conflict ")) { + const start = index; + index += 1; + while ( + index < lines.length && + !lines[index].text.startsWith("<<<<<<< conflict ") + ) { + index += 1; + } + const raw = joinLines(lines, start, index); + parts.push({ type: "plain", orig: raw, a: raw, b: raw }); + continue; + } + + const block = parseConflictBlock(lines, index); + if (!block.supported) { + return { supported: false, parts: [] }; + } + parts.push(block.part); + index = block.nextIndex; + } + + return { supported: true, parts }; +} + +function parseConflictBlock(lines, startIndex) { + let index = startIndex + 1; + const entries = []; + + while (index < lines.length) { + const line = lines[index].text; + if (line.startsWith(">>>>>>> conflict ")) { + break; + } + if (line.startsWith("+++++++ ")) { + const contentStart = index + 1; + index += 1; + while ( + index < lines.length && + !isConflictEntryStart(lines[index].text) && + !lines[index].text.startsWith(">>>>>>> conflict ") + ) { + index += 1; + } + entries.push({ + kind: "explicit", + text: joinLines(lines, contentStart, index), + }); + continue; + } + if (line.startsWith("%%%%%%% diff from:")) { + if (index + 1 >= lines.length || !/^\\\\+\s*to:/.test(lines[index + 1].text)) { + return { supported: false, nextIndex: startIndex + 1 }; + } + index += 2; + let text = ""; + while ( + index < lines.length && + !isConflictEntryStart(lines[index].text) && + !lines[index].text.startsWith(">>>>>>> conflict ") + ) { + const diffLine = lines[index].text; + const prefix = diffLine[0]; + if (prefix === " " || prefix === "+") { + text += diffLine.slice(1); + } else if (prefix !== "-") { + return { supported: false, nextIndex: startIndex + 1 }; + } + index += 1; + } + entries.push({ kind: "diff", text }); + continue; + } + return { supported: false, nextIndex: startIndex + 1 }; + } + + if ( + index >= lines.length || + !lines[index].text.startsWith(">>>>>>> conflict ") || + entries.length !== 2 + ) { + return { supported: false, nextIndex: startIndex + 1 }; + } + + return { + supported: true, + nextIndex: index + 1, + part: { + type: "conflict", + orig: joinLines(lines, startIndex, index + 1), + a: entries[0].text, + b: entries[1].text, + }, + }; +} + +function isConflictEntryStart(line) { + return line.startsWith("+++++++ ") || line.startsWith("%%%%%%% diff from:"); +} + +function splitLines(text) { + const lines = []; + let start = 0; + + for (let index = 0; index < text.length; index += 1) { + if (text[index] !== "\n") continue; + lines.push({ + start, + end: index + 1, + text: text.slice(start, index + 1), + }); + start = index + 1; + } + + if (start < text.length) { + lines.push({ start, end: text.length, text: text.slice(start) }); + } + + return lines; +} + +function joinLines(lines, start, end) { + let text = ""; + for (let index = start; index < end; index += 1) { + text += lines[index].text; + } + return text; +} + +function extractTopImportBlock(text) { + const lines = splitLines(text); + if (lines.length === 0) return null; + + let index = 0; + if (lines[0]?.text.startsWith("#!")) { + index += 1; + } + + index = skipPrelude(lines, index); + if (index == null || index >= lines.length) { + return null; + } + + const first = parseImportStatement(lines, index); + if (!first) { + return null; + } + + const start = lines[index].start; + const statements = [first.statement]; + let end = offsetForLine(lines, first.nextIndex); + index = first.nextIndex; + + while (true) { + const afterTrivia = skipBlankAndComments(lines, index); + if (afterTrivia == null) { + return null; + } + + const next = parseImportStatement(lines, afterTrivia); + if (!next) { + break; + } + + statements.push(next.statement); + end = offsetForLine(lines, next.nextIndex); + index = next.nextIndex; + } + + return { start, end, statements }; +} + +function skipPrelude(lines, startIndex) { + let index = startIndex; + while (index < lines.length) { + const afterTrivia = skipBlankAndComments(lines, index); + if (afterTrivia == null) { + return null; + } + if (afterTrivia >= lines.length) { + return afterTrivia; + } + + const trimmed = lines[afterTrivia].text.trim(); + if (isDirectiveLine(trimmed)) { + index = afterTrivia + 1; + continue; + } + return afterTrivia; + } + return index; +} + +function skipBlankAndComments(lines, startIndex) { + let index = startIndex; + + while (index < lines.length) { + const trimmed = lines[index].text.trim(); + if (trimmed === "") { + index += 1; + continue; + } + if (trimmed.startsWith("//")) { + index += 1; + continue; + } + if (trimmed.startsWith("/*")) { + index = skipBlockComment(lines, index); + if (index == null) { + return null; + } + continue; + } + break; + } + + return index; +} + +function skipBlockComment(lines, startIndex) { + for (let index = startIndex; index < lines.length; index += 1) { + if (lines[index].text.includes("*/")) { + return index + 1; + } + } + return null; +} + +function isDirectiveLine(line) { + return /^["'][^"'\\]*(?:\\.[^"'\\]*)*["'];?$/.test(line); +} + +function parseImportStatement(lines, startIndex) { + if (startIndex >= lines.length) { + return null; + } + + const firstLine = lines[startIndex].text.trimStart(); + if (!firstLine.startsWith("import")) { + return null; + } + if (/^import(?:\s*\(|\s*\.)/.test(firstLine)) { + return null; + } + + let raw = ""; + const state = { + braceDepth: 0, + inBlockComment: false, + stringQuote: null, + }; + + for (let index = startIndex; index < lines.length; index += 1) { + raw += lines[index].text; + scanLine(lines[index].text, state); + + if (state.inBlockComment || state.braceDepth !== 0) { + continue; + } + + const analysis = parseImportSemantics(raw); + if (analysis) { + return { + nextIndex: index + 1, + statement: raw, + }; + } + } + + return null; +} + +function scanLine(line, state) { + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + const next = line[index + 1]; + + if (state.inBlockComment) { + if (char === "*" && next === "/") { + state.inBlockComment = false; + index += 1; + } + continue; + } + + if (state.stringQuote) { + if (char === "\\") { + index += 1; + continue; + } + if (char === state.stringQuote) { + state.stringQuote = null; + } + continue; + } + + if (char === "/" && next === "/") { + break; + } + if (char === "/" && next === "*") { + state.inBlockComment = true; + index += 1; + continue; + } + if (char === "'" || char === "\"" || char === "`") { + state.stringQuote = char; + continue; + } + if (char === "{") { + state.braceDepth += 1; + } else if (char === "}") { + state.braceDepth = Math.max(0, state.braceDepth - 1); + } + } +} + +function parseImportSemantics(raw) { + const normalized = collapseWhitespace(raw); + if (!normalized.startsWith("import ")) { + return null; + } + + const sideEffectMatch = normalized.match( + /^import\s+(["'])(.*?)\1(\s+with\s+\{.*\})?\s*;?$/, + ); + if (sideEffectMatch) { + return { + kind: "side-effect", + source: sideEffectMatch[2], + quote: sideEffectMatch[1], + attributes: sideEffectMatch[3] ?? "", + key: `side:${sideEffectMatch[2]}:${sideEffectMatch[3] ?? ""}`, + }; + } + + const bindingMatch = normalized.match( + /^import\s+(type\s+)?(.+?)\s+from\s+(["'])(.*?)\3(\s+with\s+\{.*\})?\s*;?$/, + ); + if (!bindingMatch) { + return null; + } + + const parsed = parseBindingSpec(bindingMatch[2].trim()); + if (!parsed) { + return null; + } + + const named = parsed.namedSpecifiers.join(","); + return { + kind: "binding", + isTypeImport: !!bindingMatch[1], + defaultBinding: parsed.defaultBinding, + namespaceImport: parsed.namespaceImport, + namedSpecifiers: parsed.namedSpecifiers, + source: bindingMatch[4], + quote: bindingMatch[3], + attributes: bindingMatch[5] ?? "", + key: [ + "binding", + bindingMatch[1] ? "type" : "value", + parsed.defaultBinding ?? "", + parsed.namespaceImport ?? "", + named, + bindingMatch[4], + bindingMatch[5] ?? "", + ].join(":"), + }; +} + +function parseBindingSpec(specPart) { + let defaultBinding = null; + let namespaceImport = null; + let namedSpecifiers = []; + + const braceStart = specPart.indexOf("{"); + const braceEnd = specPart.lastIndexOf("}"); + if (braceStart !== -1 || braceEnd !== -1) { + if (braceStart === -1 || braceEnd === -1 || braceEnd < braceStart) { + return null; + } + + const beforeBrace = specPart + .slice(0, braceStart) + .trim() + .replace(/,\s*$/, "") + .trim(); + const afterBrace = specPart.slice(braceEnd + 1).trim(); + if (afterBrace !== "") { + return null; + } + + const head = parseBindingHead(beforeBrace); + if (!head) { + return null; + } + defaultBinding = head.defaultBinding; + namespaceImport = head.namespaceImport; + namedSpecifiers = splitCommaList(specPart.slice(braceStart + 1, braceEnd)); + } else { + const head = parseBindingHead(specPart); + if (!head) { + return null; + } + defaultBinding = head.defaultBinding; + namespaceImport = head.namespaceImport; + } + + return { + defaultBinding, + namespaceImport, + namedSpecifiers, + }; +} + +function parseBindingHead(head) { + if (head === "") { + return { defaultBinding: null, namespaceImport: null }; + } + + if (head.startsWith("* as ")) { + return { defaultBinding: null, namespaceImport: head }; + } + + const parts = head.split(/\s*,\s*/).filter(Boolean); + if (parts.length === 1) { + return { + defaultBinding: parts[0], + namespaceImport: null, + }; + } + + if (parts.length === 2 && parts[1].startsWith("* as ")) { + return { + defaultBinding: parts[0], + namespaceImport: parts[1], + }; + } + + return null; +} + +function splitCommaList(text) { + return text + .split(",") + .map((part) => collapseWhitespace(part)) + .filter(Boolean); +} + +function mergeImportStatements(leftStatements, rightStatements, newline) { + const merged = []; + + for (const statement of [...leftStatements, ...rightStatements]) { + const analysis = parseImportSemantics(statement); + if (!analysis) { + const key = `raw:${collapseWhitespace(statement)}`; + if (!merged.some((entry) => entry.key === key)) { + merged.push({ + key, + raw: ensureTrailingNewline(statement.trim(), newline), + analysis: null, + }); + } + continue; + } + + const exactIndex = merged.findIndex((entry) => entry.key === analysis.key); + if (exactIndex !== -1) { + continue; + } + + const mergeIndex = merged.findIndex( + (entry) => entry.analysis && canMergeImports(entry.analysis, analysis), + ); + if (mergeIndex !== -1) { + const nextAnalysis = mergeImports(merged[mergeIndex].analysis, analysis); + merged[mergeIndex] = { + key: nextAnalysis.key, + raw: renderImport(nextAnalysis, newline), + analysis: nextAnalysis, + }; + continue; + } + + merged.push({ + key: analysis.key, + raw: renderImport(analysis, newline), + analysis, + }); + } + + return merged + .map((entry) => entry.raw.trimEnd()) + .join(newline) + newline; +} + +function canMergeImports(left, right) { + if (left.kind !== "binding" || right.kind !== "binding") { + return false; + } + if ( + left.source !== right.source || + left.attributes !== right.attributes || + left.isTypeImport !== right.isTypeImport + ) { + return false; + } + + const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding); + const namespaceImport = mergeField(left.namespaceImport, right.namespaceImport); + if (defaultBinding === undefined || namespaceImport === undefined) { + return false; + } + + const namedSpecifiers = unique([...left.namedSpecifiers, ...right.namedSpecifiers]); + if (namespaceImport && namedSpecifiers.length > 0) { + return false; + } + + return true; +} + +function mergeImports(left, right) { + const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding); + const namespaceImport = mergeField(left.namespaceImport, right.namespaceImport); + const namedSpecifiers = unique([...left.namedSpecifiers, ...right.namedSpecifiers]); + + return { + kind: "binding", + isTypeImport: left.isTypeImport, + defaultBinding, + namespaceImport, + namedSpecifiers, + source: left.source, + quote: left.quote, + attributes: left.attributes, + key: [ + "binding", + left.isTypeImport ? "type" : "value", + defaultBinding ?? "", + namespaceImport ?? "", + namedSpecifiers.join(","), + left.source, + left.attributes, + ].join(":"), + }; +} + +function mergeField(left, right) { + if (left == null) return right ?? null; + if (right == null) return left; + if (left === right) return left; + return undefined; +} + +function renderImport(analysis, newline) { + if (analysis.kind === "side-effect") { + return ensureTrailingNewline( + `import ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`, + newline, + ); + } + + const pieces = []; + if (analysis.defaultBinding) { + pieces.push(analysis.defaultBinding); + } + if (analysis.namespaceImport) { + pieces.push(analysis.namespaceImport); + } + if (analysis.namedSpecifiers.length > 0) { + if (analysis.namedSpecifiers.length === 1) { + pieces.push(`{ ${analysis.namedSpecifiers[0]} }`); + } else { + pieces.push( + `{${newline}${analysis.namedSpecifiers.map((spec) => ` ${spec},`).join(newline)}${newline}}`, + ); + } + } + + return ensureTrailingNewline( + `import ${analysis.isTypeImport ? "type " : ""}${pieces.join(", ")} from ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`, + newline, + ); +} + +function projectBoundaryToOriginal(parts, targetA, targetB) { + let offsetA = 0; + let offsetB = 0; + let offsetOrig = 0; + + if (targetA === 0 && targetB === 0) { + return 0; + } + + for (const part of parts) { + if (targetA === offsetA && targetB === offsetB) { + return offsetOrig; + } + + const nextA = offsetA + part.a.length; + const nextB = offsetB + part.b.length; + const nextOrig = offsetOrig + part.orig.length; + + if (part.type === "plain") { + if ( + targetA >= offsetA && + targetA <= nextA && + targetB >= offsetB && + targetB <= nextB + ) { + const deltaA = targetA - offsetA; + const deltaB = targetB - offsetB; + if (deltaA !== deltaB) { + return null; + } + return offsetOrig + deltaA; + } + } + + if (targetA === nextA && targetB === nextB) { + return nextOrig; + } + + offsetA = nextA; + offsetB = nextB; + offsetOrig = nextOrig; + } + + if (targetA === offsetA && targetB === offsetB) { + return offsetOrig; + } + + return null; +} + +function hasConflictMarkers(text) { + return CONFLICT_START_RE.test(text) || CONFLICT_END_RE.test(text); +} + +function detectNewline(text) { + return text.includes("\r\n") ? "\r\n" : "\n"; +} + +function ensureTrailingNewline(text, newline) { + return text.endsWith(newline) ? text : `${text}${newline}`; +} + +function offsetForLine(lines, lineIndex) { + if (lineIndex >= lines.length) { + return lines.length === 0 ? 0 : lines[lines.length - 1].end; + } + return lines[lineIndex].start; +} + +function collapseWhitespace(text) { + return text.replace(/\s+/g, " ").trim(); +} + +function unique(values) { + return [...new Set(values)]; +} diff --git a/users/clover/home.nix b/users/clover/home.nix index d587dc0700fc88f4ea885677ed228a6236780788..7e1dcdeefc3d3a7833cdd3c9b329ca798ff2ac41 100644 --- a/users/clover/home.nix +++ b/users/clover/home.nix @@ -12,6 +12,7 @@ in stateVersion = "23.05"; # Please read the comment before changing. sessionPath = [ "$HOME/.vite-plus/bin" + "$HOME/.local/bin" ]; packages = with pkgs; @@ -23,8 +24,9 @@ in uv nh autofmt + pkgs."jj-import-merge" gnupg - nodejs_24 + just ]; # packages to install for desktop environments (non-server) desktop = [ @@ -38,9 +40,6 @@ in ]; in all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop); - sessionPath = [ - "$HOME/.local/bin" - ]; }; programs = { # sort-lines:start @@ -81,7 +80,23 @@ in cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit"; }; initContent = '' - export PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ " + function _update_prompt_theme() { + local dark=false + if [[ "$OSTYPE" == darwin* ]]; then + [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" == "Dark" ]] && dark=true + else + local scheme + scheme=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null) + [[ "$scheme" == *"dark"* ]] && dark=true + fi + if $dark; then + PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ " + else + PS1="%{$(tput setaf 168)%}%m%{$(tput setaf 238)%}:%{$(tput setaf 33)%}%~ %{$(tput sgr0)%}$ " + fi + } + precmd_functions+=(_update_prompt_theme) + _update_prompt_theme function python() { dirname=$(dirname $1 2>/dev/null) @@ -102,14 +117,67 @@ in fi } + function wrapWithAutoInstall() { + root="$(command jj workspace root)" + if [ -n "$root" ]; then + if [ -e "$root/bun.lock" ]; then + file="$root/bun.lock" + cmd="bun i -f" + elif [ -e "$root/package-lock.json" ]; then + file="$root/package-lock.json" + cmd="npm i" + elif [ -e "$root/pnpm-lock.yaml" ]; then + file="$root/pnpm-lock.yaml" + cmd="pnpm i" + elif [ -e "$root/Cargo.lock" ]; then + file="$root/Cargo.lock" + cmd="cargo fetch" + fi + fi + if [ -n "$file" ]; then + before="$(sha1sum "$file")" + fi + command "$@" + if [ -n "$file" ] && [ -e "$file" ]; then + after="$(sha1sum "$file")" + if [ "$after" != "$before" ]; then + eval "$cmd" + fi + fi + } + + function opencode() { + local image="opencode-local" + if ! docker image inspect "$image" &>/dev/null; then + echo "opencode: image not found, building..." >&2 + docker build -t "$image" "$HOME/config/files/opencode" || { echo "opencode: build failed" >&2; return 1; } + fi + docker run --rm -it \ + -v "$(pwd):$(pwd)" \ + -w "$(pwd)" \ + -v "$HOME/.config/opencode:/home/agent/.config/opencode" \ + -v "$HOME/.local/share/opencode:/home/agent/.local/share/opencode" \ + -v "$HOME/.local/state/opencode:/home/agent/.local/state/opencode" \ + -v "$HOME/.config/opencode:$HOME/.config/opencode" \ + -v "$HOME/.local/share/opencode:$HOME/.local/share/opencode" \ + -v "$HOME/.local/state/opencode:$HOME/.local/state/opencode" \ + -v "$HOME/.cache/opencode:$HOME/.cache/opencode" \ + --env-file <(env | grep '^OPENAI_\|^ANTHROPIC_\|^OPENCODE_\|^XDG_') \ + --name "opencode-$(basename $(pwd))-$$" \ + "$image" \ + opencode "$@" + } + function jj() { if [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then - command jj "$@" --dry-run - printf "Press any key to continue... " - read -r - command jj "$@" - else - command jj "$@" + if command jj "$@" --dry-run; then + printf "Press any key to continue... " + read -r + command jj "$@" + fi + else if [[ "$*" != "undo" ]] || [[ "$*" != "redo" ]] + # auto-install deps when switching commits + wrapWithAutoInstall jj "$@" fi } ''; diff --git a/users/clover/paperback/home.nix b/users/clover/paperback/home.nix index 6686bc71ada574880cc557bb0207d2d8cef33c11..26a293cf2031554ebe307f8b3f40f21595508ecd 100644 --- a/users/clover/paperback/home.nix +++ b/users/clover/paperback/home.nix @@ -1,23 +1,12 @@ { pkgs, ... }: { - # these programs are not globally installed to reduce distractions. - # most of these are needed for my work environment. - programs = { - zsh.profileExtra = '' - _bun() { - local context state line - _arguments -C '*:file:_files' - } - ''; - }; home.packages = with pkgs; [ - deno rustup typescript - pm2 pnpm yt-dlp spotdl zig + nodejs_24 ]; } diff --git a/users/clover/sandwich/home.nix b/users/clover/sandwich/home.nix new file mode 100644 index 0000000000000000000000000000000000000000..60fe8437d1309bd834f1e7b0f7852826c4f3285e --- /dev/null +++ b/users/clover/sandwich/home.nix @@ -0,0 +1,7 @@ +{ pkgs, ... }: +{ + + home.packages = with pkgs; [ + nodejs_24 + ]; +} diff --git a/users/clover/work/home.nix b/users/clover/work/home.nix index f9a2d9515898b3a742f55de6574a7ddeb452cc4a..14a2e1138b94fa2dd40cf806bc66e286aca82961 100644 --- a/users/clover/work/home.nix +++ b/users/clover/work/home.nix @@ -8,21 +8,22 @@ } ''; }; - programs.git.signing.key = "CE485807AE4068AF8508C8DC5CBB21841C7920AD"; home.packages = with pkgs; [ (bun.overrideAttrs (_: rec { - version = "1.3.0"; + version = "1.3.5"; src = fetchurl { url = "https://github.com/oven-sh/bun/releases/download/bun-v${version}/bun-darwin-aarch64.zip"; - hash = "sha256-hYSOP5ZIHvyr51pQD9O5S5u5VoaretCjiSl2x74VA2o="; + hash = "sha256-2xdYikrqiASFaCXUvq0/BeHzcnbKYG8342m09y810/s="; }; })) - doppler + awscli deno + doppler + nodejs_25 rustup - typescript ssm-session-manager-plugin - awscli + typescript + zig ]; home.sessionPath = [ "$HOME/.opencode/bin" -- 2.54.0 From 3ca6191b1bb7a67ad542962d501d3ad61c771bc0 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 8 May 2026 15:01:44 -0700 Subject: [PATCH 07/10] clover: ai stuff, please forgive me everyone for falling into the trap --- packages/autofmt/default.nix | 22 +++++++++----------- users/clover/AGENTS.md | 1 + users/clover/home.nix | 40 +++++++++++++----------------------- users/clover/switch-admin.sh | 26 +++++++++++++++++++++++ 4 files changed, 51 insertions(+), 38 deletions(-) create mode 120000 users/clover/AGENTS.md create mode 100755 users/clover/switch-admin.sh diff --git a/packages/autofmt/default.nix b/packages/autofmt/default.nix index 82a0fe2a6a1e1133498d93c2d118e070cb25ee70..64cebfb26f07b232564e8818e4732c0663c0dbd4 100644 --- a/packages/autofmt/default.nix +++ b/packages/autofmt/default.nix @@ -11,17 +11,15 @@ let zig ]; in -pkgs.symlinkJoin { +pkgs.writeTextFile { name = "autofmt"; - paths = [ - (pkgs.writeTextFile { - name = "autofmt"; - destination = "/bin/autofmt"; - executable = true; - text = builtins.replaceStrings - [ "#!/usr/bin/env node" ] - [ "#!${pkgs.nodejs}/bin/node" ] - (builtins.readFile ./autofmt.js); - }) - ] ++ runtimeInputs; + destination = "/bin/autofmt"; + executable = true; + text = builtins.replaceStrings + [ "#!/usr/bin/env node" ] + [ + ''#!${pkgs.nodejs}/bin/node + process.env.PATH = "${pkgs.lib.makeBinPath runtimeInputs}:" + (process.env.PATH ?? "");'' + ] + (builtins.readFile ./autofmt.js); } diff --git a/users/clover/AGENTS.md b/users/clover/AGENTS.md new file mode 120000 index 0000000000000000000000000000000000000000..3c17c141dc2d1847af202b7719ec399f6237c92a --- /dev/null +++ b/users/clover/AGENTS.md @@ -0,0 +1 @@ +../../../.config/opencode/AGENTS.md \ No newline at end of file diff --git a/users/clover/home.nix b/users/clover/home.nix index 7e1dcdeefc3d3a7833cdd3c9b329ca798ff2ac41..24c202b2afc2368abc297926f9ff384894748bad 100644 --- a/users/clover/home.nix +++ b/users/clover/home.nix @@ -146,28 +146,6 @@ in fi } - function opencode() { - local image="opencode-local" - if ! docker image inspect "$image" &>/dev/null; then - echo "opencode: image not found, building..." >&2 - docker build -t "$image" "$HOME/config/files/opencode" || { echo "opencode: build failed" >&2; return 1; } - fi - docker run --rm -it \ - -v "$(pwd):$(pwd)" \ - -w "$(pwd)" \ - -v "$HOME/.config/opencode:/home/agent/.config/opencode" \ - -v "$HOME/.local/share/opencode:/home/agent/.local/share/opencode" \ - -v "$HOME/.local/state/opencode:/home/agent/.local/state/opencode" \ - -v "$HOME/.config/opencode:$HOME/.config/opencode" \ - -v "$HOME/.local/share/opencode:$HOME/.local/share/opencode" \ - -v "$HOME/.local/state/opencode:$HOME/.local/state/opencode" \ - -v "$HOME/.cache/opencode:$HOME/.cache/opencode" \ - --env-file <(env | grep '^OPENAI_\|^ANTHROPIC_\|^OPENCODE_\|^XDG_') \ - --name "opencode-$(basename $(pwd))-$$" \ - "$image" \ - opencode "$@" - } - function jj() { if [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then if command jj "$@" --dry-run; then @@ -185,6 +163,7 @@ in git = { # use a git-specific email + ignores = [ "AGENTS.md" ]; settings.user.email = "git@paperclover.net"; settings.gpg.ssh.allowedSignersFile = "${pkgs.writeText "allowed_signers" '' git@paperclover.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVa1aq1LEj/VXHJYlR5HJpK65lS/9jOL/dAB9ZS1chd @@ -213,15 +192,19 @@ in "working_copy remote_bookmarks" = colors."working_copy bookmark"; "author" = { - fg = "white"; + fg = "bright black"; bold = false; italic = true; }; "working_copy author" = colors.author; + committer = { + fg = "bright black"; + bold = false; + }; desc = { - bold = false; - fg = "bright white"; + bold = true; + fg = "default"; }; description = colors.desc; cc_wip = { @@ -231,7 +214,7 @@ in }; cc_header = { bold = true; - fg = "bright white"; + fg = "default"; }; cc_feat = "green"; cc_fix = "red"; @@ -250,6 +233,10 @@ in bold = true; reverse = true; }; + "change_id shortest rest" = { + fg = "bright black"; + bold = true; + }; conflict_marker = { fg = "bright red"; bold = true; @@ -257,6 +244,7 @@ in }; timestamp = "bright black"; + "committer timestamp" = colors.committer; commit_id = { fg = "bright black"; bold = false; diff --git a/users/clover/switch-admin.sh b/users/clover/switch-admin.sh new file mode 100755 index 0000000000000000000000000000000000000000..5ccb8d5aa54c63541bc4c965b07c8fcb219711da --- /dev/null +++ b/users/clover/switch-admin.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -eu + +cd /Users/clo/config + +target_host="${1:-}" +if [ -z "$target_host" ]; then + if [ -f .hostname ]; then + target_host="$(cat .hostname)" + else + target_host="$(hostname)" + fi +fi + +case "$target_host" in + '' | *[!A-Za-z0-9._-]*) + printf 'invalid host: %s\n' "$target_host" >&2 + exit 2 + ;; +esac + +if [ "$(uname -s)" != Darwin ]; then + exec ./switch "$target_host" +fi + +exec osascript -e "do shell script \"cd /Users/clo/config && /nix/var/nix/profiles/default/bin/nix run .#darwin-rebuild -- switch --flake .#$target_host\" with administrator privileges" -- 2.54.0 From 96faebcbdbd8bac6d597f787bfd02e2f7b0ccf89 Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 14 May 2026 15:52:30 -0700 Subject: [PATCH 08/10] fix: remove unnecessary overlay for vesktop --- flake.nix | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/flake.nix b/flake.nix index 46386565e2b359f5630120d51a68f30d2d343549..1284a2c3a95d402421224251a85c5080747ade08 100644 --- a/flake.nix +++ b/flake.nix @@ -67,24 +67,6 @@ }; }); }) - # https://github.com/NixOS/nixpkgs/issues/484618#issuecomment-3827163603 - (_: prev: { - vesktop = prev.vesktop.overrideAttrs (_: { - buildPhase = '' - runHook preBuild - - pnpm build - pnpm exec electron-builder \ - --dir \ - -c.asarUnpack="**/*.node" \ - -c.electronDist=${if prev.stdenv.hostPlatform.isDarwin then "." else "electron-dist"} \ - -c.electronVersion=${prev.electron.version} \ - ${prev.lib.optionalString prev.stdenv.hostPlatform.isDarwin "-c.mac.identity=null"} - - runHook postBuild - ''; - }); - }) # custom packages (_: pkgs: { -- 2.54.0 From 9ae977f9147e5f09b28d22870d05b3e1f305d616 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Thu, 14 May 2026 22:30:17 -0700 Subject: [PATCH 09/10] terror --- modules/home/default.nix | 1 + users/clover/AGENTS.md | 47 +++++++++++++++++++++++++++++++++++++++- users/clover/home.nix | 3 +++ 3 files changed, 50 insertions(+), 1 deletion(-) mode change 120000 => 100644 users/clover/AGENTS.md diff --git a/modules/home/default.nix b/modules/home/default.nix index fc538513d7e2b2bfb7916c71e2a97e1aa8bcc633..c137f374bdee0de3013b37b5eaaa00da07c061d7 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -149,6 +149,7 @@ in behavior = "drop"; }; git = { + colocate = false; sign-on-push = config.programs.git.signing.key != null; }; remotes.origin = { diff --git a/users/clover/AGENTS.md b/users/clover/AGENTS.md deleted file mode 120000 index 3c17c141dc2d1847af202b7719ec399f6237c92a..0000000000000000000000000000000000000000 --- a/users/clover/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -../../../.config/opencode/AGENTS.md \ No newline at end of file diff --git a/users/clover/AGENTS.md b/users/clover/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..fb19e0f9b690d9071ca7ad198fc5d0f163c44e31 --- /dev/null +++ b/users/clover/AGENTS.md @@ -0,0 +1,46 @@ +# Clover's Preferences + +You are working with Clover, an intelligent web/systems engineer. Please avoid explaining things to her unless she asks and is curious. Her weak points are around writing good database queries and sometimes overcomplicating things. Make sure that she doesn't pull the session into an overcomplicated solution; not to say it's bad to do large reworks -- refactors can be complex but put the code at a simpler end state. You and her should plan strongly. + +## Responses + +- When writing output responses, please keep responses very dense. Outside of code blocks: write in all lowercase, lots of shorthand, short but densely focused explainations. Razor sharp accuracy. It's okay to mimic how Clover speaks to you. + - Terminology: something is "cooked" = bad, someone is "cooking" = good, gotta "lock in" = gotta focus, "locked in" = good, solid, or high quality, high/low "aura" = quality, "wtf" = what the fuck, "bait used to be believable" = when you find something surprising or wrong, that's "based" = good, "this is peak" / "peak design" / etc = good, "nuke" = remove, "blow up" = remove / crash / errored, "goated" = greatest of all time, "my live reaction" = my opinion, "disaster" = mistake, "holy shit" = surprise, "strat" = strategy, "lore" = information/reasoning. + - Use the above terminologies ALL of the time, it's incredible. + - Verbiage that models tend to use; "i'm done" -> "it's peak", "[checking ground truth] instead of just guessing" -> "to make sure i'm not trolling" + - We have fun in our process, but write to the codebase professionally. +- Within `opencode`, everything is monospace, but markdown Headers, Bold, Italic, and inline/block Code blocks highlight different with different colors. Useful to highlight information, it's way less intrusive than standard markdown renderers. +- Never include a section on checks saying "pnpm test passed" or "checks passed" or whatever. Just make sure your validation is strong in the first place; You're expected to have that done. +- Feedback should be useful and actionable. A couple words is OK if there is genuinely nothing to add. + +## Comments + +Comments must always be placed and edited with intent to serialize codebase theory for future versions of ourselves and coworkers. They are professionally written, containing relevant context to the current iteration. Clover's comments are always timeless and rarely written from the first person (no `I`, `we`) -- refer directly to the code. Rewording comments this way helps solidify your own understanding, and can even reveal logic bugs in the designed systems. Prefer documentation comments over line comments. + +## Jujutsu + +- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run + `git`, and be cautious about jujutsu commands. Here are the standard commands. + By default, don't use any other mutating commands. + - `jj st` - show current commit + - `jj log -r @ -T description --no-graph` show the description for the current commit, which is important for review. consider running `jj st && jj log -r ...` to get both views in a single command. + - `jj diff --git`: diff current commit, can take a file/fileset - Drizzle + migrations are quite verbose. In alphaXiv (~/dev/1, ~/dev/2, etc), use + `jj diff '. ~ shared/db/migrations'` to subtract verbose migrations. + - `jj file show -r ` - read a file at a revision, such as + `main` or previous commit `@-` or change ID. + - `jj log` shows a set of commits +- When I say solve all merge conflicts, I mean: + 1. `jj st` to list files alongside the conflicts + 2. Resolve merge conflicts. Either edit manually or you could use + `jj restore --from ` to take one side. + 3. `jj st` to confirm no conflicts + project-specific checks + 4. You should resolve everything in the stack, check `jj log -r "@::"` to spot + future commits in the stack by me, then incrementally edit them with + `jj edit `. Always resolve bottom ones first, as that may + auto-resolve future merge conflicts. +- When I say "rebase ", I mean: + 1. `jj log -r ''`, observe what kind of commit. + 2. `jj git fetch`. If a parent commit was merged as a PR, then the parent will disappear and you'll have to rebase it onto main first or else you'll have incorrect conflicts. + 3. If it looks to be a pushed branch (clo/feature-name), then use `jj new main` to create a merge commit, otherwise just move the commit and its children with `jj rebase -s -d 'trunk()'` and `jj edit `. + 4. Solve merge conflicts if any arise following "solve all merge conflicts" rules. diff --git a/users/clover/home.nix b/users/clover/home.nix index 24c202b2afc2368abc297926f9ff384894748bad..a6fc8a82376aaef174a2d556a500ef175b43c016 100644 --- a/users/clover/home.nix +++ b/users/clover/home.nix @@ -41,6 +41,9 @@ in in all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop); }; + + xdg.configFile."opencode/AGENTS.md".source = ./AGENTS.md; + programs = { # sort-lines:start # bat.enable = true; -- 2.54.0 From cec0cd905ddaec1f0ddce5cbd2c022ca6dc82a4c Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 15 May 2026 14:10:49 -0700 Subject: [PATCH 10/10] terror 2 --- users/clover/AGENTS.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/users/clover/AGENTS.md b/users/clover/AGENTS.md index fb19e0f9b690d9071ca7ad198fc5d0f163c44e31..f3656226b2c41927d98de0484dd98df29191dca8 100644 --- a/users/clover/AGENTS.md +++ b/users/clover/AGENTS.md @@ -5,9 +5,9 @@ You are working with Clover, an intelligent web/systems engineer. Please avoid e ## Responses - When writing output responses, please keep responses very dense. Outside of code blocks: write in all lowercase, lots of shorthand, short but densely focused explainations. Razor sharp accuracy. It's okay to mimic how Clover speaks to you. - - Terminology: something is "cooked" = bad, someone is "cooking" = good, gotta "lock in" = gotta focus, "locked in" = good, solid, or high quality, high/low "aura" = quality, "wtf" = what the fuck, "bait used to be believable" = when you find something surprising or wrong, that's "based" = good, "this is peak" / "peak design" / etc = good, "nuke" = remove, "blow up" = remove / crash / errored, "goated" = greatest of all time, "my live reaction" = my opinion, "disaster" = mistake, "holy shit" = surprise, "strat" = strategy, "lore" = information/reasoning. + - Terminology: something is "cooked" = bad, someone is "cooking" = good, gotta "lock in" = gotta focus, "locked in" = good, solid, or high quality, high/low "aura" = quality, "wtf" = what the fuck, "bait used to be believable" = when you find something surprising or wrong, that's "based" = good, "this is peak" / "peak design" / etc = good, "blow up" = remove / crash / errored, "nuke" = remove a large thing, "goated" = greatest of all time, "my live reaction" = my opinion, "disaster" = mistake, "holy shit" = surprise, "strat" = strategy, "propaganda" = fake information, "it's over" or "it's so over" = bad, "we're so back" = it's better now, "that's wraps" = it's even more over, "chat" = twitch live chat not chatgpt. - Use the above terminologies ALL of the time, it's incredible. - - Verbiage that models tend to use; "i'm done" -> "it's peak", "[checking ground truth] instead of just guessing" -> "to make sure i'm not trolling" + - Verbiage that models tend to use; "[checking ground truth] instead of just guessing" -> "to make sure i'm not trolling" - We have fun in our process, but write to the codebase professionally. - Within `opencode`, everything is monospace, but markdown Headers, Bold, Italic, and inline/block Code blocks highlight different with different colors. Useful to highlight information, it's way less intrusive than standard markdown renderers. - Never include a section on checks saying "pnpm test passed" or "checks passed" or whatever. Just make sure your validation is strong in the first place; You're expected to have that done. @@ -17,6 +17,15 @@ You are working with Clover, an intelligent web/systems engineer. Please avoid e Comments must always be placed and edited with intent to serialize codebase theory for future versions of ourselves and coworkers. They are professionally written, containing relevant context to the current iteration. Clover's comments are always timeless and rarely written from the first person (no `I`, `we`) -- refer directly to the code. Rewording comments this way helps solidify your own understanding, and can even reveal logic bugs in the designed systems. Prefer documentation comments over line comments. +## Code Quality + +- Prefer fewer moving parts. Do not introduce helpers, methods, state fields, abstractions, or types unless they remove real duplication or encode a meaningful invariant. If a helper has one caller, inline it unless it makes a tricky operation substantially safer. +- Actively look for dead code and fake state while editing. If a value is always derived from another source, expose it as derived behavior instead of storing it. If a field is only written for type appeasement and never read, nuke it. +- Be suspicious of helper functions that hide one line of logic, especially wrappers around another object's property, trivial context builders, and "just in case" extension points. These are low aura unless they prevent a concrete bug. +- Before calling a change done, do one cleanup pass asking: can this be fewer names, fewer branches, fewer casts, fewer helper functions, or fewer public API promises? Prefer the smaller locked-in version. +- Type safety should constrain real runtime behavior. Avoid casts that paper over a mismatch; if a cast remains, it should be localized and justified by an actual API boundary. +- It is okay to spend more time to have a more correct response. + ## Jujutsu - My repos use Jujutsu and usually don't have a visible `.git` folder. Never run -- 2.54.0