From bffd74f55151c809bd75951db52cdc47bf344f27 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Jan 2026 15:43:19 +0000 Subject: [PATCH 1/2] feat: add CLI commands for AI assistant hook management Add `architect hook install|uninstall|status` commands to automate setting up hooks for Claude Code, Codex, and Gemini CLI. This eliminates the manual process of copying scripts and editing configuration files. The commands: - Copy notification scripts to the tool's config directory - Update settings.json (Claude/Gemini) or config.toml (Codex) - Show installation status across all supported tools New modules: - src/cli.zig: CLI argument parser - src/hook_manager.zig: Hook installation/uninstallation logic Usage: architect hook install claude architect hook uninstall gemini architect hook status --- CLAUDE.md | 1 + README.md | 23 ++ docs/architecture.md | 8 +- docs/development.md | 29 ++ src/cli.zig | 210 ++++++++++++++ src/hook_manager.zig | 675 +++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 57 ++++ 7 files changed, 1001 insertions(+), 2 deletions(-) create mode 100644 src/cli.zig create mode 100644 src/hook_manager.zig diff --git a/CLAUDE.md b/CLAUDE.md index 154979f..8cae629 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,6 +131,7 @@ const result = row * GRID_COLS + grid_col; // Works correctly ## Claude Socket Hook - The app creates `${XDG_RUNTIME_DIR:-/tmp}/architect_notify_.sock` and sets `ARCHITECT_SESSION_ID`/`ARCHITECT_NOTIFY_SOCK` for each shell. - Send a single JSON line to signal UI states: `{"session":N,"state":"start"|"awaiting_approval"|"done"}`. The helper `scripts/architect_notify.py` is available if needed. +- Use `architect hook install claude` (or `codex`/`gemini`) to automatically set up hooks for AI assistants. ## Done? Share - Provide a concise summary of edits, test/build outcomes, and documentation updates. diff --git a/README.md b/README.md index 627a22f..9086410 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,29 @@ just build - Cmd+Click opening for OSC 8 hyperlinks - AI assistant status highlights (awaiting approval / done) - Kitty keyboard protocol support for enhanced key handling +- Built-in CLI for managing AI assistant hooks + +## CLI Commands + +Architect includes CLI commands for managing AI assistant hooks: + +```bash +# Install hook for an AI assistant +architect hook install claude # Claude Code +architect hook install codex # OpenAI Codex +architect hook install gemini # Google Gemini CLI + +# Remove an installed hook +architect hook uninstall claude + +# Check installation status +architect hook status + +# Show help +architect help +``` + +The hooks enable visual status indicators in Architect when AI assistants are waiting for approval or have completed tasks. ## Configuration diff --git a/docs/architecture.md b/docs/architecture.md index 96bb042..cb14b0c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,7 +28,9 @@ Architect is a terminal multiplexer displaying interactive sessions in a grid wi ## Runtime Flow -**main.zig** owns application lifetime, window sizing, PTY/session startup, configuration persistence, and the frame loop. Each frame it: +**main.zig** first checks command-line arguments via `cli.zig`. If a CLI command is detected (e.g., `architect hook install claude`), it handles the command and exits without starting the GUI. Otherwise, it proceeds with the normal application lifecycle. + +In **GUI mode**, main.zig owns application lifetime, window sizing, PTY/session startup, configuration persistence, and the frame loop. Each frame it: 1. Polls SDL events and scales coordinates to render space. 2. Builds a lightweight `UiHost` snapshot and lets `UiRoot` handle events first. @@ -72,7 +74,9 @@ Architect is a terminal multiplexer displaying interactive sessions in a grid wi ``` src/ -├── main.zig # Entry point, frame loop, event dispatch +├── main.zig # Entry point, frame loop, event dispatch, CLI routing +├── cli.zig # CLI argument parser (hook, help, version commands) +├── hook_manager.zig # AI assistant hook installation/uninstallation ├── c.zig # C bindings (SDL3, TTF, etc.) ├── colors.zig # Theme and color palette management (ANSI 16/256) ├── config.zig # TOML config persistence diff --git a/docs/development.md b/docs/development.md index 3b787ee..2e9bbd6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -97,6 +97,35 @@ Each release includes: Architect exposes a Unix domain socket to let external tools (Claude Code, Codex, Gemini CLI, etc.) signal UI states. +### Automated Installation + +The easiest way to set up hooks is using the built-in CLI commands: + +```bash +# Install hook for Claude Code +architect hook install claude + +# Install hook for Codex +architect hook install codex + +# Install hook for Gemini CLI +architect hook install gemini + +# Check which hooks are installed +architect hook status + +# Uninstall a hook +architect hook uninstall claude +``` + +These commands automatically: +1. Copy the notification scripts to the tool's config directory +2. Update the tool's configuration file with the appropriate hooks + +### Manual Installation + +If you prefer manual setup or need custom configuration, follow the instructions below. + ### Socket Protocol - Socket: `${XDG_RUNTIME_DIR:-/tmp}/architect_notify_.sock` diff --git a/src/cli.zig b/src/cli.zig new file mode 100644 index 0000000..4cd6a77 --- /dev/null +++ b/src/cli.zig @@ -0,0 +1,210 @@ +// CLI argument parser for Architect subcommands. +// Currently supports: `architect hook install|uninstall|status [tool]` +const std = @import("std"); + +pub const Tool = enum { + claude, + codex, + gemini, + + pub fn displayName(self: Tool) []const u8 { + return switch (self) { + .claude => "Claude Code", + .codex => "Codex", + .gemini => "Gemini CLI", + }; + } + + pub fn configDir(self: Tool) []const u8 { + return switch (self) { + .claude => ".claude", + .codex => ".codex", + .gemini => ".gemini", + }; + } + + pub fn fromString(s: []const u8) ?Tool { + if (std.mem.eql(u8, s, "claude") or std.mem.eql(u8, s, "claude-code")) { + return .claude; + } else if (std.mem.eql(u8, s, "codex")) { + return .codex; + } else if (std.mem.eql(u8, s, "gemini") or std.mem.eql(u8, s, "gemini-cli")) { + return .gemini; + } + return null; + } +}; + +pub const HookCommand = enum { + install, + uninstall, + status, + + pub fn fromString(s: []const u8) ?HookCommand { + if (std.mem.eql(u8, s, "install")) { + return .install; + } else if (std.mem.eql(u8, s, "uninstall")) { + return .uninstall; + } else if (std.mem.eql(u8, s, "status")) { + return .status; + } + return null; + } +}; + +pub const Command = union(enum) { + hook: struct { + action: HookCommand, + tool: ?Tool, + }, + help, + version, + gui, // No CLI args, run GUI mode +}; + +pub const ParseError = error{ + UnknownCommand, + MissingHookAction, + UnknownHookAction, + UnknownTool, + MissingToolArgument, +}; + +/// Parse command-line arguments and return the appropriate Command. +/// Returns `.gui` if no arguments are provided (normal GUI launch). +pub fn parse(args: []const []const u8) ParseError!Command { + // Skip program name + const cmd_args = if (args.len > 0) args[1..] else args; + + if (cmd_args.len == 0) { + return .gui; + } + + const first = cmd_args[0]; + + // Check for help flags + if (std.mem.eql(u8, first, "--help") or std.mem.eql(u8, first, "-h") or std.mem.eql(u8, first, "help")) { + return .help; + } + + // Check for version flags + if (std.mem.eql(u8, first, "--version") or std.mem.eql(u8, first, "-v") or std.mem.eql(u8, first, "version")) { + return .version; + } + + // Hook command + if (std.mem.eql(u8, first, "hook")) { + if (cmd_args.len < 2) { + return error.MissingHookAction; + } + + const action = HookCommand.fromString(cmd_args[1]) orelse { + return error.UnknownHookAction; + }; + + // Status doesn't require a tool argument + if (action == .status) { + return .{ .hook = .{ .action = action, .tool = null } }; + } + + // Install/uninstall require a tool argument + if (cmd_args.len < 3) { + return error.MissingToolArgument; + } + + const tool = Tool.fromString(cmd_args[2]) orelse { + return error.UnknownTool; + }; + + return .{ .hook = .{ .action = action, .tool = tool } }; + } + + return error.UnknownCommand; +} + +pub fn printUsage(writer: anytype) !void { + try writer.writeAll( + \\Architect - A terminal multiplexer for AI-assisted development + \\ + \\USAGE: + \\ architect Launch the GUI + \\ architect hook Manage AI assistant hooks + \\ architect help Show this help message + \\ architect version Show version information + \\ + \\HOOK COMMANDS: + \\ architect hook install Install hook for an AI tool + \\ architect hook uninstall Uninstall hook for an AI tool + \\ architect hook status Show installed hooks status + \\ + \\SUPPORTED TOOLS: + \\ claude, claude-code Claude Code AI assistant + \\ codex OpenAI Codex CLI + \\ gemini, gemini-cli Google Gemini CLI + \\ + \\EXAMPLES: + \\ architect hook install claude + \\ architect hook uninstall gemini + \\ architect hook status + \\ + ); +} + +pub fn printError(err: ParseError, writer: anytype) !void { + switch (err) { + error.UnknownCommand => try writer.writeAll("Error: Unknown command. Use 'architect help' for usage.\n"), + error.MissingHookAction => try writer.writeAll("Error: Missing hook action. Use: architect hook install|uninstall|status\n"), + error.UnknownHookAction => try writer.writeAll("Error: Unknown hook action. Valid actions: install, uninstall, status\n"), + error.UnknownTool => try writer.writeAll("Error: Unknown tool. Valid tools: claude, codex, gemini\n"), + error.MissingToolArgument => try writer.writeAll("Error: Missing tool argument. Use: architect hook install \n"), + } +} + +test "parse - no args returns gui" { + const result = try parse(&[_][]const u8{"architect"}); + try std.testing.expectEqual(.gui, result); +} + +test "parse - help flags" { + try std.testing.expectEqual(.help, try parse(&[_][]const u8{ "architect", "help" })); + try std.testing.expectEqual(.help, try parse(&[_][]const u8{ "architect", "--help" })); + try std.testing.expectEqual(.help, try parse(&[_][]const u8{ "architect", "-h" })); +} + +test "parse - version flags" { + try std.testing.expectEqual(.version, try parse(&[_][]const u8{ "architect", "version" })); + try std.testing.expectEqual(.version, try parse(&[_][]const u8{ "architect", "--version" })); + try std.testing.expectEqual(.version, try parse(&[_][]const u8{ "architect", "-v" })); +} + +test "parse - hook install claude" { + const result = try parse(&[_][]const u8{ "architect", "hook", "install", "claude" }); + switch (result) { + .hook => |h| { + try std.testing.expectEqual(.install, h.action); + try std.testing.expectEqual(.claude, h.tool.?); + }, + else => return error.TestUnexpectedResult, + } +} + +test "parse - hook status" { + const result = try parse(&[_][]const u8{ "architect", "hook", "status" }); + switch (result) { + .hook => |h| { + try std.testing.expectEqual(.status, h.action); + try std.testing.expect(h.tool == null); + }, + else => return error.TestUnexpectedResult, + } +} + +test "parse - missing hook action" { + const result = parse(&[_][]const u8{ "architect", "hook" }); + try std.testing.expectError(error.MissingHookAction, result); +} + +test "parse - missing tool for install" { + const result = parse(&[_][]const u8{ "architect", "hook", "install" }); + try std.testing.expectError(error.MissingToolArgument, result); +} diff --git a/src/hook_manager.zig b/src/hook_manager.zig new file mode 100644 index 0000000..832619b --- /dev/null +++ b/src/hook_manager.zig @@ -0,0 +1,675 @@ +// Hook manager for installing/uninstalling Architect hooks for AI assistants. +// Supports Claude Code, Codex, and Gemini CLI. +const std = @import("std"); +const builtin = @import("builtin"); +const cli = @import("cli.zig"); + +const Tool = cli.Tool; + +pub const HookError = error{ + HomeNotFound, + ScriptNotFound, + CopyFailed, + ConfigReadFailed, + ConfigWriteFailed, + JsonParseFailed, + OutOfMemory, + InvalidPath, +}; + +const ScriptInfo = struct { + name: []const u8, + dest_name: []const u8, +}; + +fn getScriptsForTool(tool: Tool) []const ScriptInfo { + return switch (tool) { + .claude, .codex => &[_]ScriptInfo{ + .{ .name = "architect_notify.py", .dest_name = "architect_notify.py" }, + }, + .gemini => &[_]ScriptInfo{ + .{ .name = "architect_notify.py", .dest_name = "architect_notify.py" }, + .{ .name = "architect_hook_gemini.py", .dest_name = "architect_hook.py" }, + }, + }; +} + +fn getHomeDir() ?[]const u8 { + return std.posix.getenv("HOME"); +} + +fn findScriptsDir(allocator: std.mem.Allocator) !?[]u8 { + // Try relative to executable first (for installed binaries) + const self_exe = std.fs.selfExePath(allocator) catch null; + if (self_exe) |exe_path| { + defer allocator.free(exe_path); + if (std.fs.path.dirname(exe_path)) |exe_dir| { + // Check ../share/architect/scripts (standard install location) + const share_path = try std.fs.path.join(allocator, &.{ exe_dir, "..", "share", "architect", "scripts" }); + defer allocator.free(share_path); + if (std.fs.cwd().openDir(share_path, .{})) |dir| { + dir.close(); + var buf: [std.fs.max_path_bytes]u8 = undefined; + const resolved = std.fs.cwd().realpath(share_path, &buf) catch null; + if (resolved) |p| return try allocator.dupe(u8, p); + } else |_| {} + + // Check ../scripts (development layout) + const dev_path = try std.fs.path.join(allocator, &.{ exe_dir, "..", "scripts" }); + defer allocator.free(dev_path); + if (std.fs.cwd().openDir(dev_path, .{})) |dir| { + dir.close(); + var buf: [std.fs.max_path_bytes]u8 = undefined; + const resolved = std.fs.cwd().realpath(dev_path, &buf) catch null; + if (resolved) |p| return try allocator.dupe(u8, p); + } else |_| {} + } + } + + // Try current working directory's scripts folder (for running from source) + if (std.fs.cwd().openDir("scripts", .{})) |dir| { + dir.close(); + var buf: [std.fs.max_path_bytes]u8 = undefined; + const resolved = std.fs.cwd().realpath("scripts", &buf) catch null; + if (resolved) |p| return try allocator.dupe(u8, p); + } else |_| {} + + return null; +} + +/// Copy a file from src_path to dest_path and make it executable. +fn copyScriptFile(src_path: []const u8, dest_path: []const u8) !void { + // Read source file + const src_file = std.fs.openFileAbsolute(src_path, .{}) catch return error.ScriptNotFound; + defer src_file.close(); + + const content = src_file.readToEndAlloc(std.heap.page_allocator, 1024 * 1024) catch return error.CopyFailed; + defer std.heap.page_allocator.free(content); + + // Create destination directory if needed + const dest_dir = std.fs.path.dirname(dest_path) orelse return error.InvalidPath; + std.fs.makeDirAbsolute(dest_dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return error.CopyFailed, + }; + + // Write destination file + const dest_file = std.fs.createFileAbsolute(dest_path, .{ .mode = 0o755 }) catch return error.CopyFailed; + defer dest_file.close(); + + dest_file.writeAll(content) catch return error.CopyFailed; +} + +/// Remove a file if it exists. +fn removeFile(path: []const u8) void { + std.fs.deleteFileAbsolute(path) catch {}; +} + +/// Check if a file exists. +fn fileExists(path: []const u8) bool { + const file = std.fs.openFileAbsolute(path, .{}) catch return false; + file.close(); + return true; +} + +// ============================================================================ +// JSON Configuration Handling +// ============================================================================ + +/// Install Claude Code hooks by updating settings.json +fn installClaudeHooks(allocator: std.mem.Allocator, config_path: []const u8, script_path: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + // Read existing file or start with empty object + var content: []u8 = &.{}; + const file = std.fs.openFileAbsolute(config_path, .{}) catch |err| switch (err) { + error.FileNotFound => blk: { + // Create directory if needed + const dir_path = std.fs.path.dirname(config_path) orelse return error.InvalidPath; + std.fs.makeDirAbsolute(dir_path) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return error.ConfigWriteFailed, + }; + break :blk null; + }, + else => return error.ConfigReadFailed, + }; + + if (file) |f| { + defer f.close(); + content = f.readToEndAlloc(alloc, 10 * 1024 * 1024) catch return error.ConfigReadFailed; + } + + // Parse existing JSON or create empty object + var root: std.json.Value = undefined; + if (content.len > 0) { + const parsed = std.json.parseFromSlice(std.json.Value, alloc, content, .{}) catch return error.JsonParseFailed; + root = parsed.value; + } else { + root = .{ .object = std.json.ObjectMap.init(alloc) }; + } + + if (root != .object) return error.JsonParseFailed; + + // Build hook configuration + const stop_cmd = try std.fmt.allocPrint(alloc, "python3 {s} done || true", .{script_path}); + const notif_cmd = try std.fmt.allocPrint(alloc, "python3 {s} awaiting_approval || true", .{script_path}); + + // Create hooks structure + var hooks: std.json.ObjectMap = undefined; + if (root.object.getPtr("hooks")) |existing| { + if (existing.* == .object) { + hooks = existing.object; + } else { + hooks = std.json.ObjectMap.init(alloc); + } + } else { + hooks = std.json.ObjectMap.init(alloc); + } + + // Stop hook + var stop_hook = std.json.ObjectMap.init(alloc); + try stop_hook.put("type", .{ .string = "command" }); + try stop_hook.put("command", .{ .string = stop_cmd }); + + var stop_hooks_array = std.json.Array.init(alloc); + try stop_hooks_array.append(.{ .object = stop_hook }); + + var stop_obj = std.json.ObjectMap.init(alloc); + try stop_obj.put("hooks", .{ .array = stop_hooks_array }); + + var stop_array = std.json.Array.init(alloc); + try stop_array.append(.{ .object = stop_obj }); + try hooks.put("Stop", .{ .array = stop_array }); + + // Notification hook + var notif_hook = std.json.ObjectMap.init(alloc); + try notif_hook.put("type", .{ .string = "command" }); + try notif_hook.put("command", .{ .string = notif_cmd }); + + var notif_hooks_array = std.json.Array.init(alloc); + try notif_hooks_array.append(.{ .object = notif_hook }); + + var notif_obj = std.json.ObjectMap.init(alloc); + try notif_obj.put("hooks", .{ .array = notif_hooks_array }); + + var notif_array = std.json.Array.init(alloc); + try notif_array.append(.{ .object = notif_obj }); + try hooks.put("Notification", .{ .array = notif_array }); + + try root.object.put("hooks", .{ .object = hooks }); + + // Write back + const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return error.ConfigWriteFailed; + defer out_file.close(); + + std.json.stringify(root, .{ .whitespace = .indent_2 }, out_file.writer()) catch return error.ConfigWriteFailed; + out_file.writer().writeByte('\n') catch return error.ConfigWriteFailed; +} + +fn uninstallClaudeHooks(allocator: std.mem.Allocator, config_path: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const file = std.fs.openFileAbsolute(config_path, .{}) catch return; + defer file.close(); + + const content = file.readToEndAlloc(alloc, 10 * 1024 * 1024) catch return; + if (content.len == 0) return; + + const parsed = std.json.parseFromSlice(std.json.Value, alloc, content, .{}) catch return; + var root = parsed.value; + + if (root != .object) return; + + if (root.object.getPtr("hooks")) |hooks| { + if (hooks.* == .object) { + _ = hooks.object.fetchSwapRemove("Stop"); + _ = hooks.object.fetchSwapRemove("Notification"); + + if (hooks.object.count() == 0) { + _ = root.object.fetchSwapRemove("hooks"); + } + } + } + + const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return; + defer out_file.close(); + + std.json.stringify(root, .{ .whitespace = .indent_2 }, out_file.writer()) catch return; + out_file.writer().writeByte('\n') catch return; +} + +// ============================================================================ +// Codex Hook Configuration (TOML) +// ============================================================================ + +fn installCodexHooks(allocator: std.mem.Allocator, config_path: []const u8, script_path: []const u8) !void { + // Read existing config if present + var content: []u8 = &.{}; + const file = std.fs.openFileAbsolute(config_path, .{}) catch |err| switch (err) { + error.FileNotFound => { + // Create new config file + const dir_path = std.fs.path.dirname(config_path) orelse return error.InvalidPath; + std.fs.makeDirAbsolute(dir_path) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return error.ConfigWriteFailed, + }; + const new_file = std.fs.createFileAbsolute(config_path, .{}) catch return error.ConfigWriteFailed; + defer new_file.close(); + const notify_line = try std.fmt.allocPrint(allocator, "notify = [\"python3\", \"{s}\"]\n", .{script_path}); + defer allocator.free(notify_line); + new_file.writeAll(notify_line) catch return error.ConfigWriteFailed; + return; + }, + else => return error.ConfigReadFailed, + }; + defer file.close(); + + content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch return error.ConfigReadFailed; + defer allocator.free(content); + + // Check if notify is already configured with architect + if (std.mem.indexOf(u8, content, "notify") != null and std.mem.indexOf(u8, content, "architect") != null) { + // Replace existing architect notify line + var new_content = std.ArrayList(u8).init(allocator); + defer new_content.deinit(); + + var lines = std.mem.splitScalar(u8, content, '\n'); + var first = true; + while (lines.next()) |line| { + if (!first) { + try new_content.append('\n'); + } + first = false; + + const trimmed = std.mem.trim(u8, line, " \t"); + if (std.mem.startsWith(u8, trimmed, "notify") and std.mem.indexOf(u8, line, "architect") != null) { + try new_content.appendSlice("notify = [\"python3\", \""); + try new_content.appendSlice(script_path); + try new_content.appendSlice("\"]"); + } else { + try new_content.appendSlice(line); + } + } + + const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return error.ConfigWriteFailed; + defer out_file.close(); + out_file.writeAll(new_content.items) catch return error.ConfigWriteFailed; + } else if (std.mem.indexOf(u8, content, "notify")) |_| { + // There's a different notify config - don't overwrite it + return; + } else { + // Append notify line + const out_file = std.fs.createFileAbsolute(config_path, .{ .truncate = false }) catch return error.ConfigWriteFailed; + defer out_file.close(); + out_file.seekFromEnd(0) catch return error.ConfigWriteFailed; + + // Add newline if content doesn't end with one + if (content.len > 0 and content[content.len - 1] != '\n') { + out_file.writeAll("\n") catch return error.ConfigWriteFailed; + } + + const notify_line = std.fmt.allocPrint(allocator, "notify = [\"python3\", \"{s}\"]\n", .{script_path}) catch return error.OutOfMemory; + defer allocator.free(notify_line); + out_file.writeAll(notify_line) catch return error.ConfigWriteFailed; + } +} + +fn uninstallCodexHooks(allocator: std.mem.Allocator, config_path: []const u8) !void { + const file = std.fs.openFileAbsolute(config_path, .{}) catch return; + defer file.close(); + + const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch return; + defer allocator.free(content); + + var new_content = std.ArrayList(u8).init(allocator); + defer new_content.deinit(); + + var lines = std.mem.splitScalar(u8, content, '\n'); + var first = true; + while (lines.next()) |line| { + // Skip lines containing notify and architect + if (std.mem.indexOf(u8, line, "notify") != null and std.mem.indexOf(u8, line, "architect") != null) { + continue; + } + if (!first) { + try new_content.append('\n'); + } + first = false; + try new_content.appendSlice(line); + } + + const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return; + defer out_file.close(); + out_file.writeAll(new_content.items) catch return; +} + +// ============================================================================ +// Gemini Hook Configuration +// ============================================================================ + +fn installGeminiHooks(allocator: std.mem.Allocator, config_path: []const u8, script_path: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + // Read existing file or start with empty object + var content: []u8 = &.{}; + const file = std.fs.openFileAbsolute(config_path, .{}) catch |err| switch (err) { + error.FileNotFound => blk: { + const dir_path = std.fs.path.dirname(config_path) orelse return error.InvalidPath; + std.fs.makeDirAbsolute(dir_path) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return error.ConfigWriteFailed, + }; + break :blk null; + }, + else => return error.ConfigReadFailed, + }; + + if (file) |f| { + defer f.close(); + content = f.readToEndAlloc(alloc, 10 * 1024 * 1024) catch return error.ConfigReadFailed; + } + + var root: std.json.Value = undefined; + if (content.len > 0) { + const parsed = std.json.parseFromSlice(std.json.Value, alloc, content, .{}) catch return error.JsonParseFailed; + root = parsed.value; + } else { + root = .{ .object = std.json.ObjectMap.init(alloc) }; + } + + if (root != .object) return error.JsonParseFailed; + + // Build commands + const after_cmd = try std.fmt.allocPrint(alloc, "python3 {s} done", .{script_path}); + const notif_cmd = try std.fmt.allocPrint(alloc, "python3 {s} awaiting_approval", .{script_path}); + + // Create hooks structure + var hooks: std.json.ObjectMap = undefined; + if (root.object.getPtr("hooks")) |existing| { + if (existing.* == .object) { + hooks = existing.object; + } else { + hooks = std.json.ObjectMap.init(alloc); + } + } else { + hooks = std.json.ObjectMap.init(alloc); + } + + // AfterAgent hook + var after_hook = std.json.ObjectMap.init(alloc); + try after_hook.put("name", .{ .string = "architect-completion" }); + try after_hook.put("type", .{ .string = "command" }); + try after_hook.put("command", .{ .string = after_cmd }); + try after_hook.put("description", .{ .string = "Notify Architect when task completes" }); + + var after_hooks_array = std.json.Array.init(alloc); + try after_hooks_array.append(.{ .object = after_hook }); + + var after_obj = std.json.ObjectMap.init(alloc); + try after_obj.put("matcher", .{ .string = "*" }); + try after_obj.put("hooks", .{ .array = after_hooks_array }); + + var after_array = std.json.Array.init(alloc); + try after_array.append(.{ .object = after_obj }); + try hooks.put("AfterAgent", .{ .array = after_array }); + + // Notification hook + var notif_hook = std.json.ObjectMap.init(alloc); + try notif_hook.put("name", .{ .string = "architect-approval" }); + try notif_hook.put("type", .{ .string = "command" }); + try notif_hook.put("command", .{ .string = notif_cmd }); + try notif_hook.put("description", .{ .string = "Notify Architect when waiting for approval" }); + + var notif_hooks_array = std.json.Array.init(alloc); + try notif_hooks_array.append(.{ .object = notif_hook }); + + var notif_obj = std.json.ObjectMap.init(alloc); + try notif_obj.put("matcher", .{ .string = "*" }); + try notif_obj.put("hooks", .{ .array = notif_hooks_array }); + + var notif_array = std.json.Array.init(alloc); + try notif_array.append(.{ .object = notif_obj }); + try hooks.put("Notification", .{ .array = notif_array }); + + try root.object.put("hooks", .{ .object = hooks }); + + // Ensure tools.enableHooks is set + var tools: std.json.ObjectMap = undefined; + if (root.object.getPtr("tools")) |existing| { + if (existing.* == .object) { + tools = existing.object; + } else { + tools = std.json.ObjectMap.init(alloc); + } + } else { + tools = std.json.ObjectMap.init(alloc); + } + try tools.put("enableHooks", .{ .bool = true }); + try root.object.put("tools", .{ .object = tools }); + + // Write back + const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return error.ConfigWriteFailed; + defer out_file.close(); + + std.json.stringify(root, .{ .whitespace = .indent_2 }, out_file.writer()) catch return error.ConfigWriteFailed; + out_file.writer().writeByte('\n') catch return error.ConfigWriteFailed; +} + +fn uninstallGeminiHooks(allocator: std.mem.Allocator, config_path: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const file = std.fs.openFileAbsolute(config_path, .{}) catch return; + defer file.close(); + + const content = file.readToEndAlloc(alloc, 10 * 1024 * 1024) catch return; + if (content.len == 0) return; + + const parsed = std.json.parseFromSlice(std.json.Value, alloc, content, .{}) catch return; + var root = parsed.value; + + if (root != .object) return; + + if (root.object.getPtr("hooks")) |hooks| { + if (hooks.* == .object) { + _ = hooks.object.fetchSwapRemove("AfterAgent"); + _ = hooks.object.fetchSwapRemove("Notification"); + + if (hooks.object.count() == 0) { + _ = root.object.fetchSwapRemove("hooks"); + } + } + } + + // Remove tools.enableHooks + if (root.object.getPtr("tools")) |tools| { + if (tools.* == .object) { + _ = tools.object.fetchSwapRemove("enableHooks"); + if (tools.object.count() == 0) { + _ = root.object.fetchSwapRemove("tools"); + } + } + } + + const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return; + defer out_file.close(); + + std.json.stringify(root, .{ .whitespace = .indent_2 }, out_file.writer()) catch return; + out_file.writer().writeByte('\n') catch return; +} + +// ============================================================================ +// Public API +// ============================================================================ + +pub fn install(allocator: std.mem.Allocator, tool: Tool, writer: anytype) !void { + const home = getHomeDir() orelse { + try writer.writeAll("Error: HOME environment variable not set\n"); + return; + }; + + const scripts_dir = try findScriptsDir(allocator) orelse { + try writer.writeAll("Error: Could not find Architect scripts directory\n"); + try writer.writeAll("Make sure you're running from the Architect installation or source directory.\n"); + return; + }; + defer allocator.free(scripts_dir); + + try writer.print("Installing Architect hook for {s}...\n", .{tool.displayName()}); + + // Build destination directory path + const dest_dir = try std.fs.path.join(allocator, &.{ home, tool.configDir() }); + defer allocator.free(dest_dir); + + // Copy scripts + const scripts = getScriptsForTool(tool); + for (scripts) |script| { + const src_path = try std.fs.path.join(allocator, &.{ scripts_dir, script.name }); + defer allocator.free(src_path); + + const dest_path = try std.fs.path.join(allocator, &.{ dest_dir, script.dest_name }); + defer allocator.free(dest_path); + + copyScriptFile(src_path, dest_path) catch |err| { + try writer.print("Error copying {s}: {}\n", .{ script.name, err }); + return; + }; + + try writer.print(" Copied {s} -> {s}\n", .{ script.name, dest_path }); + } + + // Get the main script path for config + const main_script_path = try std.fs.path.join(allocator, &.{ dest_dir, "architect_notify.py" }); + defer allocator.free(main_script_path); + + // Update tool configuration + switch (tool) { + .claude => { + const config_path = try std.fs.path.join(allocator, &.{ dest_dir, "settings.json" }); + defer allocator.free(config_path); + + installClaudeHooks(allocator, config_path, main_script_path) catch |err| { + try writer.print("Error updating {s}: {}\n", .{ config_path, err }); + return; + }; + + try writer.print(" Updated {s}\n", .{config_path}); + }, + .codex => { + const config_path = try std.fs.path.join(allocator, &.{ dest_dir, "config.toml" }); + defer allocator.free(config_path); + + installCodexHooks(allocator, config_path, main_script_path) catch |err| { + try writer.print("Error updating {s}: {}\n", .{ config_path, err }); + return; + }; + + try writer.print(" Updated {s}\n", .{config_path}); + }, + .gemini => { + const hook_script_path = try std.fs.path.join(allocator, &.{ dest_dir, "architect_hook.py" }); + defer allocator.free(hook_script_path); + + const config_path = try std.fs.path.join(allocator, &.{ dest_dir, "settings.json" }); + defer allocator.free(config_path); + + installGeminiHooks(allocator, config_path, hook_script_path) catch |err| { + try writer.print("Error updating {s}: {}\n", .{ config_path, err }); + return; + }; + + try writer.print(" Updated {s}\n", .{config_path}); + }, + } + + try writer.print("\nHook installed! {s} sessions will now show Architect status indicators.\n", .{tool.displayName()}); +} + +pub fn uninstall(allocator: std.mem.Allocator, tool: Tool, writer: anytype) !void { + const home = getHomeDir() orelse { + try writer.writeAll("Error: HOME environment variable not set\n"); + return; + }; + + try writer.print("Uninstalling Architect hook for {s}...\n", .{tool.displayName()}); + + // Build destination directory path + const dest_dir = try std.fs.path.join(allocator, &.{ home, tool.configDir() }); + defer allocator.free(dest_dir); + + // Remove scripts + const scripts = getScriptsForTool(tool); + for (scripts) |script| { + const dest_path = try std.fs.path.join(allocator, &.{ dest_dir, script.dest_name }); + defer allocator.free(dest_path); + + if (fileExists(dest_path)) { + removeFile(dest_path); + try writer.print(" Removed {s}\n", .{dest_path}); + } + } + + // Update tool configuration + switch (tool) { + .claude => { + const config_path = try std.fs.path.join(allocator, &.{ dest_dir, "settings.json" }); + defer allocator.free(config_path); + + uninstallClaudeHooks(allocator, config_path) catch {}; + try writer.print(" Cleaned {s}\n", .{config_path}); + }, + .codex => { + const config_path = try std.fs.path.join(allocator, &.{ dest_dir, "config.toml" }); + defer allocator.free(config_path); + + uninstallCodexHooks(allocator, config_path) catch {}; + try writer.print(" Cleaned {s}\n", .{config_path}); + }, + .gemini => { + const config_path = try std.fs.path.join(allocator, &.{ dest_dir, "settings.json" }); + defer allocator.free(config_path); + + uninstallGeminiHooks(allocator, config_path) catch {}; + try writer.print(" Cleaned {s}\n", .{config_path}); + }, + } + + try writer.print("\nHook uninstalled.\n", .{}); +} + +pub fn status(allocator: std.mem.Allocator, writer: anytype) !void { + const home = getHomeDir() orelse { + try writer.writeAll("Error: HOME environment variable not set\n"); + return; + }; + + try writer.writeAll("Architect Hook Status:\n\n"); + + const tools = [_]Tool{ .claude, .codex, .gemini }; + + for (tools) |tool| { + const dest_dir = try std.fs.path.join(allocator, &.{ home, tool.configDir() }); + defer allocator.free(dest_dir); + + const script_path = try std.fs.path.join(allocator, &.{ dest_dir, "architect_notify.py" }); + defer allocator.free(script_path); + + const installed = fileExists(script_path); + + if (installed) { + try writer.print(" [x] {s: <15} {s}/\n", .{ tool.displayName(), dest_dir }); + } else { + try writer.print(" [ ] {s: <15} Not installed\n", .{tool.displayName()}); + } + } + + try writer.writeAll("\nUse 'architect hook install ' to install a hook.\n"); +} diff --git a/src/main.zig b/src/main.zig index 01dbe13..5d2e6b4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -24,6 +24,8 @@ const ghostty_vt = @import("ghostty-vt"); const c = @import("c.zig"); const open_url = @import("os/open.zig"); const url_matcher = @import("url_matcher.zig"); +const cli = @import("cli.zig"); +const hook_manager = @import("hook_manager.zig"); const log = std.log.scoped(.main); @@ -122,6 +124,61 @@ pub fn main() !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); + // Handle CLI commands (hook install/uninstall/status, help, version) + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + const command = cli.parse(args) catch |err| { + const stderr = std.io.getStdErr().writer(); + cli.printError(err, stderr) catch {}; + std.process.exit(1); + }; + + switch (command) { + .help => { + const stdout = std.io.getStdOut().writer(); + cli.printUsage(stdout) catch {}; + return; + }, + .version => { + const stdout = std.io.getStdOut().writer(); + stdout.writeAll("Architect 0.35.0\n") catch {}; + return; + }, + .hook => |h| { + const stdout = std.io.getStdOut().writer(); + switch (h.action) { + .install => { + if (h.tool) |tool| { + hook_manager.install(allocator, tool, stdout) catch |err| { + const stderr = std.io.getStdErr().writer(); + stderr.print("Error: {}\n", .{err}) catch {}; + std.process.exit(1); + }; + } + }, + .uninstall => { + if (h.tool) |tool| { + hook_manager.uninstall(allocator, tool, stdout) catch |err| { + const stderr = std.io.getStdErr().writer(); + stderr.print("Error: {}\n", .{err}) catch {}; + std.process.exit(1); + }; + } + }, + .status => { + hook_manager.status(allocator, stdout) catch |err| { + const stderr = std.io.getStdErr().writer(); + stderr.print("Error: {}\n", .{err}) catch {}; + std.process.exit(1); + }; + }, + } + return; + }, + .gui => {}, // Continue with GUI initialization + } + // Socket listener relays external "awaiting approval / done" signals from // shells (or other tools) into the UI thread without blocking rendering. var notify_queue = NotificationQueue{}; From 2a6df9aa55d2ee74f9ba310c4949db6e10dc7fe6 Mon Sep 17 00:00:00 2001 From: Forketyfork Date: Sun, 18 Jan 2026 08:46:53 +0100 Subject: [PATCH 2/2] fix: address PR feedback and fix Zig 0.15 compatibility Address PR review comments: - Return HookSkipped error when Codex config already has a notify setting, allowing CLI to inform user of manual merge needed - Add || true to Gemini hook commands for consistent error handling Fix Zig 0.15 API compatibility: - Update std.json.stringify to use std.json.Stringify.value - Update ArrayList API: use {} initializer, pass allocator to methods - Update std.fs.selfExePath to use buffer instead of allocator - Update std.io.getStdErr/getStdOut to std.fs.File.stderr/stdout - Simplify directory existence checks using realpath directly - Fix variable shadowing in main.zig (command -> worktree_cmd, remove_cmd) --- src/hook_manager.zig | 86 +++++++++++++++++++++++--------------------- src/main.zig | 26 +++++++------- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/src/hook_manager.zig b/src/hook_manager.zig index 832619b..f97f367 100644 --- a/src/hook_manager.zig +++ b/src/hook_manager.zig @@ -15,6 +15,7 @@ pub const HookError = error{ JsonParseFailed, OutOfMemory, InvalidPath, + HookSkipped, }; const ScriptInfo = struct { @@ -22,6 +23,14 @@ const ScriptInfo = struct { dest_name: []const u8, }; +fn writeJsonToFile(file: std.fs.File, value: std.json.Value) !void { + var write_buffer: [4096]u8 = undefined; + var file_writer = std.fs.File.Writer.init(file, &write_buffer); + try std.json.Stringify.value(value, .{ .whitespace = .indent_2 }, &file_writer.interface); + try file_writer.interface.writeByte('\n'); + try file_writer.interface.flush(); +} + fn getScriptsForTool(tool: Tool) []const ScriptInfo { return switch (tool) { .claude, .codex => &[_]ScriptInfo{ @@ -40,38 +49,31 @@ fn getHomeDir() ?[]const u8 { fn findScriptsDir(allocator: std.mem.Allocator) !?[]u8 { // Try relative to executable first (for installed binaries) - const self_exe = std.fs.selfExePath(allocator) catch null; - if (self_exe) |exe_path| { - defer allocator.free(exe_path); - if (std.fs.path.dirname(exe_path)) |exe_dir| { + var exe_buf: [std.fs.max_path_bytes]u8 = undefined; + const exe_path = std.fs.selfExePath(&exe_buf) catch null; + if (exe_path) |path| { + if (std.fs.path.dirname(path)) |exe_dir| { // Check ../share/architect/scripts (standard install location) const share_path = try std.fs.path.join(allocator, &.{ exe_dir, "..", "share", "architect", "scripts" }); defer allocator.free(share_path); - if (std.fs.cwd().openDir(share_path, .{})) |dir| { - dir.close(); - var buf: [std.fs.max_path_bytes]u8 = undefined; - const resolved = std.fs.cwd().realpath(share_path, &buf) catch null; - if (resolved) |p| return try allocator.dupe(u8, p); + var resolve_buf: [std.fs.max_path_bytes]u8 = undefined; + if (std.fs.cwd().realpath(share_path, &resolve_buf)) |p| { + return try allocator.dupe(u8, p); } else |_| {} // Check ../scripts (development layout) const dev_path = try std.fs.path.join(allocator, &.{ exe_dir, "..", "scripts" }); defer allocator.free(dev_path); - if (std.fs.cwd().openDir(dev_path, .{})) |dir| { - dir.close(); - var buf: [std.fs.max_path_bytes]u8 = undefined; - const resolved = std.fs.cwd().realpath(dev_path, &buf) catch null; - if (resolved) |p| return try allocator.dupe(u8, p); + if (std.fs.cwd().realpath(dev_path, &resolve_buf)) |p| { + return try allocator.dupe(u8, p); } else |_| {} } } // Try current working directory's scripts folder (for running from source) - if (std.fs.cwd().openDir("scripts", .{})) |dir| { - dir.close(); - var buf: [std.fs.max_path_bytes]u8 = undefined; - const resolved = std.fs.cwd().realpath("scripts", &buf) catch null; - if (resolved) |p| return try allocator.dupe(u8, p); + var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; + if (std.fs.cwd().realpath("scripts", &cwd_buf)) |p| { + return try allocator.dupe(u8, p); } else |_| {} return null; @@ -205,8 +207,7 @@ fn installClaudeHooks(allocator: std.mem.Allocator, config_path: []const u8, scr const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return error.ConfigWriteFailed; defer out_file.close(); - std.json.stringify(root, .{ .whitespace = .indent_2 }, out_file.writer()) catch return error.ConfigWriteFailed; - out_file.writer().writeByte('\n') catch return error.ConfigWriteFailed; + writeJsonToFile(out_file, root) catch return error.ConfigWriteFailed; } fn uninstallClaudeHooks(allocator: std.mem.Allocator, config_path: []const u8) !void { @@ -239,8 +240,7 @@ fn uninstallClaudeHooks(allocator: std.mem.Allocator, config_path: []const u8) ! const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return; defer out_file.close(); - std.json.stringify(root, .{ .whitespace = .indent_2 }, out_file.writer()) catch return; - out_file.writer().writeByte('\n') catch return; + writeJsonToFile(out_file, root) catch return; } // ============================================================================ @@ -275,24 +275,24 @@ fn installCodexHooks(allocator: std.mem.Allocator, config_path: []const u8, scri // Check if notify is already configured with architect if (std.mem.indexOf(u8, content, "notify") != null and std.mem.indexOf(u8, content, "architect") != null) { // Replace existing architect notify line - var new_content = std.ArrayList(u8).init(allocator); - defer new_content.deinit(); + var new_content = std.ArrayList(u8){}; + defer new_content.deinit(allocator); var lines = std.mem.splitScalar(u8, content, '\n'); var first = true; while (lines.next()) |line| { if (!first) { - try new_content.append('\n'); + try new_content.append(allocator, '\n'); } first = false; const trimmed = std.mem.trim(u8, line, " \t"); if (std.mem.startsWith(u8, trimmed, "notify") and std.mem.indexOf(u8, line, "architect") != null) { - try new_content.appendSlice("notify = [\"python3\", \""); - try new_content.appendSlice(script_path); - try new_content.appendSlice("\"]"); + try new_content.appendSlice(allocator, "notify = [\"python3\", \""); + try new_content.appendSlice(allocator, script_path); + try new_content.appendSlice(allocator, "\"]"); } else { - try new_content.appendSlice(line); + try new_content.appendSlice(allocator, line); } } @@ -301,7 +301,7 @@ fn installCodexHooks(allocator: std.mem.Allocator, config_path: []const u8, scri out_file.writeAll(new_content.items) catch return error.ConfigWriteFailed; } else if (std.mem.indexOf(u8, content, "notify")) |_| { // There's a different notify config - don't overwrite it - return; + return error.HookSkipped; } else { // Append notify line const out_file = std.fs.createFileAbsolute(config_path, .{ .truncate = false }) catch return error.ConfigWriteFailed; @@ -326,8 +326,8 @@ fn uninstallCodexHooks(allocator: std.mem.Allocator, config_path: []const u8) !v const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch return; defer allocator.free(content); - var new_content = std.ArrayList(u8).init(allocator); - defer new_content.deinit(); + var new_content = std.ArrayList(u8){}; + defer new_content.deinit(allocator); var lines = std.mem.splitScalar(u8, content, '\n'); var first = true; @@ -337,10 +337,10 @@ fn uninstallCodexHooks(allocator: std.mem.Allocator, config_path: []const u8) !v continue; } if (!first) { - try new_content.append('\n'); + new_content.append(allocator, '\n') catch return; } first = false; - try new_content.appendSlice(line); + new_content.appendSlice(allocator, line) catch return; } const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return; @@ -387,8 +387,8 @@ fn installGeminiHooks(allocator: std.mem.Allocator, config_path: []const u8, scr if (root != .object) return error.JsonParseFailed; // Build commands - const after_cmd = try std.fmt.allocPrint(alloc, "python3 {s} done", .{script_path}); - const notif_cmd = try std.fmt.allocPrint(alloc, "python3 {s} awaiting_approval", .{script_path}); + const after_cmd = try std.fmt.allocPrint(alloc, "python3 {s} done || true", .{script_path}); + const notif_cmd = try std.fmt.allocPrint(alloc, "python3 {s} awaiting_approval || true", .{script_path}); // Create hooks structure var hooks: std.json.ObjectMap = undefined; @@ -458,8 +458,7 @@ fn installGeminiHooks(allocator: std.mem.Allocator, config_path: []const u8, scr const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return error.ConfigWriteFailed; defer out_file.close(); - std.json.stringify(root, .{ .whitespace = .indent_2 }, out_file.writer()) catch return error.ConfigWriteFailed; - out_file.writer().writeByte('\n') catch return error.ConfigWriteFailed; + writeJsonToFile(out_file, root) catch return error.ConfigWriteFailed; } fn uninstallGeminiHooks(allocator: std.mem.Allocator, config_path: []const u8) !void { @@ -502,8 +501,7 @@ fn uninstallGeminiHooks(allocator: std.mem.Allocator, config_path: []const u8) ! const out_file = std.fs.createFileAbsolute(config_path, .{}) catch return; defer out_file.close(); - std.json.stringify(root, .{ .whitespace = .indent_2 }, out_file.writer()) catch return; - out_file.writer().writeByte('\n') catch return; + writeJsonToFile(out_file, root) catch return; } // ============================================================================ @@ -568,6 +566,12 @@ pub fn install(allocator: std.mem.Allocator, tool: Tool, writer: anytype) !void defer allocator.free(config_path); installCodexHooks(allocator, config_path, main_script_path) catch |err| { + if (err == error.HookSkipped) { + try writer.print("\nSkipped: {s} already contains a 'notify' setting.\n", .{config_path}); + try writer.writeAll("Please manually add the Architect notifier to your existing notify configuration:\n"); + try writer.print(" notify = [\"python3\", \"{s}\"]\n", .{main_script_path}); + return; + } try writer.print("Error updating {s}: {}\n", .{ config_path, err }); return; }; diff --git a/src/main.zig b/src/main.zig index 5d2e6b4..2202c92 100644 --- a/src/main.zig +++ b/src/main.zig @@ -129,29 +129,29 @@ pub fn main() !void { defer std.process.argsFree(allocator, args); const command = cli.parse(args) catch |err| { - const stderr = std.io.getStdErr().writer(); + const stderr = std.fs.File.stderr().deprecatedWriter(); cli.printError(err, stderr) catch {}; std.process.exit(1); }; switch (command) { .help => { - const stdout = std.io.getStdOut().writer(); + const stdout = std.fs.File.stdout().deprecatedWriter(); cli.printUsage(stdout) catch {}; return; }, .version => { - const stdout = std.io.getStdOut().writer(); + const stdout = std.fs.File.stdout().deprecatedWriter(); stdout.writeAll("Architect 0.35.0\n") catch {}; return; }, .hook => |h| { - const stdout = std.io.getStdOut().writer(); + const stdout = std.fs.File.stdout().deprecatedWriter(); switch (h.action) { .install => { if (h.tool) |tool| { hook_manager.install(allocator, tool, stdout) catch |err| { - const stderr = std.io.getStdErr().writer(); + const stderr = std.fs.File.stderr().deprecatedWriter(); stderr.print("Error: {}\n", .{err}) catch {}; std.process.exit(1); }; @@ -160,7 +160,7 @@ pub fn main() !void { .uninstall => { if (h.tool) |tool| { hook_manager.uninstall(allocator, tool, stdout) catch |err| { - const stderr = std.io.getStdErr().writer(); + const stderr = std.fs.File.stderr().deprecatedWriter(); stderr.print("Error: {}\n", .{err}) catch {}; std.process.exit(1); }; @@ -168,7 +168,7 @@ pub fn main() !void { }, .status => { hook_manager.status(allocator, stdout) catch |err| { - const stderr = std.io.getStdErr().writer(); + const stderr = std.fs.File.stderr().deprecatedWriter(); stderr.print("Error: {}\n", .{err}) catch {}; std.process.exit(1); }; @@ -1272,14 +1272,14 @@ pub fn main() !void { continue; } - const command = buildCreateWorktreeCommand(allocator, create_action.base_path, create_action.name) catch |err| { + const worktree_cmd = buildCreateWorktreeCommand(allocator, create_action.base_path, create_action.name) catch |err| { std.debug.print("Failed to build worktree command: {}\n", .{err}); ui.showToast("Could not create worktree", now); continue; }; - defer allocator.free(command); + defer allocator.free(worktree_cmd); - session.sendInput(command) catch |err| { + session.sendInput(worktree_cmd) catch |err| { std.debug.print("Failed to send worktree command: {}\n", .{err}); ui.showToast("Could not create worktree", now); continue; @@ -1328,14 +1328,14 @@ pub fn main() !void { } } - const command = buildRemoveWorktreeCommand(allocator, remove_action.path) catch |err| { + const remove_cmd = buildRemoveWorktreeCommand(allocator, remove_action.path) catch |err| { std.debug.print("Failed to build remove worktree command: {}\n", .{err}); ui.showToast("Could not remove worktree", now); continue; }; - defer allocator.free(command); + defer allocator.free(remove_cmd); - session.sendInput(command) catch |err| { + session.sendInput(remove_cmd) catch |err| { std.debug.print("Failed to send remove worktree command: {}\n", .{err}); ui.showToast("Could not remove worktree", now); continue;