-
Notifications
You must be signed in to change notification settings - Fork 154
Expand file tree
/
Copy pathflake.nix
More file actions
944 lines (835 loc) · 36.4 KB
/
flake.nix
File metadata and controls
944 lines (835 loc) · 36.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
{
description = "git-ai - AI-powered Git tracking and intelligence for code repositories";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
# Pin Rust 1.93.0 via rust-overlay
rustToolchain = pkgs.rust-bin.stable."1.93.0".default.override {
extensions = [
"rust-src"
"rust-analyzer"
"llvm-tools-preview"
];
};
# Create a custom rustPlatform using the pinned toolchain
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
# Build the git-ai binary using the pinned Rust toolchain
git-ai-unwrapped = rustPlatform.buildRustPackage {
pname = "git-ai";
version = "1.3.3";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
};
# Prevent openssl-sys from vendoring OpenSSL (which requires perl).
# Instead, link against the system OpenSSL provided by buildInputs.
OPENSSL_NO_VENDOR = "1";
# Native build inputs needed for rusqlite with bundled SQLite
nativeBuildInputs = with pkgs; [
pkg-config
] ++ [
rustPlatform.bindgenHook # For rusqlite bundled builds
];
# Build inputs for runtime dependencies
buildInputs = with pkgs; [
# rusqlite bundled mode compiles its own SQLite, but needs these headers
sqlite
# openssl-sys needs system OpenSSL headers and libraries
openssl
] ++ lib.optionals stdenv.hostPlatform.isDarwin [
# macOS-specific dependencies
libiconv
apple-sdk_15
];
# Tests require git and specific setup
doCheck = false;
meta = with pkgs.lib; {
description = "AI-powered Git wrapper that tracks AI-generated code changes";
homepage = "https://github.com/acunniffe/git-ai";
license = licenses.gpl3Plus;
maintainers = [ ];
mainProgram = "git-ai";
platforms = platforms.unix;
};
};
# Wrapped version that sets up the git-ai environment properly
git-ai-wrapped = pkgs.writeShellScriptBin "git-ai" ''
# Ensure config directory exists
mkdir -p "$HOME/.git-ai"
# Create config.json if it doesn't exist
if [ ! -f "$HOME/.git-ai/config.json" ]; then
# Find the system git (not our wrapper)
GIT_PATH="${pkgs.git}/bin/git"
cat > "$HOME/.git-ai/config.json" <<EOF
{
"git_path": "$GIT_PATH"
}
EOF
fi
# Execute git-ai with all arguments
exec ${git-ai-unwrapped}/bin/git-ai "$@"
'';
# Wrapper for git command that preserves argv[0] as "git"
# This is critical: when symlinked as "git", the wrapper must set argv[0]
# to "git" so the Rust binary routes to handle_git() instead of handle_git_ai()
git-wrapper = pkgs.writeShellScriptBin "git" ''
# Ensure config directory exists
mkdir -p "$HOME/.git-ai"
# Create config.json if it doesn't exist
if [ ! -f "$HOME/.git-ai/config.json" ]; then
# Find the system git (not our wrapper)
GIT_PATH="${pkgs.git}/bin/git"
cat > "$HOME/.git-ai/config.json" <<EOF
{
"git_path": "$GIT_PATH"
}
EOF
fi
# Execute git-ai with argv[0] set to "git" to trigger passthrough mode
# The -a flag ensures argv[0] is "git" regardless of the actual binary path
exec -a git ${git-ai-unwrapped}/bin/git-ai "$@"
'';
# Create git-og wrapper that bypasses git-ai and calls real git directly
# This is needed because git interprets argv[0] as a subcommand
git-og = pkgs.writeShellScriptBin "git-og" ''
exec ${pkgs.git}/bin/git "$@"
'';
# Package without git wrapper - for Home Manager / environments with existing git
git-ai-minimal = pkgs.symlinkJoin {
name = "git-ai-minimal-${git-ai-unwrapped.version}";
paths = [ git-ai-wrapped git-ai-unwrapped git-og ];
# Create libexec symlink for Fork compatibility
# Fork looks for libexec relative to the git binary location
postBuild = ''
ln -s ${pkgs.git}/libexec $out/libexec
'';
meta = git-ai-unwrapped.meta // {
description = git-ai-unwrapped.meta.description + " (without git wrapper)";
};
};
# Create a complete package with git wrapper (for standalone use)
# The git-wrapper script ensures argv[0] is "git" when invoked as git
git-ai-package = pkgs.symlinkJoin {
name = "git-ai-${git-ai-unwrapped.version}";
paths = [ git-ai-wrapped git-wrapper git-ai-unwrapped git-og ];
# Create libexec symlink for Fork compatibility
# Fork looks for libexec relative to the git binary location
postBuild = ''
ln -s ${pkgs.git}/libexec $out/libexec
'';
meta = git-ai-unwrapped.meta // {
description = git-ai-unwrapped.meta.description + " (with git wrapper)";
};
};
in
{
# Development shell with full Rust toolchain
devShells.default = pkgs.mkShell {
packages = [
# Pinned Rust 1.93.0 toolchain (includes rustc, cargo, clippy, rustfmt, rust-analyzer)
rustToolchain
] ++ (with pkgs; [
# Build dependencies
pkg-config
# Runtime dependencies for testing
# NOTE: git is NOT included as a package here. Instead, the
# shellHook creates wrapper scripts (git, git-ai, git-og) that
# point to the locally-built target/debug/git-ai binary, so that
# development builds are tested directly. Use `git-og` to bypass
# git-ai and call real git.
sqlite
# Useful development tools
cargo-edit # cargo add, cargo rm, cargo upgrade
cargo-watch # Auto-rebuild on file changes
cargo-expand # Show macro expansions
cargo-llvm-cov # Code coverage via LLVM instrumentation
lefthook # Git hooks manager
go-task # Task runner (Taskfile.yml)
] ++ lib.optionals stdenv.hostPlatform.isDarwin [
libiconv
apple-sdk_15
]);
# Environment variables for development
shellHook = ''
# Unset DEVELOPER_DIR to avoid conflict between the default stdenv
# SDK (14.4) and apple-sdk_15 (15.5) baked into the clang wrapper.
unset DEVELOPER_DIR
# Set up development git-ai wrappers for nix develop (Nix-specific; non-Nix devs use scripts/dev.sh)
BUILD_TYPE="''${GIT_AI_BUILD_TYPE:-debug}"
GITWRAP_DIR="$HOME/.git-ai-local-dev/gitwrap/bin"
TARGET_DIR="''${CARGO_TARGET_DIR:-$(pwd)/target}"
BINARY="$TARGET_DIR/$BUILD_TYPE/git-ai"
mkdir -p "$GITWRAP_DIR"
# Create git wrapper (preserves argv[0] as "git" for passthrough mode)
cat > "$GITWRAP_DIR/git" <<GITEOF
#!/bin/bash
if [ ! -x "$BINARY" ]; then
echo "git-ai: dev binary not found at $BINARY" >&2
echo "Run 'cargo build' first, then retry." >&2
exit 1
fi
exec -a git "$BINARY" "\$@"
GITEOF
chmod +x "$GITWRAP_DIR/git"
# Create git-ai wrapper
cat > "$GITWRAP_DIR/git-ai" <<GITAIEOF
#!/bin/bash
if [ ! -x "$BINARY" ]; then
echo "git-ai: dev binary not found at $BINARY" >&2
echo "Run 'cargo build' first, then retry." >&2
exit 1
fi
exec "$BINARY" "\$@"
GITAIEOF
chmod +x "$GITWRAP_DIR/git-ai"
# Create git-og wrapper (bypasses git-ai, calls real git directly)
cat > "$GITWRAP_DIR/git-og" <<GITOGEOF
#!/bin/bash
exec ${pkgs.git}/bin/git "\$@"
GITOGEOF
chmod +x "$GITWRAP_DIR/git-og"
export PATH="$GITWRAP_DIR:$PATH"
# Install hooks if binary is already built
if [ -x "$BINARY" ]; then
"$GITWRAP_DIR/git-ai" install-hooks 2>/dev/null || true
fi
# Install lefthook git hooks (use real git, not the git-ai wrapper,
# since the dev binary may not be built yet)
PATH="${pkgs.git}/bin:$PATH" lefthook install 2>/dev/null || true
# Set up environment for development
export RUST_BACKTRACE=1
export RUST_LOG=debug
echo "git-ai development environment"
echo "Rust version: $(rustc --version)"
echo "Cargo version: $(cargo --version)"
echo ""
if [ -x "$BINARY" ]; then
echo "Dev binary: $BINARY (ready)"
echo "Hooks installed."
else
echo "Dev binary: $BINARY (not built yet)"
echo "Run 'cargo build' to build, then hooks will be installed on next 'nix develop'."
fi
echo ""
echo "git, git-ai, git-og -> wrappers in $GITWRAP_DIR"
echo "Set GIT_AI_BUILD_TYPE=release for release builds."
'';
};
# Main packages
packages = {
# Unwrapped binary (just the git-ai executable)
unwrapped = git-ai-unwrapped;
# Wrapped version with helper scripts
wrapped = git-ai-wrapped;
# Minimal package without git symlink (for Home Manager/environments with existing git)
minimal = git-ai-minimal;
# Complete package with git/git-og symlinks (for standalone use)
default = git-ai-package;
# Alias for clarity
git-ai = git-ai-package;
};
# Make app available for `nix run`
apps.default = flake-utils.lib.mkApp {
drv = git-ai-package;
exePath = "/bin/git-ai";
};
# Nix flake checks: run with `nix flake check`
# Tests are not included here -- they require network access, Node.js,
# and the Graphite CLI, which are not available in the Nix sandbox.
# Tests run in CI via the existing test.yml workflow instead.
checks =
let
commonNativeBuildInputs = with pkgs; [ pkg-config ]
++ [ rustPlatform.bindgenHook ];
commonBuildInputs = with pkgs; [ sqlite openssl ]
++ lib.optionals stdenv.hostPlatform.isDarwin [
libiconv apple-sdk_15
];
mkCheck = attrs: rustPlatform.buildRustPackage ({
version = git-ai-unwrapped.version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
OPENSSL_NO_VENDOR = "1";
nativeBuildInputs = commonNativeBuildInputs;
buildInputs = commonBuildInputs;
installPhase = "mkdir -p $out";
doCheck = false;
} // attrs);
in
{
# Build check - ensures the package builds
build = git-ai-unwrapped;
# Clippy lint check with warnings as errors
clippy = mkCheck {
pname = "git-ai-clippy";
buildPhase = ''
cargo clippy --all-targets -- -D warnings
'';
};
# Format check
fmt = mkCheck {
pname = "git-ai-fmt";
buildPhase = ''
cargo fmt -- --check
'';
};
# Doc check with warnings as errors
doc = mkCheck {
pname = "git-ai-doc";
RUSTDOCFLAGS = "-D warnings";
buildPhase = ''
cargo doc --no-deps
'';
};
};
# Formatter for `nix fmt`
formatter = pkgs.nixpkgs-fmt;
}
) // {
# System-independent outputs
# Overlay for importing into other flakes
overlays.default = final: prev: {
git-ai = self.packages.${prev.stdenv.hostPlatform.system}.default;
git-ai-unwrapped = self.packages.${prev.stdenv.hostPlatform.system}.unwrapped;
};
# NixOS module for system integration
nixosModules.default = { config, lib, pkgs, ... }:
with lib;
let
cfg = config.programs.git-ai;
jsonFormat = pkgs.formats.json { };
# Build the config object, filtering out null values
configFile = filterAttrs (n: v: v != null) {
git_path =
if cfg.settings.gitPath != null
then cfg.settings.gitPath
else "${pkgs.git}/bin/git";
prompt_storage = cfg.settings.promptStorage;
api_base_url = cfg.settings.apiBaseUrl;
exclude_prompts_in_repositories = cfg.settings.excludePromptsInRepositories;
include_prompts_in_repositories = cfg.settings.includePromptsInRepositories;
default_prompt_storage = cfg.settings.defaultPromptStorage;
allow_repositories = cfg.settings.allowRepositories;
exclude_repositories = cfg.settings.excludeRepositories;
telemetry_oss = cfg.settings.telemetryOss;
telemetry_enterprise_dsn = cfg.settings.telemetryEnterpriseDsn;
disable_version_checks = cfg.settings.disableVersionChecks;
disable_auto_updates = cfg.settings.disableAutoUpdates;
update_channel = cfg.settings.updateChannel;
feature_flags =
let
knownFlags = filterAttrs (n: v: v != null) {
async_mode = cfg.settings.featureFlags.asyncMode;
rewrite_stash = cfg.settings.featureFlags.rewriteStash;
checkpoint_inter_commit_move = cfg.settings.featureFlags.interCommitMove;
auth_keyring = cfg.settings.featureFlags.authKeyring;
git_hooks_enabled = cfg.settings.featureFlags.gitHooksEnabled;
git_hooks_externally_managed = cfg.settings.featureFlags.gitHooksExternallyManaged;
};
merged = cfg.settings.featureFlags.extraFlags // knownFlags;
in
if merged != { } then merged else null;
};
# Generate the config file in the Nix store
configJsonFile = jsonFormat.generate "git-ai-config.json" configFile;
in
{
options.programs.git-ai = {
enable = mkEnableOption "git-ai - AI-powered Git tracking";
package = mkOption {
type = types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
defaultText = literalExpression "inputs.git-ai.packages.\${pkgs.system}.default";
description = "The git-ai package to use.";
};
installHooks = mkOption {
type = types.bool;
default = true;
description = ''
Whether to run 'git-ai install-hooks' on system activation.
This sets up IDE and agent integration hooks.
'';
};
setGitAlias = mkOption {
type = types.bool;
default = true;
description = ''
Whether to make 'git' command use git-ai wrapper.
When enabled, git-ai is placed before regular git in PATH.
The original git is still accessible via 'git-og'.
'';
};
settings = {
gitPath = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Path to the git binary. If not specified, defaults to the
git package from nixpkgs.
'';
};
promptStorage = mkOption {
type = types.nullOr (types.enum [ "default" "notes" "local" ]);
default = null;
description = ''
Prompt storage mode:
- "default": Messages uploaded via CAS API
- "notes": Messages stored in git notes
- "local": Messages only stored in sqlite (not in notes, not uploaded)
'';
};
apiBaseUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
API base URL for git-ai services.
Defaults to "https://usegitai.com" if not specified.
'';
};
excludePromptsInRepositories = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "https://github.com/private/*" "*" ];
description = ''
List of repository URL patterns (globs) to exclude from prompt sharing.
Use "*" to exclude all repositories. Exclusions take precedence over inclusions.
Patterns are matched against remote URLs (HTTPS or SSH format).
'';
};
includePromptsInRepositories = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "https://github.com/myorg/*" "*github.com*positron*" ];
description = ''
List of repository URL patterns (globs) for which promptStorage mode applies.
Repositories not matching these patterns use defaultPromptStorage instead.
If empty or null, promptStorage applies to all repositories (legacy behavior).
Patterns are matched against remote URLs (HTTPS or SSH format).
'';
};
defaultPromptStorage = mkOption {
type = types.nullOr (types.enum [ "default" "notes" "local" ]);
default = null;
description = ''
Fallback prompt storage mode for repositories NOT matching includePromptsInRepositories.
If not specified, defaults to "local" (safest option - prompts stay local only).
Use this with includePromptsInRepositories to have different storage modes for
work repos vs personal repos.
'';
};
allowRepositories = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "https://github.com/myorg/*" ];
description = ''
List of repository URL patterns (globs) to allow.
If empty or null, all repositories are allowed (unless excluded).
'';
};
excludeRepositories = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "https://github.com/private/*" ];
description = ''
List of repository URL patterns (globs) to exclude from git-ai tracking.
Exclusions take precedence over allow list.
'';
};
telemetryOss = mkOption {
type = types.nullOr (types.enum [ "on" "off" ]);
default = null;
description = ''
OSS telemetry setting. Set to "off" to disable telemetry.
'';
};
telemetryEnterpriseDsn = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Enterprise telemetry DSN for custom telemetry endpoints.
'';
};
disableVersionChecks = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Whether to disable version checks.
'';
};
disableAutoUpdates = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Whether to disable automatic updates.
'';
};
updateChannel = mkOption {
type = types.nullOr (types.enum [
"latest" "next" "enterprise-latest" "enterprise-next"
]);
default = null;
description = ''
Update channel: "latest" for stable releases, "next" for
pre-releases, "enterprise-latest" and "enterprise-next" for
enterprise deployments.
'';
};
featureFlags = {
asyncMode = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable async daemon mode for background processing.
When enabled, git operations are processed asynchronously
through a background daemon, improving performance for IDE
and agent workflows.
Equivalent to: git ai config set feature_flags.async_mode true
'';
};
rewriteStash = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable stash rewriting for improved AI tracking of stash
operations.
'';
};
interCommitMove = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable checkpoint inter-commit move tracking.
'';
};
authKeyring = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable system keyring integration for authentication.
'';
};
gitHooksEnabled = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable git hooks integration for git-ai tracking.
'';
};
gitHooksExternallyManaged = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Indicate that git hooks are managed externally
(e.g., by lefthook or husky). When enabled, git-ai will not
attempt to install or manage git hooks itself.
'';
};
extraFlags = mkOption {
type = types.attrsOf types.bool;
default = { };
description = ''
Additional feature flags not explicitly defined above.
Keys should use snake_case to match the config.json format.
'';
};
};
};
};
config = mkIf cfg.enable {
# Add git-ai to system packages
environment.systemPackages = [ cfg.package ];
# Set up system-wide configuration on activation
system.activationScripts.git-ai = mkIf cfg.installHooks (
stringAfter [ "users" ] ''
# Run install-hooks for all users with home directories
for user_home in /home/* /Users/* /root; do
if [ -d "$user_home" ]; then
user=$(basename "$user_home")
# Create config directory
# Create config directory
mkdir -p "$user_home/.git-ai"
chown "$user" "$user_home/.git-ai" 2>/dev/null || true
# Copy config.json from store (allows user to override later if needed)
# Only copy if the file doesn't exist or is a symlink (from previous Nix activation)
if [ ! -f "$user_home/.git-ai/config.json" ] || [ -L "$user_home/.git-ai/config.json" ]; then
cp -f ${configJsonFile} "$user_home/.git-ai/config.json"
chmod 644 "$user_home/.git-ai/config.json"
chown "$user" "$user_home/.git-ai/config.json" 2>/dev/null || true
fi
# Install hooks (run as user if possible)
if command -v sudo >/dev/null 2>&1 && [ "$user" != "root" ]; then
sudo -u "$user" ${cfg.package}/bin/git-ai install-hooks 2>/dev/null || true
else
${cfg.package}/bin/git-ai install-hooks 2>/dev/null || true
fi
fi
done
''
);
};
};
# Home Manager module for user-level configuration
homeManagerModules.default = { config, lib, pkgs, ... }:
with lib;
let
cfg = config.programs.git-ai;
jsonFormat = pkgs.formats.json { };
# Build the config object, filtering out null values
# We use explicit null checks since Nix 'or' only works for attribute access
configFile = filterAttrs (n: v: v != null) {
git_path =
if cfg.settings.gitPath != null
then cfg.settings.gitPath
else "${pkgs.git}/bin/git";
prompt_storage = cfg.settings.promptStorage;
api_base_url = cfg.settings.apiBaseUrl;
exclude_prompts_in_repositories = cfg.settings.excludePromptsInRepositories;
include_prompts_in_repositories = cfg.settings.includePromptsInRepositories;
default_prompt_storage = cfg.settings.defaultPromptStorage;
allow_repositories = cfg.settings.allowRepositories;
exclude_repositories = cfg.settings.excludeRepositories;
telemetry_oss = cfg.settings.telemetryOss;
telemetry_enterprise_dsn = cfg.settings.telemetryEnterpriseDsn;
disable_version_checks = cfg.settings.disableVersionChecks;
disable_auto_updates = cfg.settings.disableAutoUpdates;
update_channel = cfg.settings.updateChannel;
feature_flags =
let
knownFlags = filterAttrs (n: v: v != null) {
async_mode = cfg.settings.featureFlags.asyncMode;
rewrite_stash = cfg.settings.featureFlags.rewriteStash;
checkpoint_inter_commit_move = cfg.settings.featureFlags.interCommitMove;
auth_keyring = cfg.settings.featureFlags.authKeyring;
git_hooks_enabled = cfg.settings.featureFlags.gitHooksEnabled;
git_hooks_externally_managed = cfg.settings.featureFlags.gitHooksExternallyManaged;
};
merged = cfg.settings.featureFlags.extraFlags // knownFlags;
in
if merged != { } then merged else null;
};
in
{
options.programs.git-ai = {
enable = mkEnableOption "git-ai - AI-powered Git tracking";
package = mkOption {
type = types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
defaultText = literalExpression "inputs.git-ai.packages.\${pkgs.system}.default";
description = "The git-ai package to use.";
};
installHooks = mkOption {
type = types.bool;
default = true;
description = ''
Whether to run 'git-ai install-hooks' on activation.
This sets up IDE and agent integration hooks.
'';
};
settings = {
gitPath = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Path to the git binary. If not specified, defaults to the
git package from nixpkgs.
'';
};
promptStorage = mkOption {
type = types.nullOr (types.enum [ "default" "notes" "local" ]);
default = null;
description = ''
Prompt storage mode:
- "default": Messages uploaded via CAS API
- "notes": Messages stored in git notes
- "local": Messages only stored in sqlite (not in notes, not uploaded)
'';
};
apiBaseUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
API base URL for git-ai services.
Defaults to "https://usegitai.com" if not specified.
'';
};
excludePromptsInRepositories = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "https://github.com/private/*" "*" ];
description = ''
List of repository URL patterns (globs) to exclude from prompt sharing.
Use "*" to exclude all repositories. Exclusions take precedence over inclusions.
Patterns are matched against remote URLs (HTTPS or SSH format).
'';
};
includePromptsInRepositories = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "https://github.com/myorg/*" "*github.com*positron*" ];
description = ''
List of repository URL patterns (globs) for which promptStorage mode applies.
Repositories not matching these patterns use defaultPromptStorage instead.
If empty or null, promptStorage applies to all repositories (legacy behavior).
Patterns are matched against remote URLs (HTTPS or SSH format).
'';
};
defaultPromptStorage = mkOption {
type = types.nullOr (types.enum [ "default" "notes" "local" ]);
default = null;
description = ''
Fallback prompt storage mode for repositories NOT matching includePromptsInRepositories.
If not specified, defaults to "local" (safest option - prompts stay local only).
Use this with includePromptsInRepositories to have different storage modes for
work repos vs personal repos.
'';
};
allowRepositories = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "https://github.com/myorg/*" ];
description = ''
List of repository URL patterns (globs) to allow.
If empty or null, all repositories are allowed (unless excluded).
'';
};
excludeRepositories = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "https://github.com/private/*" ];
description = ''
List of repository URL patterns (globs) to exclude from git-ai tracking.
Exclusions take precedence over allow list.
'';
};
telemetryOss = mkOption {
type = types.nullOr (types.enum [ "on" "off" ]);
default = null;
description = ''
OSS telemetry setting. Set to "off" to disable telemetry.
'';
};
telemetryEnterpriseDsn = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Enterprise telemetry DSN for custom telemetry endpoints.
'';
};
disableVersionChecks = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Whether to disable version checks.
'';
};
disableAutoUpdates = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Whether to disable automatic updates.
'';
};
updateChannel = mkOption {
type = types.nullOr (types.enum [
"latest" "next" "enterprise-latest" "enterprise-next"
]);
default = null;
description = ''
Update channel: "latest" for stable releases, "next" for
pre-releases, "enterprise-latest" and "enterprise-next" for
enterprise deployments.
'';
};
featureFlags = {
asyncMode = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable async daemon mode for background processing.
When enabled, git operations are processed asynchronously
through a background daemon, improving performance for IDE
and agent workflows.
Equivalent to: git ai config set feature_flags.async_mode true
'';
};
rewriteStash = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable stash rewriting for improved AI tracking of stash
operations.
'';
};
interCommitMove = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable checkpoint inter-commit move tracking.
'';
};
authKeyring = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable system keyring integration for authentication.
'';
};
gitHooksEnabled = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Enable git hooks integration for git-ai tracking.
'';
};
gitHooksExternallyManaged = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Indicate that git hooks are managed externally
(e.g., by lefthook or husky). When enabled, git-ai will not
attempt to install or manage git hooks itself.
'';
};
extraFlags = mkOption {
type = types.attrsOf types.bool;
default = { };
description = ''
Additional feature flags not explicitly defined above.
Keys should use snake_case to match the config.json format.
'';
};
};
};
};
config = mkIf cfg.enable {
# Add git-ai to user packages
home.packages = [ cfg.package ];
# Create config directory and file
home.file.".git-ai/config.json" = {
source = jsonFormat.generate "git-ai-config.json" configFile;
};
# Run install-hooks on activation
home.activation.git-ai-install-hooks = mkIf cfg.installHooks (
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
$DRY_RUN_CMD ${cfg.package}/bin/git-ai install-hooks || true
''
);
};
};
};
}