74510ec9f7dd
config: use XDG_CONFIG_HOME (~/.config/nina/config) with auto-migration — configPath resolves $XDG_CONFIG_HOME/nina/config then ~/.config/nina/config; load() migrates legacy ~/.nina.conf (copy+delete, dim note) idempotently; firstRunWizard+setup write to XDG path and mkdir -p the parent; exec.ensureDir helper added — build+tests green, migration/XDG/idempotency verified on temp HOME
- time
- 2026-06-10 01:06 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- nina community improvements
- parent
- d55887bd29e5
↓ nina-74510ec9.tar.gzatlas at this save
files changed4 files
src/commands.zig+15 −19src/config.zig+56 −2src/exec.zig+6 −0src/output.zig+5 −0src/commands.zig+15 −19
@@ -1101,11 +1101,6 @@11011101 // Runs when ~/.nina.conf is absent on first invocation (interactive TTY only).11021102 // Prompts for machine name, config path, and local flag, then writes the file.11031103 pub fn firstRunWizard(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1104- const home = environ.get("HOME") orelse {1105- errors.error_info.setSuggestion("set HOME environment variable", .{});1106- return error.ConfigNotFound;1107- };1108-11091104 output.firstRunBanner();11101105 output.firstRunNoConfig();11111106 @@ -1117,8 +1112,9 @@11171112 const config_path = output.firstRunPrompt("config path", "/etc/nixos", &path_buf);11181113 const local_str = output.firstRunPrompt("local", "true", &local_buf);11191114 1120- const conf_path = try std.fmt.allocPrint(gpa, "{s}/.nina.conf", .{home});1115+ const conf_path = try config.configPath(gpa, environ);11211116 defer gpa.free(conf_path);1117+ if (std.fs.path.dirname(conf_path)) |dir| exec.ensureDir(gpa, io, dir);11221118 11231119 var content_buf: [1024]u8 = undefined;11241120 const content = try std.fmt.bufPrint(&content_buf,@@ -1143,19 +1139,19 @@11431139 }11441140 11451141 pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1146- // Create ~/.nina.conf first if it doesn't exist. This makes `nina setup`1147- // a complete onboarding command — not just a nix-features check.1148- if (environ.get("HOME")) |home| {1149- var path_buf: [512]u8 = undefined;1150- if (std.fmt.bufPrint(&path_buf, "{s}/.nina.conf", .{home})) |conf_path| {1151- if (std.Io.Dir.openFile(.cwd(), io, conf_path, .{})) |f| {1152- f.close(io);1153- } else |_| {1154- try firstRunWizard(gpa, io, environ);1155- errors.error_info.reset();1156- }1157- } else |_| {}1158- }1142+ // Create the config first if it doesn't exist. This makes `nina setup`1143+ // a complete onboarding command — not just a nix-features check. Any legacy1144+ // ~/.nina.conf is migrated to the XDG path before we decide to run the wizard.1145+ config.migrateLegacyConfig(gpa, io, environ);1146+ if (config.configPath(gpa, environ)) |conf_path| {1147+ defer gpa.free(conf_path);1148+ if (std.Io.Dir.openFile(.cwd(), io, conf_path, .{})) |f| {1149+ f.close(io);1150+ } else |_| {1151+ try firstRunWizard(gpa, io, environ);1152+ errors.error_info.reset();1153+ }1154+ } else |_| {}11591155 11601156 output.setupCheck();11611157 const status = exec.checkNixFeatures(gpa, io, environ);src/config.zig+56 −2
@@ -1,14 +1,68 @@11 const std = @import("std");22 const types = @import("types.zig");33 const errors = @import("errors.zig");4+const exec = @import("exec.zig");5+const output = @import("output.zig");46 5-pub fn load(arena: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !types.NinaConfig {7+// Resolve the config file path: $XDG_CONFIG_HOME/nina/config, falling back to8+// ~/.config/nina/config. nina historically stored ~/.nina.conf; that legacy9+// location is auto-migrated to this path on load (see migrateLegacyConfig).10+pub fn configPath(arena: std.mem.Allocator, environ: *const std.process.Environ.Map) ![]const u8 {11+ if (environ.get("XDG_CONFIG_HOME")) |xdg| {12+ if (xdg.len > 0) return std.fs.path.join(arena, &.{ xdg, "nina", "config" });13+ }614 const home = environ.get("HOME") orelse {715 errors.error_info.setSuggestion("set HOME environment variable", .{});816 return error.ConfigNotFound;917 };18+ return std.fs.path.join(arena, &.{ home, ".config", "nina", "config" });19+}1020 11- const path = try std.fmt.allocPrint(arena, "{s}/.nina.conf", .{home});21+fn legacyPath(arena: std.mem.Allocator, environ: *const std.process.Environ.Map) ?[]const u8 {22+ const home = environ.get("HOME") orelse return null;23+ return std.fmt.allocPrint(arena, "{s}/.nina.conf", .{home}) catch null;24+}25+26+fn fileExists(io: std.Io, path: []const u8) bool {27+ const f = std.Io.Dir.openFile(.cwd(), io, path, .{}) catch return false;28+ f.close(io);29+ return true;30+}31+32+// One-time migration of the legacy ~/.nina.conf to the XDG path. Runs only when33+// the old file exists and the new one does not, so it's idempotent and a no-op34+// for fresh installs. Best-effort: any failure leaves the legacy file in place35+// so the next load still finds it.36+pub fn migrateLegacyConfig(arena: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) void {37+ const new_path = configPath(arena, environ) catch return;38+ if (fileExists(io, new_path)) return;39+ const legacy = legacyPath(arena, environ) orelse return;40+ const content = std.Io.Dir.readFileAlloc(.cwd(), io, legacy, arena, .unlimited) catch return;41+42+ if (std.fs.path.dirname(new_path)) |dir| exec.ensureDir(arena, io, dir);43+ const f = std.Io.Dir.createFile(.cwd(), io, new_path, .{}) catch return;44+ f.writePositionalAll(io, content, 0) catch {45+ f.close(io);46+ return;47+ };48+ f.close(io);49+ std.Io.Dir.deleteFile(.cwd(), io, legacy) catch {};50+ output.configMigrated(homeShort(arena, new_path, environ));51+}52+53+// Collapse a leading $HOME to ~ for display only. Returns the input unchanged on54+// any mismatch or allocation failure.55+fn homeShort(arena: std.mem.Allocator, path: []const u8, environ: *const std.process.Environ.Map) []const u8 {56+ const home = environ.get("HOME") orelse return path;57+ if (home.len == 0 or !std.mem.startsWith(u8, path, home)) return path;58+ return std.fmt.allocPrint(arena, "~{s}", .{path[home.len..]}) catch path;59+}60+61+pub fn load(arena: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !types.NinaConfig {62+ migrateLegacyConfig(arena, io, environ);63+64+ const path = try configPath(arena, environ);65+ const home = environ.get("HOME") orelse "";1266 1367 const content = std.Io.Dir.readFileAlloc(.cwd(), io, path, arena, .unlimited) catch {1468 errors.error_info.setSuggestion("run nina to set up", .{});src/exec.zig+6 −0
@@ -136,6 +136,12 @@136136 return result.stdout;137137 }138138 139+// Create a directory and its parents, best-effort. Shelling out to `mkdir -p`140+// matches how the rest of exec.zig provisions config locations (~/.config/nix).141+pub fn ensureDir(gpa: std.mem.Allocator, io: std.Io, dir: []const u8) void {142+ _ = captureLocal(gpa, io, &.{ "mkdir", "-p", dir }) catch {};143+}144+139145 fn buildArgv(gpa: std.mem.Allocator, machine: *const types.Machine, cmd: []const []const u8) ![]const []const u8 {140146 if (machine.local) return cmd;141147 src/output.zig+5 −0
@@ -133,6 +133,11 @@133133 p("{s}{s}\n", .{ if (color_enabled) WARN_C else WARN_P, message });134134 }135135 136+// One-time dim note when ~/.nina.conf is migrated to the XDG config path.137+pub fn configMigrated(path: []const u8) void {138+ p("{s}{s}config migrated to {s}{s}\n", .{ step(), c(DIM), path, c(RESET) });139+}140+136141 pub fn printTeach(cmd: []const u8) void {137142 if (teach_enabled) p("{s}{s}{s}\n", .{ teach_pfx(), cmd, c(RESET) });138143 }