Compare commits
2 Commits
d02544fefc
...
357bcd7387
| Author | SHA1 | Date | |
|---|---|---|---|
| 357bcd7387 | |||
| 61d1d9da49 |
+2
-8
@@ -1,8 +1,2 @@
|
||||
# Zig build artifacts
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
|
||||
# Local editor/OS noise
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
/.zig-cache/
|
||||
/zig-out/
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -1,41 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Overview
|
||||
|
||||
`have` is a tiny compiled Zig CLI that lists executable command names available
|
||||
through `PATH`. It exists as an agent-safe availability primitive for comparing a
|
||||
run-tool allowlist against what the local Unix system can actually execute.
|
||||
|
||||
## How to build
|
||||
|
||||
```sh
|
||||
zig build
|
||||
```
|
||||
|
||||
## How to test
|
||||
|
||||
```sh
|
||||
zig build test
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- One source file: `have.zig`.
|
||||
- POSIX-shaped: inspect `PATH` directories and executable bits only.
|
||||
- No package-manager knowledge, shell builtins, aliases, functions, login shells,
|
||||
network access, mutation, config files, JSON default, color, pager, or daemon
|
||||
behavior.
|
||||
- stdout is one command name per line; diagnostics go to stderr.
|
||||
- This is an inspection primitive. Keep OS/package inventory in separate tools if
|
||||
it is ever needed.
|
||||
|
||||
## Requirement Governance
|
||||
|
||||
`REQUIREMENTS.md` is the requirement authority for this repository and is checked
|
||||
with `redgate`. Before implementation work, read the applicable requirement row;
|
||||
if none covers durable behavior, update `REQUIREMENTS.md` first or stop and ask.
|
||||
For behavior intended to stick, add or update a failing smoke/contract proof
|
||||
before implementation unless that would be fake or disproportionate. Before
|
||||
handoff, compare the user request and diff against `REQUIREMENTS.md`; report
|
||||
`REQUIREMENT IMPACT: updated <rows>` or exactly
|
||||
`REQUIREMENT IMPACT: none - <specific reason>`.
|
||||
@@ -0,0 +1,10 @@
|
||||
# have — migrated
|
||||
|
||||
The canonical `have` agent tool moved to `/opt/repositories/toolset`.
|
||||
|
||||
This repository no longer builds or releases an executable. Its Git history is the migration record; source, requirements, tests, installation, and release authority now live in toolset.
|
||||
|
||||
```sh
|
||||
cd /opt/repositories/toolset
|
||||
zig build install-text -p "$HOME/.ink"
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
# REQUIREMENTS
|
||||
|
||||
## cli
|
||||
|
||||
ring id requirement
|
||||
0 001 `have` must be a compiled single-file Zig CLI with no runtime dependencies beyond the OS/libc boundary needed for POSIX executable checks.
|
||||
0 002 `have` must inspect command availability through `PATH` only; it must not mutate files, execute discovered commands, invoke a shell, inspect package managers, access the network, read hidden config, prompt, page, colorize, or emit JSON by default.
|
||||
1 003 With no arguments, `have` must stream executable file names discovered in `PATH`, one per stdout line, de-duplicated by first executable `PATH` occurrence without collecting the full listing before writing records.
|
||||
1 004 With `NAME...` arguments, `have` must print only available command names found through `PATH`, exit 0 when all requested names are available, and exit nonzero when any requested name is missing or not a command name.
|
||||
1 005 `have` must not report shell builtins, aliases, shell functions, directories, non-executable files, or package installation state unless they also exist as executable files in `PATH`.
|
||||
1 006 Usage-error and runtime diagnostics must go to stderr and never corrupt stdout records.
|
||||
1 007 `zig build test` must prove PATH listing, executable-bit filtering, first-occurrence de-duplication, query output, missing-command failure, help stdout behavior, and that inspected commands are not executed.
|
||||
1 008 `have --help` and `have -h` must print manual text to stdout and exit 0 without emitting stderr.
|
||||
@@ -1,84 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "have",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("have.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
}),
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
b.step("run", "Run have").dependOn(&run_cmd.step);
|
||||
|
||||
const tests = b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
});
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
|
||||
const smoke = b.addSystemCommand(&.{ "sh", "smoke.sh", b.getInstallPath(.bin, "have") });
|
||||
smoke.setCwd(b.path("."));
|
||||
smoke.step.dependOn(b.getInstallStep());
|
||||
|
||||
const contract = b.addSystemCommand(&.{ "sh", "-c",
|
||||
\\set -eu
|
||||
\\tool=$1
|
||||
\\root=.zig-cache/have-contract
|
||||
\\rm -rf "$root"
|
||||
\\mkdir -p "$root/a" "$root/b" "$root/dup-a" "$root/dup-b" "$root/filter"
|
||||
\\cat > "$root/a/alpha" <<'SCRIPT'
|
||||
\\#!/bin/sh
|
||||
\\echo ran >> .zig-cache/have-contract/ran
|
||||
\\SCRIPT
|
||||
\\cat > "$root/b/beta" <<'SCRIPT'
|
||||
\\#!/bin/sh
|
||||
\\echo ran >> .zig-cache/have-contract/ran
|
||||
\\SCRIPT
|
||||
\\cat > "$root/dup-a/dup" <<'SCRIPT'
|
||||
\\#!/bin/sh
|
||||
\\echo ran >> .zig-cache/have-contract/ran
|
||||
\\SCRIPT
|
||||
\\cat > "$root/dup-b/dup" <<'SCRIPT'
|
||||
\\#!/bin/sh
|
||||
\\echo ran >> .zig-cache/have-contract/ran
|
||||
\\SCRIPT
|
||||
\\chmod +x "$root/a/alpha" "$root/b/beta" "$root/dup-a/dup" "$root/dup-b/dup"
|
||||
\\printf nope > "$root/filter/not-exec"
|
||||
\\mkdir "$root/filter/exec-dir"
|
||||
\\chmod +x "$root/filter/exec-dir"
|
||||
\\ln -s exec-dir "$root/filter/dir-link"
|
||||
\\PATH="$root/a:$root/b" "$tool" > "$root/list.out"
|
||||
\\test "$(sed -n '1p' "$root/list.out")" = alpha
|
||||
\\test "$(sed -n '2p' "$root/list.out")" = beta
|
||||
\\test ! -e "$root/ran"
|
||||
\\PATH="$root/dup-a:$root/dup-b" "$tool" > "$root/dup.out"
|
||||
\\test "$(grep -c '^dup$' "$root/dup.out")" = 1
|
||||
\\PATH="$root/filter" "$tool" > "$root/filter.out"
|
||||
\\! grep -q '^not-exec$' "$root/filter.out"
|
||||
\\! grep -q '^exec-dir$' "$root/filter.out"
|
||||
\\! grep -q '^dir-link$' "$root/filter.out"
|
||||
\\PATH="$root/a" "$tool" alpha > "$root/query.out"
|
||||
\\grep -q '^alpha$' "$root/query.out"
|
||||
\\test ! -e "$root/ran"
|
||||
\\PATH="$root/a" "$tool" ./alpha > "$root/slash.out" && exit 1 || true
|
||||
\\test ! -s "$root/slash.out"
|
||||
\\"$tool" --help > "$root/help.out" 2> "$root/help.err"
|
||||
\\grep -q '^usage:' "$root/help.out"
|
||||
\\test ! -s "$root/help.err"
|
||||
, "have-contract", b.getInstallPath(.bin, "have") });
|
||||
contract.setCwd(b.path("."));
|
||||
contract.step.dependOn(b.getInstallStep());
|
||||
|
||||
const test_step = b.step("test", "Run unit and smoke tests");
|
||||
test_step.dependOn(&run_tests.step);
|
||||
test_step.dependOn(&smoke.step);
|
||||
test_step.dependOn(&contract.step);
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
fn usage(file: std.Io.File, io: std.Io) !void {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var w = file.writer(io, &buf);
|
||||
try w.interface.writeAll(
|
||||
\\usage: have [NAME...]
|
||||
\\
|
||||
\\List executable command names found in PATH, one per line.
|
||||
\\With NAME args, print the names that are available and exit 1 if any are missing.
|
||||
\\This inspects PATH only; it does not know shell builtins, aliases, functions, or packages.
|
||||
\\
|
||||
\\stdout:
|
||||
\\ one command name per line
|
||||
\\
|
||||
\\examples:
|
||||
\\ have | grep '^zig$'
|
||||
\\ have zig git curl
|
||||
\\
|
||||
);
|
||||
try w.interface.flush();
|
||||
}
|
||||
|
||||
fn die(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn {
|
||||
var buf: [512]u8 = undefined;
|
||||
var w = std.Io.File.stderr().writer(io, &buf);
|
||||
w.interface.print(fmt, args) catch {};
|
||||
w.interface.flush() catch {};
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
fn pathEnv() []const u8 {
|
||||
const raw = std.c.getenv("PATH") orelse return "";
|
||||
return std.mem.span(raw);
|
||||
}
|
||||
|
||||
fn commandName(name: []const u8) bool {
|
||||
return name.len != 0 and std.mem.indexOfScalar(u8, name, '/') == null;
|
||||
}
|
||||
|
||||
fn executableFileInDir(dir: std.Io.Dir, io: std.Io, name: []const u8) bool {
|
||||
const stat = dir.statFile(io, name, .{}) catch return false;
|
||||
if (stat.kind != .file) return false;
|
||||
dir.access(io, name, .{ .execute = true }) catch return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn haveOne(io: std.Io, name: []const u8) bool {
|
||||
if (!commandName(name)) return false;
|
||||
|
||||
var dirs = std.mem.splitScalar(u8, pathEnv(), ':');
|
||||
while (dirs.next()) |dir_path| {
|
||||
const actual_dir = if (dir_path.len == 0) "." else dir_path;
|
||||
var dir = std.Io.Dir.cwd().openDir(io, actual_dir, .{}) catch continue;
|
||||
defer dir.close(io);
|
||||
if (executableFileInDir(dir, io, name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn listDir(io: std.Io, writer: *std.Io.Writer, seen: *std.StringHashMap(void), dir_path: []const u8) !void {
|
||||
var dir = std.Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }) catch return;
|
||||
defer dir.close(io);
|
||||
|
||||
var it = dir.iterate();
|
||||
while (try it.next(io)) |entry| {
|
||||
if (!commandName(entry.name)) continue;
|
||||
if (seen.contains(entry.name)) continue;
|
||||
if (!executableFileInDir(dir, io, entry.name)) continue;
|
||||
|
||||
const owned = try seen.allocator.dupe(u8, entry.name);
|
||||
errdefer seen.allocator.free(owned);
|
||||
try seen.put(owned, {});
|
||||
try writer.print("{s}\n", .{entry.name});
|
||||
}
|
||||
}
|
||||
|
||||
fn listAll(allocator: std.mem.Allocator, io: std.Io, writer: *std.Io.Writer) !void {
|
||||
var seen = std.StringHashMap(void).init(allocator);
|
||||
defer {
|
||||
var keys = seen.keyIterator();
|
||||
while (keys.next()) |key| allocator.free(key.*);
|
||||
seen.deinit();
|
||||
}
|
||||
|
||||
var dirs = std.mem.splitScalar(u8, pathEnv(), ':');
|
||||
while (dirs.next()) |dir_path| {
|
||||
const actual_dir = if (dir_path.len == 0) "." else dir_path;
|
||||
try listDir(io, writer, &seen, actual_dir);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
const allocator = init.gpa;
|
||||
var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator);
|
||||
defer args.deinit();
|
||||
_ = args.skip();
|
||||
|
||||
var out_buf: [8192]u8 = undefined;
|
||||
var out = std.Io.File.stdout().writer(init.io, &out_buf);
|
||||
|
||||
var names = std.ArrayList([]const u8).empty;
|
||||
defer names.deinit(allocator);
|
||||
while (args.next()) |arg| {
|
||||
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||
try usage(std.Io.File.stdout(), init.io);
|
||||
return;
|
||||
}
|
||||
try names.append(allocator, arg);
|
||||
}
|
||||
|
||||
if (names.items.len == 0) {
|
||||
listAll(allocator, init.io, &out.interface) catch |err| die(init.io, "have: {s}\n", .{@errorName(err)});
|
||||
try out.interface.flush();
|
||||
return;
|
||||
}
|
||||
|
||||
var missing = false;
|
||||
for (names.items) |name| {
|
||||
if (haveOne(init.io, name)) {
|
||||
try out.interface.print("{s}\n", .{name});
|
||||
} else {
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
try out.interface.flush();
|
||||
if (missing) std.process.exit(1);
|
||||
}
|
||||
|
||||
test commandName {
|
||||
try std.testing.expect(commandName("zig"));
|
||||
try std.testing.expect(commandName("."));
|
||||
try std.testing.expect(!commandName(""));
|
||||
try std.testing.expect(!commandName("./zig"));
|
||||
try std.testing.expect(!commandName("bin/zig"));
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
tool=${1:-zig-out/bin/have}
|
||||
mkdir -p .zig-cache/have-smoke/bin
|
||||
cat > .zig-cache/have-smoke/bin/ok <<'SCRIPT'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
SCRIPT
|
||||
chmod +x .zig-cache/have-smoke/bin/ok
|
||||
printf 'nope' > .zig-cache/have-smoke/bin/not-exec
|
||||
|
||||
PATH=.zig-cache/have-smoke/bin "$tool" > .zig-cache/have-list.out
|
||||
grep -q '^ok$' .zig-cache/have-list.out
|
||||
if grep -q '^not-exec$' .zig-cache/have-list.out; then
|
||||
echo 'have: listed non-executable file' >&2
|
||||
exit 1
|
||||
fi
|
||||
PATH=.zig-cache/have-smoke/bin "$tool" ok missing > .zig-cache/have-query.out && {
|
||||
echo 'have: query succeeded despite missing command' >&2
|
||||
exit 1
|
||||
}
|
||||
grep -q '^ok$' .zig-cache/have-query.out
|
||||
if grep -q '^missing$' .zig-cache/have-query.out; then
|
||||
echo 'have: printed missing command' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$tool" --help > .zig-cache/have-help.out 2> .zig-cache/have-help.err
|
||||
grep -q '^usage: have \[NAME\.\.\.\]' .zig-cache/have-help.out
|
||||
if [ -s .zig-cache/have-help.err ]; then
|
||||
echo 'have: --help wrote stderr' >&2
|
||||
exit 1
|
||||
fi
|
||||
"$tool" -h > .zig-cache/have-help-short.out 2> .zig-cache/have-help-short.err
|
||||
grep -q '^usage: have \[NAME\.\.\.\]' .zig-cache/have-help-short.out
|
||||
if [ -s .zig-cache/have-help-short.err ]; then
|
||||
echo 'have: -h wrote stderr' >&2
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user