e66b7c22892f
auto-run setup on first run when ~/.nina.conf not found
- time
- 2026-06-08 17:37 UTC · 4 months ago
- machine
- koh2
- agent
- claude-sonnet-4-6
- session
- auto-run setup on first run
- parent
- b7b8e9e220c2
↓ nina-e66b7c22.tar.gzatlas at this save
files changed5 files
VERSION+1 −1build.zig.zon+1 −1src/commands.zig+44 −0src/main.zig+32 −8src/output.zig+41 −0build.zig.zon+1 −1
@@ -9,7 +9,7 @@99 .name = .nina,1010 // This is a [Semantic Version](https://semver.org/).1111 // In a future version of Zig it will be used for package deduplication.12- .version = "3.0.19",12+ .version = "3.0.21",1313 // Together with name, this represents a globally unique package1414 // identifier. This field is generated by the Zig toolchain when the1515 // package is first created, and then *never changes*. This allowssrc/commands.zig+44 −0
@@ -1097,6 +1097,50 @@10971097 output.hello(ctx.cfg.machines);10981098 }10991099 1100+// Runs when ~/.nina.conf is absent on first invocation (interactive TTY only).1101+// Prompts for machine name, config path, and local flag, then writes the file.1102+pub fn firstRunWizard(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1103+ const home = environ.get("HOME") orelse {1104+ errors.error_info.setSuggestion("set HOME environment variable", .{});1105+ return error.ConfigNotFound;1106+ };1107+1108+ output.firstRunBanner();1109+ output.firstRunNoConfig();1110+1111+ var name_buf: [256]u8 = undefined;1112+ var path_buf: [256]u8 = undefined;1113+ var local_buf: [256]u8 = undefined;1114+1115+ const machine_name = output.firstRunPrompt("machine name", "my-machine", &name_buf);1116+ const config_path = output.firstRunPrompt("config path", "/etc/nixos", &path_buf);1117+ const local_str = output.firstRunPrompt("local", "true", &local_buf);1118+1119+ const conf_path = try std.fmt.allocPrint(gpa, "{s}/.nina.conf", .{home});1120+ defer gpa.free(conf_path);1121+1122+ var content_buf: [1024]u8 = undefined;1123+ const content = try std.fmt.bufPrint(&content_buf,1124+ \\[machine]1125+ \\name = {s}1126+ \\config = {s}1127+ \\local = {s}1128+ \\1129+ , .{ machine_name, config_path, local_str });1130+1131+ const file = std.Io.Dir.createFile(.cwd(), io, conf_path, .{}) catch {1132+ errors.error_info.setMessage("could not write {s}", .{conf_path});1133+ return error.Internal;1134+ };1135+ defer file.close(io);1136+ file.writePositionalAll(io, content, 0) catch {1137+ errors.error_info.setMessage("could not write {s}", .{conf_path});1138+ return error.Internal;1139+ };1140+1141+ output.firstRunWritten(conf_path);1142+}1143+11001144 pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {11011145 output.setupCheck();11021146 const status = exec.checkNixFeatures(gpa, io, environ);src/main.zig+32 −8
@@ -98,14 +98,38 @@9898 return;9999 }100100 101- // Load config and resolve machine102- const cfg = config.load(arena, io, startup.environ_map) catch |err| {103- const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);104- const detail = errors.error_info.getDetail();105- const suggestion = errors.error_info.getSuggestion();106- output.printError(msg, detail, suggestion);107- output.flush();108- std.process.exit(1);101+ // Load config and resolve machine. On first run with no config file, offer102+ // the setup wizard when stdout is a TTY; skip silently in piped/scripted mode.103+ const cfg = cfg_blk: {104+ if (config.load(arena, io, startup.environ_map)) |c| {105+ break :cfg_blk c;106+ } else |err| {107+ if (err == error.ConfigNotFound and (std.Io.File.stdout().isTty(io) catch false)) {108+ commands.firstRunWizard(ca, io, startup.environ_map) catch |wizard_err| {109+ const msg = errors.error_info.getMessage() orelse errors.staticMessage(wizard_err);110+ const detail = errors.error_info.getDetail();111+ const suggestion = errors.error_info.getSuggestion();112+ output.printError(msg, detail, suggestion);113+ output.flush();114+ std.process.exit(1);115+ };116+ errors.error_info.reset();117+ break :cfg_blk config.load(arena, io, startup.environ_map) catch |err2| {118+ const msg = errors.error_info.getMessage() orelse errors.staticMessage(err2);119+ const detail = errors.error_info.getDetail();120+ const suggestion = errors.error_info.getSuggestion();121+ output.printError(msg, detail, suggestion);122+ output.flush();123+ std.process.exit(1);124+ };125+ }126+ const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);127+ const detail = errors.error_info.getDetail();128+ const suggestion = errors.error_info.getSuggestion();129+ output.printError(msg, detail, suggestion);130+ output.flush();131+ std.process.exit(1);132+ }109133 };110134 111135 output.setTeach(cfg.teach);src/output.zig+41 −0
@@ -942,3 +942,44 @@942942 pub fn flakeShowEntry(kind: []const u8, name: []const u8, desc: []const u8) void {943943 p(" {s:<12} {s:<24} {s}\n", .{ kind, name, desc });944944 }945+946+// --- First-run wizard ---947+948+pub fn firstRunBanner() void {949+ p("{s}{s}nina \u{2014} first run\n\n", .{ KAO_FACE, hdr() });950+}951+952+pub fn firstRunNoConfig() void {953+ p(" no config found. let me get set up.\n\n", .{});954+}955+956+// Print a prompt line and read the user's answer. Returns the default if the957+// user presses Enter without typing anything. buf must be at least 256 bytes.958+// Returns a slice into buf (or default if empty input).959+pub fn firstRunPrompt(label: []const u8, default: []const u8, buf: []u8) []const u8 {960+ p(" {s:<14} {s}> {s}", .{ label, c(DIM), c(RESET) });961+ flush();962+ var i: usize = 0;963+ while (i < buf.len - 1) {964+ const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break;965+ if (n == 0) break;966+ if (buf[i] == '\n') break;967+ i += 1;968+ }969+ const input = std.mem.trim(u8, buf[0..i], " \t\r");970+ if (input.len == 0) {971+ // Echo the default so the output shows what was chosen972+ p("{s}{s}{s}\n", .{ c(DIM), default, c(RESET) });973+ flush();974+ return default;975+ }976+ return input;977+}978+979+pub fn firstRunWritten(path: []const u8) void {980+ p("\n {s}written to {s}{s}\n\n", .{ c(DIM), path, c(RESET) });981+}982+983+pub fn firstRunSkipped() void {984+ p("\n {s}skipping setup in non-interactive mode{s}\n", .{ c(DIM), c(RESET) });985+}