aiur-cli 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/bin/aiur.js +147 -5
- package/libexec/aiur-engine.sh +695 -0
- package/package.json +13 -2
- package/share/aiur.tmux.conf +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# AIUR
|
|
2
|
+
|
|
3
|
+
**AI Unit Runtime for Executors**
|
|
4
|
+
|
|
5
|
+
Oversee a fleet of coding agents that **coordinate via events**.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g aiur-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires [tmux](https://github.com/tmux/tmux) at runtime. Install
|
|
14
|
+
[opencode](https://opencode.ai) too for in-pane agent chat.
|
|
15
|
+
|
|
16
|
+
## Get started
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cd your-repo
|
|
20
|
+
aiur init # interactive setup wizard
|
|
21
|
+
aiur # start agents (foreground); use `aiur --bg` to detach
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`aiur init` scaffolds `.aiurconfig` and provisions the repo. It walks you through:
|
|
25
|
+
|
|
26
|
+
1. **Where to store config** — repo-local `./.aiurconfig` or global `~/.aiurconfig`.
|
|
27
|
+
2. **Tracker** — GitHub or Linear, plus the repo.
|
|
28
|
+
3. **Agents & routing** — Claude and/or Codex, optional per-complexity model
|
|
29
|
+
routing, and the permission mode.
|
|
30
|
+
4. **Limits** — max concurrent agents, turns, duration, pre-warmed sessions, and
|
|
31
|
+
polling interval.
|
|
32
|
+
5. **GitHub token** — to create labels and act as the bot account.
|
|
33
|
+
6. **Labels** — the lifecycle (`agent:*`), complexity, model, and remote-control
|
|
34
|
+
labels aiur routes on. Only missing labels are created; a group that already
|
|
35
|
+
exists is reported as created and skipped.
|
|
36
|
+
|
|
37
|
+
Re-running resumes from your saved answers. When setup finishes, add `agent:todo`
|
|
38
|
+
to the issues you want worked and run `aiur`.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | What it does |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `aiur init` | Interactive setup wizard (scaffold `.aiurconfig`) |
|
|
45
|
+
| `aiur` | Start the workflow in the foreground (local-only bind) |
|
|
46
|
+
| `aiur --bg` | Start in a detached tmux session |
|
|
47
|
+
| `aiur status` | Show active agents and their state |
|
|
48
|
+
| `aiur pause <id…>` / `resume <id…>` | Pause or resume agents by issue ID |
|
|
49
|
+
| `aiur stop` | Stop the running session |
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- **Issue Monitoring** — Dispatch agents automatically via GitHub or Linear tickets.
|
|
54
|
+
- **Shared Bus** — Agents auto-pub/sub to dependency events, unblocking work early.
|
|
55
|
+
- **Optimize Effort** — Story points guide model sizes and skills. Supports Claude and Codex.
|
|
56
|
+
- **Take the Wheel** — Drive at any moment via pre-warmed [Opencode](https://opencode.ai) sessions in configurable tmux panes.
|
|
57
|
+
- **Smart Alerts** — Customize notifications and sounds when agents finish, stall, or need a decision.
|
|
58
|
+
- **Live Dashboard** — Shareable web view of every agent, event, and progress bar.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
_Command macro, delegate micro, maximize APM._
|
|
63
|
+
|
|
64
|
+
[github.com/its-everdred/aiur](https://github.com/its-everdred/aiur)
|
package/bin/aiur.js
CHANGED
|
@@ -1,6 +1,148 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
);
|
|
6
|
-
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require("node:child_process");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
|
|
8
|
+
// Map this host to the platform package that carries its OTP release.
|
|
9
|
+
const PLATFORMS = {
|
|
10
|
+
"darwin arm64": "darwin-arm64",
|
|
11
|
+
"darwin x64": "darwin-x64",
|
|
12
|
+
"linux arm64": "linux-arm64",
|
|
13
|
+
"linux x64": "linux-x64",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MIN_TMUX = [3, 3];
|
|
17
|
+
|
|
18
|
+
function fail(message) {
|
|
19
|
+
process.stderr.write(`aiur: ${message}\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveReleaseDir() {
|
|
24
|
+
// A pre-set AIUR_RELEASE_DIR (the dev shim, or local `npm pack` install
|
|
25
|
+
// verification) wins over platform-package resolution.
|
|
26
|
+
const preset = process.env.AIUR_RELEASE_DIR;
|
|
27
|
+
if (preset) {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.statSync(preset).isDirectory()) return preset;
|
|
30
|
+
} catch (_) {
|
|
31
|
+
// fall through to the explicit error below
|
|
32
|
+
}
|
|
33
|
+
fail(`AIUR_RELEASE_DIR is set but not a directory: ${preset}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const triple = PLATFORMS[`${process.platform} ${process.arch}`];
|
|
37
|
+
if (!triple) {
|
|
38
|
+
fail(
|
|
39
|
+
`unsupported platform ${process.platform}/${process.arch}. ` +
|
|
40
|
+
"Supported: darwin/linux on arm64/x64. " +
|
|
41
|
+
"See https://github.com/its-everdred/aiur",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pkg = `aiur-cli-${triple}`;
|
|
46
|
+
let pkgJson;
|
|
47
|
+
try {
|
|
48
|
+
pkgJson = require.resolve(`${pkg}/package.json`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err && err.code === "MODULE_NOT_FOUND") {
|
|
51
|
+
fail(
|
|
52
|
+
`platform package "${pkg}" is not installed. ` +
|
|
53
|
+
"This usually means optional dependencies were skipped " +
|
|
54
|
+
"(--no-optional, a shared lockfile, or a known npm install bug). " +
|
|
55
|
+
"Reinstall aiur-cli, or see https://github.com/its-everdred/aiur",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return path.join(path.dirname(pkgJson), "release");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function tmuxVersion() {
|
|
65
|
+
const result = spawnSync("tmux", ["-V"], { encoding: "utf8" });
|
|
66
|
+
if (result.error || result.status !== 0) return null;
|
|
67
|
+
const match = /tmux\s+(\d+)\.(\d+)/.exec(result.stdout || "");
|
|
68
|
+
if (!match) return null;
|
|
69
|
+
return [Number(match[1]), Number(match[2])];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tmuxHint() {
|
|
73
|
+
return process.platform === "darwin"
|
|
74
|
+
? "install it with: brew install tmux"
|
|
75
|
+
: "install it with: apt install tmux (or your distro's package manager)";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function preflightTmux() {
|
|
79
|
+
const version = tmuxVersion();
|
|
80
|
+
if (!version) {
|
|
81
|
+
fail(`tmux is required but was not found on PATH. ${tmuxHint()}`);
|
|
82
|
+
}
|
|
83
|
+
const [major, minor] = version;
|
|
84
|
+
const [needMajor, needMinor] = MIN_TMUX;
|
|
85
|
+
const tooOld =
|
|
86
|
+
major < needMajor || (major === needMajor && minor < needMinor);
|
|
87
|
+
if (tooOld) {
|
|
88
|
+
fail(
|
|
89
|
+
`tmux ${major}.${minor} is too old; aiur needs ` +
|
|
90
|
+
`>= ${needMajor}.${needMinor}. ${tmuxHint()}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function preflightOpencode() {
|
|
96
|
+
const result = spawnSync("opencode", ["--version"], { stdio: "ignore" });
|
|
97
|
+
if (result.error || result.status !== 0) {
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
"aiur: opencode was not found on PATH. The interactive 'take the wheel' " +
|
|
100
|
+
"feature needs it; everything else works without it. " +
|
|
101
|
+
"Install from https://opencode.ai\n",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// `init` and `--version` run as foreground one-shots that never start the
|
|
107
|
+
// tmux-backed UI, so their tmux/opencode preflight is irrelevant — and tmux may
|
|
108
|
+
// legitimately be absent on a machine that only ever runs `aiur init`.
|
|
109
|
+
function isForegroundOneShot(argv) {
|
|
110
|
+
for (const arg of argv) {
|
|
111
|
+
if (arg === "--version") return true;
|
|
112
|
+
if (arg.startsWith("-")) continue;
|
|
113
|
+
return arg === "init";
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function main() {
|
|
119
|
+
const releaseDir = resolveReleaseDir();
|
|
120
|
+
|
|
121
|
+
if (!isForegroundOneShot(process.argv.slice(2))) {
|
|
122
|
+
preflightTmux();
|
|
123
|
+
preflightOpencode();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const pkgRoot = path.resolve(__dirname, "..");
|
|
127
|
+
const launcher = path.join(pkgRoot, "libexec", "aiur-engine.sh");
|
|
128
|
+
|
|
129
|
+
const result = spawnSync(launcher, process.argv.slice(2), {
|
|
130
|
+
stdio: "inherit",
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
AIUR_RELEASE_DIR: releaseDir,
|
|
134
|
+
AIUR_TMUX_CONF: path.join(pkgRoot, "share", "aiur.tmux.conf"),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (result.error) {
|
|
139
|
+
fail(`failed to launch ${launcher}: ${result.error.message}`);
|
|
140
|
+
}
|
|
141
|
+
if (result.signal) {
|
|
142
|
+
process.kill(process.pid, result.signal);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
main();
|
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Shared aiur launcher engine.
|
|
4
|
+
#
|
|
5
|
+
# Owns every aiur subcommand. Both the installed `aiur` (bin/aiur.js) and the dev
|
|
6
|
+
# `aiurdev` (scripts/aiurdev) exec this one engine, differing only in which
|
|
7
|
+
# release is run:
|
|
8
|
+
#
|
|
9
|
+
# AIUR_RELEASE_DIR release dir whose bin/aiur is exec'd (required; set by the caller)
|
|
10
|
+
#
|
|
11
|
+
# There is a single `aiur` distribution identity (node/cookie/session). aiurdev
|
|
12
|
+
# shares it. The identity vars resolve to the fixed `aiur` values; they remain
|
|
13
|
+
# overridable only so tests can redirect state to a temp dir:
|
|
14
|
+
#
|
|
15
|
+
# AIUR_BG_STATE_DIR cookie + state dir (default: ~/.config/aiur)
|
|
16
|
+
# AIUR_COOKIE_FILE cookie file (default: $AIUR_BG_STATE_DIR/cookie)
|
|
17
|
+
# AIUR_SESSION_PREFIX tmux session prefix (default: aiur)
|
|
18
|
+
# AIUR_PROFILES_FILE profiles file (default: ~/.config/aiur/aiur.profiles)
|
|
19
|
+
# AIUR_RELEASE_NODE full node name (default: aiur-$USER@127.0.0.1)
|
|
20
|
+
#
|
|
21
|
+
# The release is self-contained (bundled ERTS), so it runs without mise/Elixir on
|
|
22
|
+
# PATH. Dev's build-if-stale step lives in the aiurdev shim, not here.
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
die() {
|
|
27
|
+
echo "❌ $*" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
engine_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
|
|
33
|
+
# --- distribution identity (fixed: the single `aiur` identity) ---------------
|
|
34
|
+
|
|
35
|
+
aiur_resolve_identity() {
|
|
36
|
+
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
37
|
+
|
|
38
|
+
: "${AIUR_BG_STATE_DIR:=$config_home/aiur}"
|
|
39
|
+
: "${AIUR_COOKIE_FILE:=$AIUR_BG_STATE_DIR/cookie}"
|
|
40
|
+
: "${AIUR_SESSION_PREFIX:=aiur}"
|
|
41
|
+
: "${AIUR_PROFILES_FILE:=$config_home/aiur/aiur.profiles}"
|
|
42
|
+
: "${AIUR_RELEASE_NODE:=aiur-${USER}@127.0.0.1}"
|
|
43
|
+
|
|
44
|
+
export AIUR_BG_STATE_DIR AIUR_COOKIE_FILE AIUR_SESSION_PREFIX \
|
|
45
|
+
AIUR_PROFILES_FILE AIUR_RELEASE_NODE
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
aiur_print_identity() {
|
|
49
|
+
aiur_resolve_identity
|
|
50
|
+
printf 'AIUR_RELEASE_DIR=%s\n' "${AIUR_RELEASE_DIR:-}"
|
|
51
|
+
printf 'AIUR_BG_STATE_DIR=%s\n' "$AIUR_BG_STATE_DIR"
|
|
52
|
+
printf 'AIUR_SESSION_PREFIX=%s\n' "$AIUR_SESSION_PREFIX"
|
|
53
|
+
printf 'AIUR_PROFILES_FILE=%s\n' "$AIUR_PROFILES_FILE"
|
|
54
|
+
printf 'AIUR_RELEASE_NODE=%s\n' "$AIUR_RELEASE_NODE"
|
|
55
|
+
printf 'AIUR_COOKIE_FILE=%s\n' "$AIUR_COOKIE_FILE"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# --- BEAM distribution (cookie + named node) ---------------------------------
|
|
59
|
+
|
|
60
|
+
ensure_bg_state_dir() {
|
|
61
|
+
aiur_resolve_identity
|
|
62
|
+
mkdir -p "$AIUR_BG_STATE_DIR"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ensure_erlang_cookie() {
|
|
66
|
+
aiur_resolve_identity
|
|
67
|
+
ensure_bg_state_dir
|
|
68
|
+
|
|
69
|
+
local cookie_file="$AIUR_COOKIE_FILE"
|
|
70
|
+
|
|
71
|
+
if [ ! -f "$cookie_file" ]; then
|
|
72
|
+
(
|
|
73
|
+
umask 0177
|
|
74
|
+
tmp_file="$(mktemp "$AIUR_BG_STATE_DIR/cookie.XXXXXX")"
|
|
75
|
+
head -c 32 /dev/urandom | base64 | tr -d '\n=+/' | head -c 32 >"$tmp_file"
|
|
76
|
+
mv "$tmp_file" "$cookie_file"
|
|
77
|
+
)
|
|
78
|
+
chmod 0400 "$cookie_file"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
[ -r "$cookie_file" ] || die "$cookie_file is not readable"
|
|
82
|
+
|
|
83
|
+
if [ "$(stat -c '%U' "$cookie_file" 2>/dev/null || stat -f '%Su' "$cookie_file")" != "$USER" ]; then
|
|
84
|
+
die "$cookie_file is not owned by $USER"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
local size
|
|
88
|
+
size="$(wc -c <"$cookie_file" | tr -d ' ')"
|
|
89
|
+
[ "$size" -ge 16 ] || die "$cookie_file is shorter than 16 bytes"
|
|
90
|
+
|
|
91
|
+
printf '%s' "$cookie_file"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
prepare_distribution() {
|
|
95
|
+
aiur_resolve_identity
|
|
96
|
+
|
|
97
|
+
local cookie_file
|
|
98
|
+
cookie_file="$(ensure_erlang_cookie)" || return 1
|
|
99
|
+
|
|
100
|
+
local cookie
|
|
101
|
+
cookie="$(cat "$cookie_file")"
|
|
102
|
+
|
|
103
|
+
export RELEASE_DISTRIBUTION="name"
|
|
104
|
+
export RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
105
|
+
export RELEASE_COOKIE="$cookie"
|
|
106
|
+
# Pin the distribution listener to 127.0.0.1 too, matching the node-name IP.
|
|
107
|
+
# {127,0,0,1} is the Erlang tuple — unquoted because bash inside the
|
|
108
|
+
# surrounding double quotes does not brace-expand it.
|
|
109
|
+
export ERL_AFLAGS=" -proto_dist inet_tcp -kernel inet_dist_use_interface {127,0,0,1}"
|
|
110
|
+
export ERL_EPMD_ADDRESS="127.0.0.1"
|
|
111
|
+
export AIUR_NODE="$RELEASE_NODE"
|
|
112
|
+
export AIUR_ERLANG_COOKIE="$RELEASE_COOKIE"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# --- release resolution ------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
release_dir=""
|
|
118
|
+
vsn_dir=""
|
|
119
|
+
release_bin=""
|
|
120
|
+
|
|
121
|
+
resolve_release() {
|
|
122
|
+
release_dir="${AIUR_RELEASE_DIR:-}"
|
|
123
|
+
[ -n "$release_dir" ] || die "AIUR_RELEASE_DIR is not set; the engine must be invoked via the aiur or aiurdev wrapper"
|
|
124
|
+
[ -d "$release_dir" ] || die "AIUR_RELEASE_DIR does not exist: $release_dir"
|
|
125
|
+
|
|
126
|
+
local release_vsn
|
|
127
|
+
release_vsn="$(cut -d' ' -f2 "$release_dir/releases/start_erl.data")"
|
|
128
|
+
vsn_dir="$release_dir/releases/$release_vsn"
|
|
129
|
+
release_bin="$release_dir/bin/aiur"
|
|
130
|
+
[ -x "$vsn_dir/elixir" ] || die "release elixir launcher not found at $vsn_dir/elixir"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# --- argv round-trip (System.argv is empty under `elixir --eval`) -------------
|
|
134
|
+
|
|
135
|
+
argv_file=""
|
|
136
|
+
init_argv_file() {
|
|
137
|
+
argv_file="$(mktemp "${TMPDIR:-/tmp}/aiur-argv.XXXXXX")"
|
|
138
|
+
: >"$argv_file"
|
|
139
|
+
}
|
|
140
|
+
write_argv() {
|
|
141
|
+
local a
|
|
142
|
+
for a in "$@"; do printf '%s\n' "$a" >>"$argv_file"; done
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
release_cmd=()
|
|
146
|
+
build_release_cmd() {
|
|
147
|
+
release_cmd=(
|
|
148
|
+
"$vsn_dir/elixir"
|
|
149
|
+
--cookie "$RELEASE_COOKIE"
|
|
150
|
+
--name "$RELEASE_NODE"
|
|
151
|
+
--erl-config "$vsn_dir/sys"
|
|
152
|
+
--boot "$vsn_dir/start_clean"
|
|
153
|
+
--boot-var RELEASE_LIB "$release_dir/lib"
|
|
154
|
+
--vm-args "$vsn_dir/vm.args"
|
|
155
|
+
--eval "Aiur.CLI.main(Aiur.CLI.argv_from_file())"
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Distribution-free boot for the `init` wizard: same interactive `--eval` form
|
|
160
|
+
# (so the wizard's prompts receive keystrokes — `bin/aiur eval` is -noinput),
|
|
161
|
+
# but with no --name/--cookie since the wizard makes no RPC calls.
|
|
162
|
+
build_init_cmd() {
|
|
163
|
+
release_cmd=(
|
|
164
|
+
"$vsn_dir/elixir"
|
|
165
|
+
--erl-config "$vsn_dir/sys"
|
|
166
|
+
--boot "$vsn_dir/start_clean"
|
|
167
|
+
--boot-var RELEASE_LIB "$release_dir/lib"
|
|
168
|
+
--vm-args "$vsn_dir/vm.args"
|
|
169
|
+
--eval "Aiur.CLI.main(Aiur.CLI.argv_from_file())"
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
usage() {
|
|
174
|
+
cat <<'EOF'
|
|
175
|
+
Usage: aiur [--interactive] [--logs-root <path>] [--port <port>] [--host <host>] [path-to-.aiurconfig]
|
|
176
|
+
aiur init [--force] scaffold .aiurconfig (interactive setup wizard)
|
|
177
|
+
aiur --bg start in a detached tmux session
|
|
178
|
+
aiur stop stop the running session
|
|
179
|
+
aiur status show agent status
|
|
180
|
+
aiur pause <ids|--all> | resume <ids|--all>
|
|
181
|
+
aiur --version
|
|
182
|
+
EOF
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# --- one-shot: --version (no tmux) -------------------------------------------
|
|
186
|
+
|
|
187
|
+
run_version() {
|
|
188
|
+
resolve_release
|
|
189
|
+
# Distribution-free (like init): printing the version is a compile-time
|
|
190
|
+
# constant, so never claim the node name — otherwise `aiur --version` fails
|
|
191
|
+
# whenever an aiur session is already running.
|
|
192
|
+
build_init_cmd
|
|
193
|
+
init_argv_file
|
|
194
|
+
write_argv "$@"
|
|
195
|
+
export AIUR_ARGV_FILE="$argv_file"
|
|
196
|
+
exec "${release_cmd[@]}"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# --- one-shot: init (interactive wizard, distribution-free, no tmux) ----------
|
|
200
|
+
|
|
201
|
+
run_init() {
|
|
202
|
+
resolve_release
|
|
203
|
+
build_init_cmd
|
|
204
|
+
init_argv_file
|
|
205
|
+
write_argv "$@"
|
|
206
|
+
export AIUR_ARGV_FILE="$argv_file"
|
|
207
|
+
exec "${release_cmd[@]}"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# --- interactive / background run -------------------------------------------
|
|
211
|
+
#
|
|
212
|
+
# mode=foreground attaches the UI and tears down on exit; mode=background leaves
|
|
213
|
+
# the detached tmux session running and returns.
|
|
214
|
+
|
|
215
|
+
# Load KEY=VALUE pairs from ./.env into the environment so the running release
|
|
216
|
+
# (e.g. GITHUB_TOKEN, dashboard creds) sees what `aiur init` scaffolded there.
|
|
217
|
+
# An already-exported variable always wins, so a shell export overrides the file.
|
|
218
|
+
load_dotenv() {
|
|
219
|
+
local file=".env" line key val
|
|
220
|
+
[ -f "$file" ] || return 0
|
|
221
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
222
|
+
line="${line#"${line%%[![:space:]]*}"}"
|
|
223
|
+
case "$line" in '' | '#'*) continue ;; esac
|
|
224
|
+
[ "${line#*=}" = "$line" ] && continue
|
|
225
|
+
key="${line%%=*}"
|
|
226
|
+
key="${key%"${key##*[![:space:]]}"}"
|
|
227
|
+
case "$key" in '' | *[!A-Za-z0-9_]*) continue ;; esac
|
|
228
|
+
val="${line#*=}"
|
|
229
|
+
val="${val#"${val%%[![:space:]]*}"}"
|
|
230
|
+
case "$val" in
|
|
231
|
+
\"*\") val="${val#\"}" && val="${val%\"}" ;;
|
|
232
|
+
\'*\') val="${val#\'}" && val="${val%\'}" ;;
|
|
233
|
+
esac
|
|
234
|
+
[ -n "${!key+x}" ] && continue
|
|
235
|
+
export "$key=$val"
|
|
236
|
+
done <"$file"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
run_session() {
|
|
240
|
+
local mode="$1"
|
|
241
|
+
shift
|
|
242
|
+
|
|
243
|
+
# --debug is an engine-level convenience on the run path: turn on verbose
|
|
244
|
+
# logging (AIUR_DEBUG) and strip the flag before the release parses argv.
|
|
245
|
+
local run_args=() run_arg
|
|
246
|
+
for run_arg in "$@"; do
|
|
247
|
+
case "$run_arg" in
|
|
248
|
+
--debug) export AIUR_DEBUG=1 ;;
|
|
249
|
+
*) run_args+=("$run_arg") ;;
|
|
250
|
+
esac
|
|
251
|
+
done
|
|
252
|
+
if [ "${#run_args[@]}" -gt 0 ]; then set -- "${run_args[@]}"; else set --; fi
|
|
253
|
+
|
|
254
|
+
resolve_release
|
|
255
|
+
|
|
256
|
+
local tmux_bin
|
|
257
|
+
tmux_bin="$(command -v tmux || true)"
|
|
258
|
+
[ -n "$tmux_bin" ] || die "tmux is required to run aiur; install tmux and retry"
|
|
259
|
+
|
|
260
|
+
aiur_resolve_identity
|
|
261
|
+
|
|
262
|
+
# Pick up GITHUB_TOKEN / dashboard creds the wizard wrote to ./.env so the
|
|
263
|
+
# running tracker can authenticate. Shell exports still take precedence.
|
|
264
|
+
load_dotenv
|
|
265
|
+
|
|
266
|
+
init_argv_file
|
|
267
|
+
|
|
268
|
+
# Inject the flags a bare `aiur` needs: loopback bind, interactive UI, and the
|
|
269
|
+
# no-guardrails ack. Skip any the user already passed.
|
|
270
|
+
local has_host=0 has_interactive=0 has_ack=0 arg
|
|
271
|
+
for arg in "$@"; do
|
|
272
|
+
case "$arg" in
|
|
273
|
+
--host | --host=*) has_host=1 ;;
|
|
274
|
+
--interactive) has_interactive=1 ;;
|
|
275
|
+
--i-understand-that-this-will-be-running-without-the-usual-guardrails) has_ack=1 ;;
|
|
276
|
+
esac
|
|
277
|
+
done
|
|
278
|
+
local injected=()
|
|
279
|
+
[ "$has_host" -eq 1 ] || injected+=(--host 127.0.0.1)
|
|
280
|
+
[ "$has_interactive" -eq 1 ] || injected+=(--interactive)
|
|
281
|
+
[ "$has_ack" -eq 1 ] || injected+=(--i-understand-that-this-will-be-running-without-the-usual-guardrails)
|
|
282
|
+
|
|
283
|
+
write_argv "${injected[@]}" "$@"
|
|
284
|
+
export AIUR_ARGV_FILE="$argv_file"
|
|
285
|
+
|
|
286
|
+
prepare_distribution
|
|
287
|
+
build_release_cmd
|
|
288
|
+
|
|
289
|
+
# Force +fnu when no locale is set so the BEAM does not mangle non-ASCII paths.
|
|
290
|
+
if [ -z "${LANG:-}" ] && [ -z "${LC_ALL:-}" ] && [ -z "${LC_CTYPE:-}" ]; then
|
|
291
|
+
export ELIXIR_ERL_OPTIONS="${ELIXIR_ERL_OPTIONS:-} +fnu"
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
local session="${AIUR_SESSION_PREFIX}-${USER:-user}-default"
|
|
295
|
+
local socket="${AIUR_SESSION_PREFIX}-${USER:-user}"
|
|
296
|
+
local conf
|
|
297
|
+
conf="$(resolve_tmux_conf)"
|
|
298
|
+
[ -f "$conf" ] || die "tmux conf not found at $conf"
|
|
299
|
+
|
|
300
|
+
mkdir -p "$AIUR_BG_STATE_DIR"
|
|
301
|
+
printf '%s\n' "$session" >"$AIUR_BG_STATE_DIR/state"
|
|
302
|
+
export AIUR_TMUX_SESSION="$session"
|
|
303
|
+
export AIUR_TMUX_SOCKET="$socket"
|
|
304
|
+
export AIUR_TMUX_CONF="$conf"
|
|
305
|
+
export AIUR_BIN="${BASH_SOURCE[0]}"
|
|
306
|
+
|
|
307
|
+
local session_root="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}"
|
|
308
|
+
export AIUR_SESSION_TMPFILE="${session_root}/aiur-${$}-sessions"
|
|
309
|
+
: >"$AIUR_SESSION_TMPFILE"
|
|
310
|
+
|
|
311
|
+
local startup_capture
|
|
312
|
+
startup_capture="$(mktemp "${TMPDIR:-/tmp}/aiur-startup.XXXXXX")"
|
|
313
|
+
|
|
314
|
+
# Inner pane script: tmux's server may pre-exist and not inherit our env, so
|
|
315
|
+
# re-export every var the BEAM needs. tee preserves a startup capture.
|
|
316
|
+
local launcher
|
|
317
|
+
launcher="$(mktemp "${TMPDIR:-/tmp}/aiur-pane.XXXXXX")"
|
|
318
|
+
chmod +x "$launcher"
|
|
319
|
+
{
|
|
320
|
+
printf '#!/usr/bin/env bash\n'
|
|
321
|
+
printf 'set -o pipefail\n'
|
|
322
|
+
printf 'cd %q || exit 1\n' "$PWD"
|
|
323
|
+
local v
|
|
324
|
+
for v in AIUR_RELEASE_DIR AIUR_ARGV_FILE RELEASE_DISTRIBUTION RELEASE_NODE \
|
|
325
|
+
RELEASE_COOKIE ERL_AFLAGS ERL_EPMD_ADDRESS AIUR_NODE AIUR_ERLANG_COOKIE \
|
|
326
|
+
AIUR_TMUX_SESSION AIUR_TMUX_SOCKET AIUR_TMUX_CONF AIUR_BIN \
|
|
327
|
+
AIUR_SESSION_TMPFILE ELIXIR_ERL_OPTIONS AIUR_LOGS_ROOT AIUR_DEBUG; do
|
|
328
|
+
if [ -n "${!v:-}" ]; then printf 'export %s=%q\n' "$v" "${!v}"; fi
|
|
329
|
+
done
|
|
330
|
+
printf 'capture=%q\n' "$startup_capture"
|
|
331
|
+
printf '%q' "${release_cmd[0]}"
|
|
332
|
+
for arg in "${release_cmd[@]:1}"; do printf ' %q' "$arg"; done
|
|
333
|
+
printf ' 2>&1 | tee -a "$capture"\n'
|
|
334
|
+
printf 'exit ${PIPESTATUS[0]}\n'
|
|
335
|
+
} >"$launcher"
|
|
336
|
+
|
|
337
|
+
local inner_cmd
|
|
338
|
+
printf -v inner_cmd '%q; rc=$?; rm -f %q; exit $rc' "$launcher" "$launcher"
|
|
339
|
+
|
|
340
|
+
if [ "$mode" = "foreground" ]; then
|
|
341
|
+
_session_socket="$socket" _session_name="$session" _session_conf="$conf" \
|
|
342
|
+
_session_tmpfile="$AIUR_SESSION_TMPFILE" _session_capture="$startup_capture" \
|
|
343
|
+
_session_argv="$argv_file" _session_release="$release_dir" _session_tmux="$tmux_bin" \
|
|
344
|
+
_session_node="$AIUR_RELEASE_NODE"
|
|
345
|
+
install_foreground_traps
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# An orphaned BEAM (its tmux session gone) can still hold the node name —
|
|
349
|
+
# under the unified identity, even one from a different release dir. With no
|
|
350
|
+
# live session on our socket, reap any name-holder so the launch isn't blocked
|
|
351
|
+
# by "name seems to be in use".
|
|
352
|
+
if ! "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
353
|
+
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
if ! "$tmux_bin" -L "$socket" -f "$conf" new-session -d -s "$session" \
|
|
357
|
+
-x "${COLUMNS:-200}" -y "${LINES:-50}" "$inner_cmd"; then
|
|
358
|
+
echo "❌ aiur failed to start; captured output:" >&2
|
|
359
|
+
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
360
|
+
rm -f "$startup_capture" "$argv_file" 2>/dev/null || true
|
|
361
|
+
exit 1
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
# Grace window: surface a boot crash instead of attaching to nothing.
|
|
365
|
+
local tick
|
|
366
|
+
for ((tick = 0; tick < 30; tick++)); do
|
|
367
|
+
if ! "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
368
|
+
echo "❌ aiur exited during startup; captured output:" >&2
|
|
369
|
+
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
370
|
+
[ "$mode" = "foreground" ] && return 1
|
|
371
|
+
rm -f "$startup_capture" "$argv_file" 2>/dev/null || true
|
|
372
|
+
exit 1
|
|
373
|
+
fi
|
|
374
|
+
sleep 0.1
|
|
375
|
+
done
|
|
376
|
+
|
|
377
|
+
if [ "$mode" = "background" ]; then
|
|
378
|
+
echo "aiur started in the background (tmux session ${session}). Attach with: aiur" >&2
|
|
379
|
+
rm -f "$startup_capture" "$argv_file" 2>/dev/null || true
|
|
380
|
+
return 0
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
# Foreground: attach the UI. Do not exec — that would drop the teardown trap.
|
|
384
|
+
"$tmux_bin" -L "$socket" -f "$conf" attach -t "$session" 2> >(grep -v -F "[server exited]" >&2)
|
|
385
|
+
return $?
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
resolve_tmux_conf() {
|
|
389
|
+
if [ -n "${AIUR_TMUX_CONF:-}" ] && [ -f "${AIUR_TMUX_CONF:-}" ]; then
|
|
390
|
+
printf '%s\n' "$AIUR_TMUX_CONF"
|
|
391
|
+
return
|
|
392
|
+
fi
|
|
393
|
+
local user_conf="${XDG_CONFIG_HOME:-$HOME/.config}/aiur/tmux.conf"
|
|
394
|
+
if [ -f "$user_conf" ]; then
|
|
395
|
+
printf '%s\n' "$user_conf"
|
|
396
|
+
return
|
|
397
|
+
fi
|
|
398
|
+
printf '%s\n' "$engine_dir/../share/aiur.tmux.conf"
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# TERM-then-KILL every BEAM whose command line matches $1. Reaps a stale node by
|
|
402
|
+
# name (covers orphans from any release dir): under the unified identity a dev
|
|
403
|
+
# `_build` BEAM and an installed BEAM both claim the same node name, so a
|
|
404
|
+
# release-path pgrep misses whichever one this run didn't launch.
|
|
405
|
+
kill_beams_matching() {
|
|
406
|
+
local pattern="$1" pids pid waited=0
|
|
407
|
+
pids="$(pgrep -f -- "$pattern" 2>/dev/null || true)"
|
|
408
|
+
[ -n "$pids" ] || return 0
|
|
409
|
+
for pid in $pids; do kill -TERM "$pid" 2>/dev/null || true; done
|
|
410
|
+
while [ "$waited" -lt 30 ]; do
|
|
411
|
+
pids="$(pgrep -f -- "$pattern" 2>/dev/null || true)"
|
|
412
|
+
[ -z "$pids" ] && break
|
|
413
|
+
sleep 0.1
|
|
414
|
+
waited=$((waited + 1))
|
|
415
|
+
done
|
|
416
|
+
pids="$(pgrep -f -- "$pattern" 2>/dev/null || true)"
|
|
417
|
+
for pid in $pids; do kill -KILL "$pid" 2>/dev/null || true; done
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Foreground teardown: kill the session + BEAM and reap opencode sessions on exit.
|
|
421
|
+
_session_socket="" _session_name="" _session_conf="" _session_tmpfile=""
|
|
422
|
+
_session_capture="" _session_argv="" _session_release="" _session_tmux="" _session_node=""
|
|
423
|
+
_cleanup_ran=0
|
|
424
|
+
session_cleanup() {
|
|
425
|
+
[ "$_cleanup_ran" = 1 ] && return 0
|
|
426
|
+
_cleanup_ran=1
|
|
427
|
+
local code=$?
|
|
428
|
+
|
|
429
|
+
if [ -n "$_session_tmux" ]; then
|
|
430
|
+
"$_session_tmux" -L "$_session_socket" -f "$_session_conf" kill-session -t "$_session_name" 2>/dev/null || true
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
local beam_pids pid waited=0
|
|
434
|
+
beam_pids="$(pgrep -f "$_session_release/.*erts.*beam.smp" 2>/dev/null || true)"
|
|
435
|
+
if [ -n "$beam_pids" ]; then
|
|
436
|
+
for pid in $beam_pids; do kill -TERM "$pid" 2>/dev/null || true; done
|
|
437
|
+
while [ $waited -lt 30 ]; do
|
|
438
|
+
beam_pids="$(pgrep -f "$_session_release/.*erts.*beam.smp" 2>/dev/null || true)"
|
|
439
|
+
[ -z "$beam_pids" ] && break
|
|
440
|
+
sleep 0.1
|
|
441
|
+
waited=$((waited + 1))
|
|
442
|
+
done
|
|
443
|
+
beam_pids="$(pgrep -f "$_session_release/.*erts.*beam.smp" 2>/dev/null || true)"
|
|
444
|
+
for pid in $beam_pids; do kill -KILL "$pid" 2>/dev/null || true; done
|
|
445
|
+
fi
|
|
446
|
+
|
|
447
|
+
# Belt-and-suspenders: reap any BEAM still holding our node name, even one
|
|
448
|
+
# launched from a different release dir than this run.
|
|
449
|
+
[ -n "$_session_node" ] && kill_beams_matching "-name ${_session_node}"
|
|
450
|
+
|
|
451
|
+
if command -v opencode >/dev/null 2>&1 && [ -s "$_session_tmpfile" ]; then
|
|
452
|
+
local id
|
|
453
|
+
while IFS= read -r id; do
|
|
454
|
+
[ -n "$id" ] || continue
|
|
455
|
+
opencode session delete "$id" >/dev/null 2>&1 &
|
|
456
|
+
done <"$_session_tmpfile"
|
|
457
|
+
wait 2>/dev/null || true
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
# Reap agent-driver sockets this run orphaned at close, not next launch.
|
|
461
|
+
sweep_dead_tmux_sockets || true
|
|
462
|
+
|
|
463
|
+
rm -f "$_session_tmpfile" "$_session_capture" "$_session_argv" 2>/dev/null || true
|
|
464
|
+
return $code
|
|
465
|
+
}
|
|
466
|
+
install_foreground_traps() {
|
|
467
|
+
trap 'session_cleanup' EXIT
|
|
468
|
+
trap 'trap - EXIT INT TERM HUP; session_cleanup; exit 130' INT
|
|
469
|
+
trap 'trap - EXIT INT TERM HUP; session_cleanup; exit 143' TERM
|
|
470
|
+
trap 'trap - EXIT INT TERM HUP; session_cleanup; exit 129' HUP
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# --- lifecycle (RPC into the running node + tmux ops) ------------------------
|
|
474
|
+
|
|
475
|
+
trim() {
|
|
476
|
+
local s="$1"
|
|
477
|
+
s="${s#"${s%%[![:space:]]*}"}"
|
|
478
|
+
s="${s%"${s##*[![:space:]]}"}"
|
|
479
|
+
printf '%s' "$s"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# RPC an expression into the running node. The control CLI prints a trailing
|
|
483
|
+
# `__AIUR_CONTROL_EXIT__:<code>` marker we translate into the process exit code.
|
|
484
|
+
run_control_rpc() {
|
|
485
|
+
local expression="$1"
|
|
486
|
+
resolve_release
|
|
487
|
+
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
488
|
+
|
|
489
|
+
local marker="__AIUR_CONTROL_EXIT__:"
|
|
490
|
+
local output status exit_code=0 saw_marker=0 line
|
|
491
|
+
|
|
492
|
+
set +e
|
|
493
|
+
output="$("$release_bin" rpc "$expression" 2>&1)"
|
|
494
|
+
status=$?
|
|
495
|
+
set -e
|
|
496
|
+
|
|
497
|
+
if [ "$status" -ne 0 ]; then
|
|
498
|
+
echo "aiur: no running aiur node at ${RELEASE_NODE}; start aiur and try again" >&2
|
|
499
|
+
return 1
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
while IFS= read -r line; do
|
|
503
|
+
case "$line" in
|
|
504
|
+
"$marker"*)
|
|
505
|
+
saw_marker=1
|
|
506
|
+
exit_code="${line#"$marker"}"
|
|
507
|
+
;;
|
|
508
|
+
:ok | "") ;;
|
|
509
|
+
*) printf '%s\n' "$line" ;;
|
|
510
|
+
esac
|
|
511
|
+
done <<<"$output"
|
|
512
|
+
|
|
513
|
+
[ "$saw_marker" -eq 1 ] || return 1
|
|
514
|
+
return "$exit_code"
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
elixir_list_literal() {
|
|
518
|
+
local first=1 item
|
|
519
|
+
printf '['
|
|
520
|
+
for item in "$@"; do
|
|
521
|
+
[ "$first" -eq 0 ] && printf ', '
|
|
522
|
+
printf '"%s"' "$item"
|
|
523
|
+
first=0
|
|
524
|
+
done
|
|
525
|
+
printf ']'
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
# Parse issue-id targets (e.g. `44 45,46` or `--all`) into parsed_targets/parsed_all.
|
|
529
|
+
parsed_targets=()
|
|
530
|
+
parsed_all=0
|
|
531
|
+
parse_issue_targets() {
|
|
532
|
+
parsed_targets=()
|
|
533
|
+
parsed_all=0
|
|
534
|
+
[ "$#" -gt 0 ] || return 1
|
|
535
|
+
|
|
536
|
+
if [ "$#" -eq 1 ] && [ "$1" = "--all" ]; then
|
|
537
|
+
parsed_all=1
|
|
538
|
+
return 0
|
|
539
|
+
fi
|
|
540
|
+
|
|
541
|
+
local raw part parts
|
|
542
|
+
for raw in "$@"; do
|
|
543
|
+
[ "$raw" = "--all" ] && return 1
|
|
544
|
+
IFS=',' read -ra parts <<<"$raw"
|
|
545
|
+
for part in "${parts[@]}"; do
|
|
546
|
+
part="$(trim "$part")"
|
|
547
|
+
if [ -z "$part" ] || [[ ! "$part" =~ ^[0-9]+$ ]]; then return 1; fi
|
|
548
|
+
parsed_targets+=("$part")
|
|
549
|
+
done
|
|
550
|
+
done
|
|
551
|
+
|
|
552
|
+
[ "${#parsed_targets[@]}" -gt 0 ]
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
cmd_status() {
|
|
556
|
+
[ "$#" -eq 0 ] || die "status does not accept arguments"
|
|
557
|
+
run_control_rpc "Aiur.AgentControlCLI.status()"
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
cmd_pause_resume() {
|
|
561
|
+
local command="$1"
|
|
562
|
+
shift
|
|
563
|
+
if ! parse_issue_targets "$@"; then
|
|
564
|
+
echo "aiur: $command expects issue IDs or --all (e.g. aiur $command 44 45,46; aiur $command --all)" >&2
|
|
565
|
+
exit 64
|
|
566
|
+
fi
|
|
567
|
+
|
|
568
|
+
local expression
|
|
569
|
+
if [ "$parsed_all" -eq 1 ]; then
|
|
570
|
+
expression="Aiur.AgentControlCLI.${command}(:all)"
|
|
571
|
+
else
|
|
572
|
+
expression="Aiur.AgentControlCLI.${command}($(elixir_list_literal "${parsed_targets[@]}"))"
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
run_control_rpc "$expression"
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
sweep_dead_tmux_sockets() {
|
|
579
|
+
local tmux_bin
|
|
580
|
+
tmux_bin="$(command -v tmux || true)"
|
|
581
|
+
[ -n "$tmux_bin" ] || return 0
|
|
582
|
+
|
|
583
|
+
local sockdir="${TMUX_TMPDIR:-/tmp}/tmux-$(id -u)"
|
|
584
|
+
[ -d "$sockdir" ] || return 0
|
|
585
|
+
|
|
586
|
+
local removed=0 name path
|
|
587
|
+
for path in "$sockdir"/*; do
|
|
588
|
+
[ -S "$path" ] || continue
|
|
589
|
+
name="${path##*/}"
|
|
590
|
+
case "$name" in
|
|
591
|
+
aiur* | *-driver | *-driver-*) ;;
|
|
592
|
+
*) continue ;;
|
|
593
|
+
esac
|
|
594
|
+
if ! "$tmux_bin" -L "$name" list-sessions >/dev/null 2>&1; then
|
|
595
|
+
rm -f "$path" 2>/dev/null && removed=$((removed + 1))
|
|
596
|
+
fi
|
|
597
|
+
done
|
|
598
|
+
|
|
599
|
+
[ "$removed" -gt 0 ] && echo "🧹 swept $removed dead aiur tmux socket(s)" >&2
|
|
600
|
+
return 0
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
# Stop the running session: kill the tmux session + the release BEAM, then sweep.
|
|
604
|
+
cmd_stop() {
|
|
605
|
+
resolve_release
|
|
606
|
+
aiur_resolve_identity
|
|
607
|
+
|
|
608
|
+
local tmux_bin
|
|
609
|
+
tmux_bin="$(command -v tmux || true)"
|
|
610
|
+
local session="${AIUR_SESSION_PREFIX}-${USER:-user}-default"
|
|
611
|
+
local socket="${AIUR_SESSION_PREFIX}-${USER:-user}"
|
|
612
|
+
if [ -n "$tmux_bin" ]; then
|
|
613
|
+
"$tmux_bin" -L "$socket" kill-session -t "$session" 2>/dev/null || true
|
|
614
|
+
fi
|
|
615
|
+
|
|
616
|
+
local beam_pids pid waited=0
|
|
617
|
+
beam_pids="$(pgrep -f "$release_dir/.*erts.*beam.smp" 2>/dev/null || true)"
|
|
618
|
+
if [ -n "$beam_pids" ]; then
|
|
619
|
+
for pid in $beam_pids; do kill -TERM "$pid" 2>/dev/null || true; done
|
|
620
|
+
while [ $waited -lt 30 ]; do
|
|
621
|
+
beam_pids="$(pgrep -f "$release_dir/.*erts.*beam.smp" 2>/dev/null || true)"
|
|
622
|
+
[ -z "$beam_pids" ] && break
|
|
623
|
+
sleep 0.1
|
|
624
|
+
waited=$((waited + 1))
|
|
625
|
+
done
|
|
626
|
+
beam_pids="$(pgrep -f "$release_dir/.*erts.*beam.smp" 2>/dev/null || true)"
|
|
627
|
+
for pid in $beam_pids; do kill -KILL "$pid" 2>/dev/null || true; done
|
|
628
|
+
fi
|
|
629
|
+
|
|
630
|
+
# Reap any BEAM holding our node name regardless of which release dir launched
|
|
631
|
+
# it — orphans from a dev build or a prior install share the unified name.
|
|
632
|
+
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
633
|
+
|
|
634
|
+
sweep_dead_tmux_sockets
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
# --- dispatch ----------------------------------------------------------------
|
|
638
|
+
|
|
639
|
+
aiur_engine_main() {
|
|
640
|
+
local cmd="${1:-}"
|
|
641
|
+
case "$cmd" in
|
|
642
|
+
__identity)
|
|
643
|
+
aiur_print_identity
|
|
644
|
+
;;
|
|
645
|
+
help | -h | -help | --h | --help)
|
|
646
|
+
usage
|
|
647
|
+
;;
|
|
648
|
+
--version)
|
|
649
|
+
run_version "$@"
|
|
650
|
+
;;
|
|
651
|
+
init)
|
|
652
|
+
run_init "$@"
|
|
653
|
+
;;
|
|
654
|
+
--bg)
|
|
655
|
+
shift
|
|
656
|
+
run_session background "$@"
|
|
657
|
+
;;
|
|
658
|
+
run)
|
|
659
|
+
shift
|
|
660
|
+
run_session foreground "$@"
|
|
661
|
+
;;
|
|
662
|
+
status)
|
|
663
|
+
shift
|
|
664
|
+
cmd_status "$@"
|
|
665
|
+
;;
|
|
666
|
+
pause | resume)
|
|
667
|
+
shift
|
|
668
|
+
cmd_pause_resume "$cmd" "$@"
|
|
669
|
+
;;
|
|
670
|
+
stop)
|
|
671
|
+
cmd_stop
|
|
672
|
+
;;
|
|
673
|
+
"")
|
|
674
|
+
run_session foreground
|
|
675
|
+
;;
|
|
676
|
+
-*)
|
|
677
|
+
# leading-flag forms (e.g. `aiur --interactive <config>`) are a run
|
|
678
|
+
run_session foreground "$@"
|
|
679
|
+
;;
|
|
680
|
+
*)
|
|
681
|
+
# a path/config argument is a run; anything else is a usage error
|
|
682
|
+
if [ -e "$cmd" ]; then
|
|
683
|
+
run_session foreground "$@"
|
|
684
|
+
else
|
|
685
|
+
echo "aiur: unknown command: $cmd" >&2
|
|
686
|
+
usage >&2
|
|
687
|
+
exit 64
|
|
688
|
+
fi
|
|
689
|
+
;;
|
|
690
|
+
esac
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
694
|
+
aiur_engine_main "$@"
|
|
695
|
+
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiur-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "AI Unit Runtime for Executors — placeholder, full release coming soon.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/its-everdred/aiur",
|
|
@@ -11,10 +11,21 @@
|
|
|
11
11
|
"bin": {
|
|
12
12
|
"aiur": "bin/aiur.js"
|
|
13
13
|
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test"
|
|
16
|
+
},
|
|
14
17
|
"files": [
|
|
15
|
-
"bin/aiur.js"
|
|
18
|
+
"bin/aiur.js",
|
|
19
|
+
"libexec/aiur-engine.sh",
|
|
20
|
+
"share/aiur.tmux.conf"
|
|
16
21
|
],
|
|
17
22
|
"engines": {
|
|
18
23
|
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"optionalDependencies": {
|
|
26
|
+
"aiur-cli-linux-x64": "0.0.1",
|
|
27
|
+
"aiur-cli-linux-arm64": "0.0.1",
|
|
28
|
+
"aiur-cli-darwin-x64": "0.0.1",
|
|
29
|
+
"aiur-cli-darwin-arm64": "0.0.1"
|
|
19
30
|
}
|
|
20
31
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Aiur's isolated tmux configuration.
|
|
2
|
+
#
|
|
3
|
+
# Loaded by `scripts/aiur` via `tmux -L aiur-$USER -f <path>` so it
|
|
4
|
+
# runs on its own socket and never touches the user's normal tmux server
|
|
5
|
+
# or `~/.tmux.conf`.
|
|
6
|
+
#
|
|
7
|
+
# To customize without touching this tracked file, copy it to
|
|
8
|
+
# `~/.config/aiur/tmux.conf` and edit there — that path takes
|
|
9
|
+
# precedence and survives `git pull`.
|
|
10
|
+
|
|
11
|
+
# No prefix — every Aiur binding is direct (`-n`).
|
|
12
|
+
set -g prefix None
|
|
13
|
+
unbind-key -aT prefix
|
|
14
|
+
|
|
15
|
+
set -sg escape-time 0
|
|
16
|
+
set -g mouse on
|
|
17
|
+
# Pass CSI-u extended-key sequences (e.g. Shift+Enter as `\e[13;2u`)
|
|
18
|
+
# through to the foreground pane instead of collapsing them. Required
|
|
19
|
+
# for the agent list pane's Shift+Enter "open in new pane" keybind.
|
|
20
|
+
set -s extended-keys on
|
|
21
|
+
set -as terminal-features "*:extkeys"
|
|
22
|
+
set -g status off
|
|
23
|
+
set -g history-limit 50000
|
|
24
|
+
set -g default-terminal "tmux-256color"
|
|
25
|
+
set -ga terminal-overrides ",*256col*:Tc"
|
|
26
|
+
set -g remain-on-exit off
|
|
27
|
+
set -g destroy-unattached off
|
|
28
|
+
|
|
29
|
+
# Pane navigation (cycle right / cycle left).
|
|
30
|
+
bind-key -n Tab select-pane -t :.+
|
|
31
|
+
bind-key -n BTab select-pane -t :.-
|
|
32
|
+
|
|
33
|
+
# Option+Tab from inside any chat pane jumps focus to pane 0 (the
|
|
34
|
+
# agent list, by convention — see the C-c handler below for the same
|
|
35
|
+
# pane-index assumption).
|
|
36
|
+
bind-key -n M-Tab select-pane -t :.0
|
|
37
|
+
|
|
38
|
+
# Close: pane 0 (the agent list, by convention) tears down the whole
|
|
39
|
+
# session; any other pane routes Ctrl+C through the opencode bridge
|
|
40
|
+
# helper, which asks Aiur for the 3-state action (drain a queued message,
|
|
41
|
+
# pause an idle agent, or close an already-paused pane). `@aiur_ctrlc` is
|
|
42
|
+
# the helper path (set by the launcher) and `@aiur_control_url` is the
|
|
43
|
+
# dashboard URL (published by Aiur once the server binds). When either is
|
|
44
|
+
# unset, or the helper isn't executable, Ctrl+C degrades to a plain pane
|
|
45
|
+
# close — the pre-bridge behaviour. `if-shell -F` is the synchronous
|
|
46
|
+
# format-string form so the condition is evaluated against tmux's format
|
|
47
|
+
# engine (the shell-mode `if-shell` has been buggy since tmux 2.4 —
|
|
48
|
+
# tmux/tmux#882).
|
|
49
|
+
set -gq @aiur_ctrlc ''
|
|
50
|
+
set -gq @aiur_control_url ''
|
|
51
|
+
bind-key -n C-c if-shell -F '#{==:#{pane_index},0}' \
|
|
52
|
+
'kill-session' \
|
|
53
|
+
'run-shell "h=\"#{@aiur_ctrlc}\"; if [ -x \"$h\" ]; then \"$h\" \"#{pane_id}\" \"#{@aiur_control_url}\"; else tmux kill-pane -t \"#{pane_id}\"; fi"'
|
|
54
|
+
|
|
55
|
+
# Pane layout is owned exclusively by `Aiur.PaneManager`, which applies an
|
|
56
|
+
# explicit `select-layout <string>` after every slot change. No
|
|
57
|
+
# `after-split-window` hook lives here — historically a hook ran
|
|
58
|
+
# `select-layout even-horizontal` to "rebalance widths", but it ran on every
|
|
59
|
+
# split (including PaneManager's anchored slot splits) and forced every pane
|
|
60
|
+
# into a single equal-width row, defeating the slot grid. See issue #34.
|
|
61
|
+
|
|
62
|
+
# Wheel routing:
|
|
63
|
+
# 1. If the foreground app set mouse mode (opencode-attach / any Bubble Tea
|
|
64
|
+
# TUI does), forward wheel events with `send-keys -M` so the app handles
|
|
65
|
+
# scroll inside its own viewport.
|
|
66
|
+
# 2. Else if tmux is already in copy mode (`pane_in_mode`), forward.
|
|
67
|
+
# 3. Else (plain shell pane, mobile SSH client doing touch-scroll) enter
|
|
68
|
+
# copy mode so the user can scroll the pane's scrollback buffer.
|
|
69
|
+
#
|
|
70
|
+
# Without (1), wheel up on an opencode chat pane would force tmux into copy
|
|
71
|
+
# mode against opencode's full-screen TUI, which has no tmux-side scrollback
|
|
72
|
+
# — the indicator just sticks at `[0/0]` and chat history can't be reached.
|
|
73
|
+
bind-key -n WheelUpPane if-shell -F -t = '#{?mouse_any_flag,1,0}' \
|
|
74
|
+
'send-keys -M' \
|
|
75
|
+
"if-shell -F -t = '#{pane_in_mode}' 'send-keys -M' 'copy-mode -e -t ='"
|
|
76
|
+
bind-key -n WheelDownPane send-keys -M
|