-- main.lua — entry point -- Phase 0: arg parsing, config load, REPL start. -- See docs/PHASE0.md §4, §10. -p one-shot mode lands per issue #4. -- Make project modules and the vendored dkjson resolvable from the repo root. -- Run aish with the repo root as cwd; PTY-relative resolution lands later. package.path = "./?.lua;./vendor/?.lua;" .. package.path local USAGE = [[ aish — AI-augmented conversational shell. Usage: luajit main.lua [--config ] [--help] -- interactive REPL luajit main.lua -p "" [--config ] -- one-shot, print + exit In -p mode, if stdin is not a TTY it's read as additional context and prepended to the prompt as a fenced block — composes with Unix pipes: tail app.log | aish -p "any anomalies?" Config resolution order (PHASE0.md §10): 1. --config 2. $AISH_CONFIG 3. ~/.config/aish/config.lua 4. ./config.lua ]] local function parse_args(argv) local out = {} local i = 1 while i <= #argv do local a = argv[i] if a == "--config" then out.config = argv[i + 1] i = i + 2 elseif a == "--help" or a == "-h" then out.help = true i = i + 1 elseif a == "-p" or a == "--prompt" then out.prompt = argv[i + 1] if not out.prompt then io.stderr:write("aish: -p requires a prompt argument\n") os.exit(2) end i = i + 2 else io.stderr:write("aish: unrecognized argument: " .. a .. "\n") os.exit(2) end end return out end local function load_config(opts) -- --config is explicit: use exactly that path or fail. No silent fallback. if opts.config then local f = io.open(opts.config, "r") if not f then error("aish: --config " .. opts.config .. ": cannot open") end f:close() return dofile(opts.config), opts.config end local home = os.getenv("HOME") or "" local candidates = {} local function push(p) if p and p ~= "" then candidates[#candidates + 1] = p end end push(os.getenv("AISH_CONFIG")) push(home .. "/.config/aish/config.lua") push("./config.lua") for _, path in ipairs(candidates) do local f = io.open(path, "r") if f then f:close(); return dofile(path), path end end error("aish: no config.lua found (tried: " .. table.concat(candidates, ", ") .. ")") end -- One-shot mode: read non-TTY stdin (if any), compose prompt, stream -- broker reply to stdout, exit. Bypasses repl.lua entirely — no REPL, -- no MCP, no tool loop, no Norris. The model's reply is printed -- verbatim (including any "CMD:" lines, which are NOT executed in -- this mode by design — the user can pipe-grep them as they wish). local function run_one_shot(config, user_prompt) local libc = require("ffi.libc") local broker = require("broker") local composed = user_prompt if not libc.isatty(0) then local piped = io.read("*a") or "" if piped ~= "" then composed = "```\n" .. piped .. "\n```\n\n" .. user_prompt end end local model_name = config.default_model local model_cfg = config.models and config.models[model_name] if not model_cfg then io.stderr:write(("aish: default_model '%s' not found in models{}\n") :format(tostring(model_name))) os.exit(2) end local messages = { { role = "user", content = composed } } local got_any = false local ok, err = broker.chat_stream(model_cfg, messages, function(kind, payload) if kind == "text" and payload and payload ~= "" then io.write(payload); io.flush() got_any = true end end) if not ok then if got_any then io.write("\n") end io.stderr:write("aish: broker error: " .. tostring(err) .. "\n") os.exit(1) end if got_any then io.write("\n") end end local function main(argv) local opts = parse_args(argv or {}) if opts.help then io.write(USAGE); return end local config, config_path = load_config(opts) io.stderr:write(("aish: loaded config from %s\n"):format(config_path)) if opts.prompt then run_one_shot(config, opts.prompt) return end local repl = require("repl") repl.run(config) end main(arg)