aiur-cli 0.0.1 → 0.0.3
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 +20 -1
- package/bin/aiur.js +352 -23
- package/libexec/aiur-engine.sh +688 -68
- package/package.json +9 -6
- package/scripts/postinstall.mjs +173 -0
- package/share/aiur.tmux.conf +10 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiur-cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "AI Unit Runtime for Executors
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "AI Unit Runtime for Executors.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/its-everdred/aiur",
|
|
7
7
|
"repository": {
|
|
@@ -12,20 +12,23 @@
|
|
|
12
12
|
"aiur": "bin/aiur.js"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
15
16
|
"test": "bun test"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"bin/aiur.js",
|
|
19
20
|
"libexec/aiur-engine.sh",
|
|
21
|
+
"scripts/postinstall.mjs",
|
|
20
22
|
"share/aiur.tmux.conf"
|
|
21
23
|
],
|
|
22
24
|
"engines": {
|
|
23
25
|
"node": ">=18"
|
|
24
26
|
},
|
|
27
|
+
"opencodeVersion": "1.15.6",
|
|
25
28
|
"optionalDependencies": {
|
|
26
|
-
"aiur-cli-linux-x64": "0.0.
|
|
27
|
-
"aiur-cli-linux-arm64": "0.0.
|
|
28
|
-
"aiur-cli-darwin-x64": "0.0.
|
|
29
|
-
"aiur-cli-darwin-arm64": "0.0.
|
|
29
|
+
"aiur-cli-linux-x64": "0.0.3",
|
|
30
|
+
"aiur-cli-linux-arm64": "0.0.3",
|
|
31
|
+
"aiur-cli-darwin-x64": "0.0.3",
|
|
32
|
+
"aiur-cli-darwin-arm64": "0.0.3"
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Provisions the peer tools every aiur user needs at runtime, so that a single
|
|
3
|
+
// global `npm install -g aiur-cli` yields a working install with nothing left
|
|
4
|
+
// to install by hand:
|
|
5
|
+
//
|
|
6
|
+
// - tmux the multiplexer the interactive TUI runs inside. aiur refuses
|
|
7
|
+
// to launch without tmux >= MIN_TMUX, so it is a hard runtime
|
|
8
|
+
// requirement (not merely nice-to-have).
|
|
9
|
+
// - opencode the peer CLI behind the interactive "take the wheel" panes.
|
|
10
|
+
//
|
|
11
|
+
// A global aiur-cli install does not link a dependency's bin onto PATH, and
|
|
12
|
+
// tmux is a system package rather than an npm one, so we provision both here.
|
|
13
|
+
// Every step is idempotent (skips when already satisfied) and non-fatal (a
|
|
14
|
+
// failure prints a hint but never breaks the aiur install).
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// opencode
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
// Pinned opencode version, read from package.json `opencodeVersion` (single
|
|
25
|
+
// source of truth — keep in sync with mise.toml). aiur's opencode pane
|
|
26
|
+
// integration is validated against this version; 1.17.x broke custom-provider
|
|
27
|
+
// model resolution (ProviderModelNotFoundError), blanking the chat panes.
|
|
28
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
29
|
+
export const OPENCODE_PACKAGE = `opencode-ai@${pkg.opencodeVersion}`;
|
|
30
|
+
|
|
31
|
+
// True only when the *pinned* opencode version is on PATH. A wrong (e.g.
|
|
32
|
+
// newer) version must be reinstalled — leaving 1.17.x in place keeps the
|
|
33
|
+
// panes broken — so a mere presence check isn't enough.
|
|
34
|
+
export function opencodeMatchesPin() {
|
|
35
|
+
const result = spawnSync("opencode", ["--version"], { encoding: "utf8" });
|
|
36
|
+
return !result.error && result.status === 0 && result.stdout.trim() === pkg.opencodeVersion;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build the nested-install command. Provisioning fetches opencode from the
|
|
40
|
+
// registry, so it must NOT inherit an offline context from the outer
|
|
41
|
+
// `npm install` that triggered this postinstall — CI's `npm install -g
|
|
42
|
+
// --offline`, a `prefer-offline` config, or an air-gapped box. npm hands those
|
|
43
|
+
// to lifecycle scripts as npm_config_* env vars; a naive nested install
|
|
44
|
+
// inherits them and dies with ENOTCACHED on a cold cache, silently leaving
|
|
45
|
+
// opencode unprovisioned. We force online two ways (CLI flags outrank env, and
|
|
46
|
+
// stripping the env vars is belt-and-suspenders) so a plain `npm install -g
|
|
47
|
+
// aiur-cli` provisions opencode regardless of how the parent was invoked.
|
|
48
|
+
export function opencodeInstallSpec(env = process.env) {
|
|
49
|
+
const childEnv = { ...env };
|
|
50
|
+
delete childEnv.npm_config_offline;
|
|
51
|
+
delete childEnv.npm_config_prefer_offline;
|
|
52
|
+
return {
|
|
53
|
+
command: "npm",
|
|
54
|
+
args: ["install", "-g", OPENCODE_PACKAGE, "--no-offline", "--no-prefer-offline"],
|
|
55
|
+
env: childEnv,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function installOpencode() {
|
|
60
|
+
const spec = opencodeInstallSpec();
|
|
61
|
+
const result = spawnSync(spec.command, spec.args, { stdio: "inherit", env: spec.env });
|
|
62
|
+
return !result.error && result.status === 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function provisionOpencode({
|
|
66
|
+
env = process.env,
|
|
67
|
+
isPresent = opencodeMatchesPin,
|
|
68
|
+
install = installOpencode,
|
|
69
|
+
log = (message) => process.stderr.write(message + "\n"),
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (env.AIUR_SKIP_OPENCODE_INSTALL === "1") return "skipped:disabled";
|
|
72
|
+
if (isPresent()) return "skipped:present";
|
|
73
|
+
|
|
74
|
+
log(`aiur: installing pinned opencode (${OPENCODE_PACKAGE}) for the interactive 'take the wheel' feature…`);
|
|
75
|
+
|
|
76
|
+
if (install()) return "installed";
|
|
77
|
+
|
|
78
|
+
log(
|
|
79
|
+
"aiur: couldn't install opencode automatically. Install it manually: " +
|
|
80
|
+
"npm install -g opencode-ai (see https://opencode.ai)",
|
|
81
|
+
);
|
|
82
|
+
return "failed";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// tmux
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
// Minimum tmux the TUI needs. Keep in sync with the runtime preflight in
|
|
90
|
+
// bin/aiur.js — that preflight stays as a safety net, this just front-loads
|
|
91
|
+
// the install so the first `aiur` run isn't a dead end.
|
|
92
|
+
export const MIN_TMUX = [3, 3];
|
|
93
|
+
|
|
94
|
+
function tmuxHint(platform = process.platform) {
|
|
95
|
+
return platform === "darwin"
|
|
96
|
+
? "install it with: brew install tmux"
|
|
97
|
+
: "install it with: apt install tmux (or your distro's package manager)";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// True only when a new-enough tmux is on PATH. The TUI won't launch with an
|
|
101
|
+
// older tmux, so — as with opencode's pin — presence alone isn't enough.
|
|
102
|
+
export function tmuxSatisfiesMin(run = (cmd, argv) => spawnSync(cmd, argv, { encoding: "utf8" })) {
|
|
103
|
+
const result = run("tmux", ["-V"]);
|
|
104
|
+
if (result.error || result.status !== 0) return false;
|
|
105
|
+
const match = /tmux\s+(\d+)\.(\d+)/.exec(result.stdout || "");
|
|
106
|
+
if (!match) return false;
|
|
107
|
+
const major = Number(match[1]);
|
|
108
|
+
const minor = Number(match[2]);
|
|
109
|
+
const [needMajor, needMinor] = MIN_TMUX;
|
|
110
|
+
return major > needMajor || (major === needMajor && minor >= needMinor);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Best-effort system install of tmux. macOS uses Homebrew; Linux uses the
|
|
114
|
+
// first package manager that is present. Returns false (so the caller can
|
|
115
|
+
// print a manual hint) when no usable installer exists or the install fails —
|
|
116
|
+
// including the common non-root case where a Linux package manager can't
|
|
117
|
+
// proceed. We never invoke sudo: a hidden password prompt during `npm install`
|
|
118
|
+
// would hang the install.
|
|
119
|
+
export function installTmux(platform = process.platform) {
|
|
120
|
+
const has = (bin) => !spawnSync(bin, ["--version"], { stdio: "ignore" }).error;
|
|
121
|
+
|
|
122
|
+
if (platform === "darwin") {
|
|
123
|
+
if (!has("brew")) return false;
|
|
124
|
+
return spawnSync("brew", ["install", "tmux"], { stdio: "inherit" }).status === 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (platform === "linux") {
|
|
128
|
+
const managers = [
|
|
129
|
+
["apt-get", ["install", "-y", "tmux"]],
|
|
130
|
+
["dnf", ["install", "-y", "tmux"]],
|
|
131
|
+
["apk", ["add", "tmux"]],
|
|
132
|
+
["pacman", ["-S", "--noconfirm", "tmux"]],
|
|
133
|
+
];
|
|
134
|
+
for (const [bin, argv] of managers) {
|
|
135
|
+
if (!has(bin)) continue;
|
|
136
|
+
return spawnSync(bin, argv, { stdio: "inherit" }).status === 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function provisionTmux({
|
|
144
|
+
env = process.env,
|
|
145
|
+
isPresent = tmuxSatisfiesMin,
|
|
146
|
+
install = installTmux,
|
|
147
|
+
platform = process.platform,
|
|
148
|
+
log = (message) => process.stderr.write(message + "\n"),
|
|
149
|
+
} = {}) {
|
|
150
|
+
if (env.AIUR_SKIP_TMUX_INSTALL === "1") return "skipped:disabled";
|
|
151
|
+
if (isPresent()) return "skipped:present";
|
|
152
|
+
|
|
153
|
+
log(
|
|
154
|
+
`aiur: tmux (>= ${MIN_TMUX.join(".")}) not found; aiur's interactive TUI runs inside tmux. Installing…`,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (install(platform)) return "installed";
|
|
158
|
+
|
|
159
|
+
log(`aiur: couldn't install tmux automatically. ${tmuxHint(platform)}`);
|
|
160
|
+
return "failed";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Entry point
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
// Run only when invoked directly as the postinstall step, not when a test
|
|
168
|
+
// imports the helpers above. tmux is provisioned first — it is the hard
|
|
169
|
+
// requirement — then opencode for the optional panes.
|
|
170
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
171
|
+
provisionTmux();
|
|
172
|
+
provisionOpencode();
|
|
173
|
+
}
|
package/share/aiur.tmux.conf
CHANGED
|
@@ -20,6 +20,16 @@ set -g mouse on
|
|
|
20
20
|
set -s extended-keys on
|
|
21
21
|
set -as terminal-features "*:extkeys"
|
|
22
22
|
set -g status off
|
|
23
|
+
|
|
24
|
+
# Show each pane's title in a one-line top border. PaneManager sets every
|
|
25
|
+
# chat pane's title to "<issue id> <issue title>" via `select-pane -T`, and
|
|
26
|
+
# the launcher sets the agent-list pane's title to "AIUR Agents". The format
|
|
27
|
+
# truncates the title to the pane's current width with an ellipsis (tmux
|
|
28
|
+
# evaluates the nested `#{pane_width}` before applying the `=/N/…` limit), so
|
|
29
|
+
# a long title never wraps and re-clips automatically on resize.
|
|
30
|
+
set -g pane-border-status top
|
|
31
|
+
set -g pane-border-format "#{=/#{pane_width}/…:#{pane_title}}"
|
|
32
|
+
|
|
23
33
|
set -g history-limit 50000
|
|
24
34
|
set -g default-terminal "tmux-256color"
|
|
25
35
|
set -ga terminal-overrides ",*256col*:Tc"
|