-- main.wo — Main.hx, file for file: subcommand dispatch, config decode. -- A free `fn main(args) -> Int` makes this project a PROGRAM (systems-track -- spec Part 2): `wo run` executes it, the return value is the exit code, -- blocking builtins are legal on the single shard. -- -- Main.hx:5-7's `#if portable` linker pragma has no equivalent and needs -- none: `woc build` produces a self-contained static binary by doctrine. -- Util.hx is gone entirely: print flushes on newline (spec Part 2), which -- is the only thing Util.say existed to do. use fs use env use time use json -- config.json, decoded typed: Main.hx:50-61's Dynamic field-poking becomes -- one checked decode — missing optional fields are fine, a shape mismatch -- yields nil, never a trap. Field names are the wire format (camelCase); -- values are seconds on the wire, ms internally. typedef FileConfig = { ?pollInterval: Int ?quietPeriod: Int ?rescanInterval: Int ?detections: Text ?services: multi Text ?logs: multi Text ?mcp: McpConfig } typedef McpConfig = { ?port: Int, ?apiKey: Text } fn main(args: multi Text) -> Int { if len(args) >= 2 and args[0] == "watch" { let quiet_s = 10; if len(args) >= 3 { let q = parse_int(args[2]); if q != nil { quiet_s = q; } } let poll_s = 2; if len(args) >= 4 { let p = parse_int(args[3]); if p != nil { poll_s = p; } } let w = Watcher { path: args[1], quiet_ms: quiet_s * 1000, poll_ms: poll_s * 1000, live: true, activated_at: time.now() }; print("watching ${args[1]} (quiet ${quiet_s}s, poll ${poll_s}s)"); while true { if env.stopping() { return 0; } w.tick(time.now()); time.sleep(poll_s * 1000); } } if len(args) >= 2 and args[0] == "run" { let cfg = SupConfig {}; -- field defaults = Main.hx:21's literal if len(args) >= 3 { if load_config(args[2], cfg) == false { return 1; } } print("supervising ${args[1]} (poll ${cfg.poll_ms / 1000}s, quiet ${cfg.quiet_ms / 1000}s, rescan ${cfg.rescan_ms / 1000}s)"); let sup = Supervisor { cron_dir: args[1], cfg: cfg }; sup.init(); return sup.run(); } if len(args) >= 3 and args[0] == "mcp" { let raw = fs.read_all(args[2], 1048576); if raw == nil { print_err("config error: cannot read ${args[2]}"); return 1; } let j = json.decode(raw) as FileConfig; if j == nil { print_err("config error: ${args[2]} is not valid JSON"); return 1; } -- the key may live outside the config file (systemd EnvironmentFile / .env) let api_key = env.get("LOG_WATCHER_API_KEY"); let port: ?Int = nil; if j.mcp != nil { if j.mcp.apiKey != nil { api_key = j.mcp.apiKey; } port = j.mcp.port; } if port == nil or api_key == nil { print_err("config error: mcp.port and an api key (mcp.apiKey or LOG_WATCHER_API_KEY) are required"); return 1; } let extra: multi Text = []; if j.logs != nil { for p in j.logs { push(extra, p); } } if j.services != nil { for s in j.services { push(extra, s); } } let mcp = Mcp { tools: Tools { cron_dir: args[1], extra_logs: extra }, api_key: api_key }; print("mcp server on 127.0.0.1:${port} (${args[1]})"); return mcp.serve(port); } print_err("usage:"); print_err(" log-watcher watch [quietPeriod] [pollInterval] continuous service watch"); print_err(" log-watcher run [config.json] supervisor (cron + services)"); print_err(" log-watcher mcp MCP server (127.0.0.1, Bearer auth)"); return 1; } fn load_config(path: Text, mut cfg: SupConfig) -> Bool { let raw = fs.read_all(path, 1048576); if raw == nil { print_err("config error: cannot read ${path}"); return false; } let j = json.decode(raw) as FileConfig; if j == nil { print_err("config error: ${path} is not valid JSON"); return false; } if j.pollInterval != nil { cfg.poll_ms = j.pollInterval * 1000; } if j.quietPeriod != nil { cfg.quiet_ms = j.quietPeriod * 1000; } if j.rescanInterval != nil { cfg.rescan_ms = j.rescanInterval * 1000; } if j.detections != nil { cfg.detections = j.detections; } if j.services != nil { for s in j.services { push(cfg.services, s); } } return true; }