148 lines
4.8 KiB
Zig
148 lines
4.8 KiB
Zig
const std = @import("std");
|
|
const protocol = @import("protocol.zig");
|
|
const session = @import("session.zig");
|
|
const socket = @import("socket.zig");
|
|
|
|
pub const version = "0.1.0-dev";
|
|
|
|
const help_text =
|
|
\\mim - mobile-first terminal code editor for SSH sessions
|
|
\\
|
|
\\Usage:
|
|
\\ mim [--help]
|
|
\\ mim [--version]
|
|
\\ mim --mimctl <socket|-> <request...>
|
|
\\
|
|
\\This early build exposes the canonical smoke path and a minimal local
|
|
\\session-control client for trusted tools.
|
|
\\
|
|
;
|
|
|
|
const Command = union(enum) {
|
|
help,
|
|
version,
|
|
smoke,
|
|
unknown: []const u8,
|
|
};
|
|
|
|
fn parseArgs(args: []const []const u8) Command {
|
|
if (args.len <= 1) return .smoke;
|
|
|
|
const arg = args[1];
|
|
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) return .help;
|
|
if (std.mem.eql(u8, arg, "--version")) return .version;
|
|
return .{ .unknown = arg };
|
|
}
|
|
|
|
fn versionText() []const u8 {
|
|
return "mim " ++ version ++ "\n";
|
|
}
|
|
|
|
pub fn main(init: std.process.Init) !u8 {
|
|
var debug_allocator = std.heap.DebugAllocator(.{}){};
|
|
defer _ = debug_allocator.deinit();
|
|
const allocator = debug_allocator.allocator();
|
|
|
|
var args = std.process.Args.Iterator.init(init.minimal.args);
|
|
_ = args.skip();
|
|
|
|
const stdout = std.Io.File.stdout();
|
|
const stderr = std.Io.File.stderr();
|
|
|
|
const first_arg = args.next();
|
|
if (first_arg) |arg| {
|
|
if (std.mem.eql(u8, arg, "--mimctl")) {
|
|
const socket_path = args.next() orelse {
|
|
try stderr.writeStreamingAll(init.io, "mim: --mimctl requires a socket path\n");
|
|
return 64;
|
|
};
|
|
const line = try collectRemainingArgs(allocator, &args);
|
|
defer allocator.free(line);
|
|
if (line.len == 0) {
|
|
try stderr.writeStreamingAll(init.io, "mim: --mimctl requires a request\n");
|
|
return 64;
|
|
}
|
|
const resolved_socket_path = if (std.mem.eql(u8, socket_path, "-"))
|
|
std.process.Environ.getAlloc(init.minimal.environ, allocator, socket.socket_env_name) catch {
|
|
try stderr.writeStreamingAll(init.io, "mim: MIM_SOCKET is not set\n");
|
|
return 69;
|
|
}
|
|
else
|
|
try allocator.dupe(u8, socket_path);
|
|
defer allocator.free(resolved_socket_path);
|
|
|
|
const response = socket.request(allocator, resolved_socket_path, line) catch {
|
|
try stderr.writeStreamingAll(init.io, "mim: mimctl request failed\n");
|
|
return 69;
|
|
};
|
|
defer allocator.free(response);
|
|
try stdout.writeStreamingAll(init.io, response);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
const command = if (first_arg) |arg|
|
|
parseArgs(&.{ "mim", arg })
|
|
else
|
|
parseArgs(&.{"mim"});
|
|
|
|
switch (command) {
|
|
.help => try stdout.writeStreamingAll(init.io, help_text),
|
|
.version => try stdout.writeStreamingAll(init.io, versionText()),
|
|
.smoke => try stdout.writeStreamingAll(init.io, help_text),
|
|
.unknown => |arg| {
|
|
try stderr.writeStreamingAll(init.io, "mim: unknown argument '");
|
|
try stderr.writeStreamingAll(init.io, arg);
|
|
try stderr.writeStreamingAll(init.io, "'\nTry 'mim --help'.\n");
|
|
return 64;
|
|
},
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
fn collectRemainingArgs(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) ![]u8 {
|
|
var line = std.ArrayList(u8).empty;
|
|
errdefer line.deinit(allocator);
|
|
var first = true;
|
|
while (args.next()) |arg| {
|
|
if (!first) try line.append(allocator, ' ');
|
|
try line.appendSlice(allocator, arg);
|
|
first = false;
|
|
}
|
|
return line.toOwnedSlice(allocator);
|
|
}
|
|
|
|
test {
|
|
_ = protocol;
|
|
_ = session;
|
|
_ = socket;
|
|
}
|
|
|
|
test "regular: help text names the binary and smoke boundary" {
|
|
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim - mobile-first terminal code editor") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, help_text, "canonical smoke path") != null);
|
|
}
|
|
|
|
test "regular: version output is stable enough for smoke checks" {
|
|
try std.testing.expectEqualStrings("mim 0.1.0-dev\n", versionText());
|
|
}
|
|
|
|
test "adversarial: unknown arguments are rejected instead of ignored" {
|
|
const argv = [_][]const u8{ "mim", "--definitely-not-supported" };
|
|
const parsed = parseArgs(&argv);
|
|
switch (parsed) {
|
|
.unknown => |arg| try std.testing.expectEqualStrings("--definitely-not-supported", arg),
|
|
else => return error.ExpectedUnknownArgument,
|
|
}
|
|
}
|
|
|
|
test "adversarial: extra positional arguments are not treated as files yet" {
|
|
const argv = [_][]const u8{ "mim", "some-file.zig" };
|
|
const parsed = parseArgs(&argv);
|
|
switch (parsed) {
|
|
.unknown => |arg| try std.testing.expectEqualStrings("some-file.zig", arg),
|
|
else => return error.ExpectedUnknownArgument,
|
|
}
|
|
}
|