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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aiur-cli",
3
- "version": "0.0.1",
4
- "description": "AI Unit Runtime for Executors — placeholder, full release coming soon.",
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.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"
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
+ }
@@ -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"