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/README.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
Oversee a fleet of coding agents that **coordinate via events**.
|
|
6
6
|
|
|
7
|
+
> [!WARNING]
|
|
8
|
+
> **Use at your own risk.** Aiur is an unstable, vibecoded engineering preview for trusted
|
|
9
|
+
> environments only. It **bypasses all agent permission prompts** and has very few efficiency
|
|
10
|
+
> optimizations. Suggested for simple tasks under supervision.
|
|
11
|
+
>
|
|
12
|
+
> Provided "as is", without warranty of any kind. You assume all risk for any cost, token
|
|
13
|
+
> spend, data loss, or damage from running it.
|
|
14
|
+
|
|
7
15
|
## Install
|
|
8
16
|
|
|
9
17
|
```bash
|
|
@@ -13,6 +21,11 @@ npm install -g aiur-cli
|
|
|
13
21
|
Requires [tmux](https://github.com/tmux/tmux) at runtime. Install
|
|
14
22
|
[opencode](https://opencode.ai) too for in-pane agent chat.
|
|
15
23
|
|
|
24
|
+
When a newer `aiur-cli` is published, a one-line update notice is printed to
|
|
25
|
+
stderr (checked in the background at most once a day, never blocking your
|
|
26
|
+
command). Set `AIUR_NO_UPDATE_NOTIFIER=1` to silence it; it is also skipped under
|
|
27
|
+
CI (`CI=true`).
|
|
28
|
+
|
|
16
29
|
## Get started
|
|
17
30
|
|
|
18
31
|
```bash
|
|
@@ -43,7 +56,7 @@ to the issues you want worked and run `aiur`.
|
|
|
43
56
|
|---|---|
|
|
44
57
|
| `aiur init` | Interactive setup wizard (scaffold `.aiurconfig`) |
|
|
45
58
|
| `aiur` | Start the workflow in the foreground (local-only bind) |
|
|
46
|
-
| `aiur --bg` | Start in
|
|
59
|
+
| `aiur --bg` | Start a headless BEAM in one detached tmux lifetime session |
|
|
47
60
|
| `aiur status` | Show active agents and their state |
|
|
48
61
|
| `aiur pause <id…>` / `resume <id…>` | Pause or resume agents by issue ID |
|
|
49
62
|
| `aiur stop` | Stop the running session |
|
|
@@ -57,6 +70,12 @@ to the issues you want worked and run `aiur`.
|
|
|
57
70
|
- **Smart Alerts** — Customize notifications and sounds when agents finish, stall, or need a decision.
|
|
58
71
|
- **Live Dashboard** — Shareable web view of every agent, event, and progress bar.
|
|
59
72
|
|
|
73
|
+
Background mode is headless inside Aiur: it skips the interactive agent-list
|
|
74
|
+
pane, chat/prewarm panes, and dashboard unless you explicitly opt into a
|
|
75
|
+
dashboard port. The launcher still keeps one detached tmux session as the BEAM
|
|
76
|
+
lifetime holder. Re-running `aiur --bg` against a live session exits with an
|
|
77
|
+
already-running hint; stale tmux state is cleaned up before restart.
|
|
78
|
+
|
|
60
79
|
---
|
|
61
80
|
|
|
62
81
|
_Command macro, delegate micro, maximize APM._
|
package/bin/aiur.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const { spawnSync, spawn } = require("node:child_process");
|
|
5
5
|
const path = require("node:path");
|
|
6
6
|
const fs = require("node:fs");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
|
|
9
|
+
// Node's spawnSync reports a missing executable via `result.error`; some
|
|
10
|
+
// runtimes (e.g. Bun) throw instead. Normalize to the error-return shape so a
|
|
11
|
+
// missing tmux/opencode/brew degrades gracefully rather than crashing.
|
|
12
|
+
function safeSpawn(command, args, options) {
|
|
13
|
+
try {
|
|
14
|
+
return spawnSync(command, args, options);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return { error, status: null, stdout: "" };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
// Map this host to the platform package that carries its OTP release.
|
|
9
21
|
const PLATFORMS = {
|
|
@@ -61,51 +73,360 @@ function resolveReleaseDir() {
|
|
|
61
73
|
return path.join(path.dirname(pkgJson), "release");
|
|
62
74
|
}
|
|
63
75
|
|
|
76
|
+
// Pinned opencode version — single source of truth is this package's
|
|
77
|
+
// package.json `opencodeVersion` (kept in sync with scripts/postinstall.mjs
|
|
78
|
+
// and mise.toml).
|
|
79
|
+
function opencodeVersion() {
|
|
80
|
+
const pkg = JSON.parse(
|
|
81
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
|
|
82
|
+
);
|
|
83
|
+
return pkg.opencodeVersion;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Lazy first-run provisioning
|
|
88
|
+
//
|
|
89
|
+
// The npm postinstall (scripts/postinstall.mjs) provisions tmux + opencode, but
|
|
90
|
+
// bun/pnpm/yarn block postinstall scripts by default — a package cannot force a
|
|
91
|
+
// consumer to run them. So the launcher guarantees the peer tools here, on the
|
|
92
|
+
// first real (TUI) launch, regardless of which package manager installed
|
|
93
|
+
// aiur-cli. Both steps are idempotent (skip when already satisfied); tmux is a
|
|
94
|
+
// hard requirement, opencode is best-effort. Honors the same AIUR_SKIP_* env
|
|
95
|
+
// vars as the postinstall. This logic mirrors postinstall.mjs intentionally —
|
|
96
|
+
// the launcher stays dependency-free and cannot import the .mjs counterpart.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
64
99
|
function tmuxVersion() {
|
|
65
|
-
const result =
|
|
100
|
+
const result = safeSpawn("tmux", ["-V"], { encoding: "utf8" });
|
|
66
101
|
if (result.error || result.status !== 0) return null;
|
|
67
102
|
const match = /tmux\s+(\d+)\.(\d+)/.exec(result.stdout || "");
|
|
68
103
|
if (!match) return null;
|
|
69
104
|
return [Number(match[1]), Number(match[2])];
|
|
70
105
|
}
|
|
71
106
|
|
|
107
|
+
function tmuxSatisfiesMin() {
|
|
108
|
+
const version = tmuxVersion();
|
|
109
|
+
if (!version) return false;
|
|
110
|
+
const [major, minor] = version;
|
|
111
|
+
const [needMajor, needMinor] = MIN_TMUX;
|
|
112
|
+
return major > needMajor || (major === needMajor && minor >= needMinor);
|
|
113
|
+
}
|
|
114
|
+
|
|
72
115
|
function tmuxHint() {
|
|
73
116
|
return process.platform === "darwin"
|
|
74
117
|
? "install it with: brew install tmux"
|
|
75
118
|
: "install it with: apt install tmux (or your distro's package manager)";
|
|
76
119
|
}
|
|
77
120
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
121
|
+
// Best-effort system install of tmux. macOS uses Homebrew; Linux uses the first
|
|
122
|
+
// package manager present. Never invokes sudo (a hidden password prompt mid-
|
|
123
|
+
// launch would hang). Returns false so the caller can fail with a clear hint.
|
|
124
|
+
function installTmux(platform) {
|
|
125
|
+
const has = (bin) => !safeSpawn(bin, ["--version"], { stdio: "ignore" }).error;
|
|
126
|
+
if (platform === "darwin") {
|
|
127
|
+
if (!has("brew")) return false;
|
|
128
|
+
return safeSpawn("brew", ["install", "tmux"], { stdio: "inherit" }).status === 0;
|
|
82
129
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
if (platform === "linux") {
|
|
131
|
+
const managers = [
|
|
132
|
+
["apt-get", ["install", "-y", "tmux"]],
|
|
133
|
+
["dnf", ["install", "-y", "tmux"]],
|
|
134
|
+
["apk", ["add", "tmux"]],
|
|
135
|
+
["pacman", ["-S", "--noconfirm", "tmux"]],
|
|
136
|
+
];
|
|
137
|
+
for (const [bin, argv] of managers) {
|
|
138
|
+
if (!has(bin)) continue;
|
|
139
|
+
return safeSpawn(bin, argv, { stdio: "inherit" }).status === 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// tmux is required — the TUI runs inside it — so this provisions if missing and
|
|
146
|
+
// fails hard only when tmux still can't be made available.
|
|
147
|
+
function ensureTmux() {
|
|
148
|
+
if (tmuxSatisfiesMin()) return;
|
|
149
|
+
if (process.env.AIUR_SKIP_TMUX_INSTALL !== "1") {
|
|
150
|
+
process.stderr.write(
|
|
151
|
+
`aiur: tmux (>= ${MIN_TMUX.join(".")}) runs the interactive TUI; installing it…\n`,
|
|
152
|
+
);
|
|
153
|
+
installTmux(process.platform);
|
|
154
|
+
}
|
|
155
|
+
if (!tmuxSatisfiesMin()) {
|
|
156
|
+
const v = tmuxVersion();
|
|
88
157
|
fail(
|
|
89
|
-
|
|
90
|
-
|
|
158
|
+
v
|
|
159
|
+
? `tmux ${v[0]}.${v[1]} is too old; aiur needs >= ${MIN_TMUX.join(".")}. ${tmuxHint()}`
|
|
160
|
+
: `tmux is required but was not found on PATH. ${tmuxHint()}`,
|
|
91
161
|
);
|
|
92
162
|
}
|
|
93
163
|
}
|
|
94
164
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
165
|
+
// True only when the pinned opencode is on PATH; a wrong version (e.g. 1.17.x,
|
|
166
|
+
// which broke the panes) must be replaced, so presence alone isn't enough.
|
|
167
|
+
function opencodeMatchesPin() {
|
|
168
|
+
const result = safeSpawn("opencode", ["--version"], { encoding: "utf8" });
|
|
169
|
+
return !result.error && result.status === 0 && (result.stdout || "").trim() === opencodeVersion();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Install the pinned opencode globally. Forces online so it never inherits an
|
|
173
|
+
// --offline / prefer-offline context (CI, mirrors, air-gapped) that would fail
|
|
174
|
+
// on a cold cache — mirrors scripts/postinstall.mjs:installOpencode.
|
|
175
|
+
function installOpencode() {
|
|
176
|
+
const env = { ...process.env };
|
|
177
|
+
delete env.npm_config_offline;
|
|
178
|
+
delete env.npm_config_prefer_offline;
|
|
179
|
+
const result = safeSpawn(
|
|
180
|
+
"npm",
|
|
181
|
+
["install", "-g", `opencode-ai@${opencodeVersion()}`, "--no-offline", "--no-prefer-offline"],
|
|
182
|
+
{ stdio: "inherit", env },
|
|
183
|
+
);
|
|
184
|
+
return !result.error && result.status === 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// opencode powers the interactive "take the wheel" panes. Non-fatal: everything
|
|
188
|
+
// else works without it.
|
|
189
|
+
function ensureOpencode() {
|
|
190
|
+
if (opencodeMatchesPin()) return;
|
|
191
|
+
if (process.env.AIUR_SKIP_OPENCODE_INSTALL !== "1") {
|
|
98
192
|
process.stderr.write(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
193
|
+
`aiur: provisioning opencode@${opencodeVersion()} (one-time, for the interactive 'take the wheel' panes)…\n`,
|
|
194
|
+
);
|
|
195
|
+
installOpencode();
|
|
196
|
+
if (opencodeMatchesPin()) return;
|
|
197
|
+
}
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
"aiur: opencode was not found on PATH. The interactive 'take the wheel' " +
|
|
200
|
+
"feature needs it; everything else works without it. " +
|
|
201
|
+
"Install from https://opencode.ai\n",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Update notifier
|
|
207
|
+
//
|
|
208
|
+
// When a newer aiur-cli is published, surface a one-line "update available"
|
|
209
|
+
// notice — npm-update-notifier style: every run reads a small cache file and
|
|
210
|
+
// prints from it; the registry is only hit in a detached background worker,
|
|
211
|
+
// at most once per interval, so the check never blocks or errors the command.
|
|
212
|
+
// The notice always goes to stderr so piped stdout stays clean.
|
|
213
|
+
//
|
|
214
|
+
// This file is its own background worker: main() re-spawns it with
|
|
215
|
+
// AIUR_UPDATE_NOTIFIER_WORKER=1 set, which short-circuits to runUpdateWorker().
|
|
216
|
+
// Keeps the launcher dependency-free (a tiny semver compare + https.get).
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
220
|
+
const REGISTRY_LATEST_URL = "https://registry.npmjs.org/aiur-cli/latest";
|
|
221
|
+
// Unpublished/source builds carry this sentinel; never nag on a dev checkout.
|
|
222
|
+
const DEV_VERSION = "0.0.0";
|
|
223
|
+
|
|
224
|
+
function currentVersion() {
|
|
225
|
+
try {
|
|
226
|
+
const pkg = JSON.parse(
|
|
227
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
|
|
228
|
+
);
|
|
229
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
230
|
+
} catch (_) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Minimal semver: parse major.minor.patch with an optional prerelease tag.
|
|
236
|
+
// Enough for the launcher's own clean release versions — not a full spec impl.
|
|
237
|
+
// The regex is fully anchored (and tolerates a +build suffix) so a registry- or
|
|
238
|
+
// cache-supplied string with trailing junk — e.g. control/ANSI bytes meant to
|
|
239
|
+
// hijack the terminal when the notice is printed — fails to parse and is treated
|
|
240
|
+
// as not-newer rather than reaching printUpdateNotice verbatim.
|
|
241
|
+
function parseSemver(value) {
|
|
242
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
243
|
+
String(value).trim(),
|
|
244
|
+
);
|
|
245
|
+
if (!match) return null;
|
|
246
|
+
return { major: +match[1], minor: +match[2], patch: +match[3], pre: match[4] || "" };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// True when `a` is strictly newer than `b`. A release outranks a prerelease at
|
|
250
|
+
// the same core version (1.0.0 > 1.0.0-rc.1); unparseable input is never newer.
|
|
251
|
+
function semverGt(a, b) {
|
|
252
|
+
const pa = parseSemver(a);
|
|
253
|
+
const pb = parseSemver(b);
|
|
254
|
+
if (!pa || !pb) return false;
|
|
255
|
+
if (pa.major !== pb.major) return pa.major > pb.major;
|
|
256
|
+
if (pa.minor !== pb.minor) return pa.minor > pb.minor;
|
|
257
|
+
if (pa.patch !== pb.patch) return pa.patch > pb.patch;
|
|
258
|
+
if (pa.pre === pb.pre) return false;
|
|
259
|
+
if (!pa.pre) return true; // a is a release, b is a prerelease
|
|
260
|
+
if (!pb.pre) return false; // a is a prerelease, b is a release
|
|
261
|
+
return pa.pre > pb.pre; // both prereleases: lexical tiebreak
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function cacheFile() {
|
|
265
|
+
const base = process.env.XDG_CACHE_HOME
|
|
266
|
+
? path.join(process.env.XDG_CACHE_HOME, "aiur")
|
|
267
|
+
: path.join(os.homedir(), ".aiur");
|
|
268
|
+
return path.join(base, "update-check.json");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function readCache() {
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(fs.readFileSync(cacheFile(), "utf8"));
|
|
274
|
+
} catch (_) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function writeCache(data) {
|
|
280
|
+
try {
|
|
281
|
+
const file = cacheFile();
|
|
282
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
283
|
+
// Write-then-rename so a reader never sees a half-written file: if the
|
|
284
|
+
// detached worker is killed mid-write, or two workers race, the rename is
|
|
285
|
+
// atomic on POSIX and a torn cache can't trigger a respawn-every-run loop.
|
|
286
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
287
|
+
fs.writeFileSync(tmp, JSON.stringify(data));
|
|
288
|
+
fs.renameSync(tmp, file);
|
|
289
|
+
} catch (_) {
|
|
290
|
+
// A missing-cache write must never break the run; silently skip.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Treat any non-falsy CI value except an explicit "false"/"0" as a CI run.
|
|
295
|
+
function isCI() {
|
|
296
|
+
const value = process.env.CI;
|
|
297
|
+
return !!value && value !== "false" && value !== "0";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function notifierDisabled(current) {
|
|
301
|
+
return (
|
|
302
|
+
process.env.AIUR_NO_UPDATE_NOTIFIER === "1" ||
|
|
303
|
+
isCI() ||
|
|
304
|
+
!current ||
|
|
305
|
+
current === DEV_VERSION
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function printUpdateNotice(current, latest) {
|
|
310
|
+
process.stderr.write(
|
|
311
|
+
`aiur: a new version is available — ${current} → ${latest}\n` +
|
|
312
|
+
" update: npm install -g aiur-cli@latest\n",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Synchronous, fast, never-throwing: print from cache when a newer version is
|
|
317
|
+
// known, then kick off a detached refresh if the cache is stale. The notice is
|
|
318
|
+
// TTY-gated so non-interactive/piped invocations stay quiet.
|
|
319
|
+
function maybeNotifyUpdate() {
|
|
320
|
+
try {
|
|
321
|
+
const current = currentVersion();
|
|
322
|
+
if (notifierDisabled(current)) return;
|
|
323
|
+
|
|
324
|
+
const cache = readCache();
|
|
325
|
+
if (
|
|
326
|
+
cache &&
|
|
327
|
+
cache.latest &&
|
|
328
|
+
process.stderr.isTTY &&
|
|
329
|
+
semverGt(cache.latest, current)
|
|
330
|
+
) {
|
|
331
|
+
printUpdateNotice(current, cache.latest);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const stale =
|
|
335
|
+
!cache ||
|
|
336
|
+
typeof cache.lastCheck !== "number" ||
|
|
337
|
+
Date.now() - cache.lastCheck > UPDATE_CHECK_INTERVAL_MS;
|
|
338
|
+
if (stale) spawnUpdateWorker();
|
|
339
|
+
} catch (_) {
|
|
340
|
+
// The version check is best-effort; never let it affect the command.
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function spawnUpdateWorker() {
|
|
345
|
+
try {
|
|
346
|
+
const child = spawn(process.execPath, [__filename], {
|
|
347
|
+
detached: true,
|
|
348
|
+
stdio: "ignore",
|
|
349
|
+
env: { ...process.env, AIUR_UPDATE_NOTIFIER_WORKER: "1" },
|
|
350
|
+
});
|
|
351
|
+
child.unref();
|
|
352
|
+
} catch (_) {
|
|
353
|
+
// If we can't detach a worker, just skip — the next run retries.
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Fetch the `latest` dist-tag's version. Bounded by a timeout and a response
|
|
358
|
+
// cap; any error resolves to null so the worker stays silent when offline.
|
|
359
|
+
function fetchLatestVersion(url, done) {
|
|
360
|
+
let settled = false;
|
|
361
|
+
const finish = (value) => {
|
|
362
|
+
if (settled) return;
|
|
363
|
+
settled = true;
|
|
364
|
+
done(value);
|
|
365
|
+
};
|
|
366
|
+
try {
|
|
367
|
+
const transport = url.startsWith("http://") ? require("node:http") : require("node:https");
|
|
368
|
+
const req = transport.get(
|
|
369
|
+
url,
|
|
370
|
+
{ headers: { Accept: "application/json" }, timeout: 5000 },
|
|
371
|
+
(res) => {
|
|
372
|
+
if (res.statusCode !== 200) {
|
|
373
|
+
res.resume();
|
|
374
|
+
return finish(null);
|
|
375
|
+
}
|
|
376
|
+
let body = "";
|
|
377
|
+
res.setEncoding("utf8");
|
|
378
|
+
res.on("data", (chunk) => {
|
|
379
|
+
body += chunk;
|
|
380
|
+
// Abort an oversized body. req.destroy() emits only "close" (no "end"
|
|
381
|
+
// or "error"), so resolve here explicitly — otherwise the worker would
|
|
382
|
+
// hang forever and never stamp the cache, respawning on every run.
|
|
383
|
+
if (body.length > 1_000_000) {
|
|
384
|
+
req.destroy();
|
|
385
|
+
finish(null);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
res.on("end", () => {
|
|
389
|
+
try {
|
|
390
|
+
const version = JSON.parse(body).version;
|
|
391
|
+
finish(typeof version === "string" ? version : null);
|
|
392
|
+
} catch (_) {
|
|
393
|
+
finish(null);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
},
|
|
102
397
|
);
|
|
398
|
+
// A timeout's req.destroy() likewise emits only "close", so resolve here too
|
|
399
|
+
// rather than waiting on an event that never comes. "error" covers the
|
|
400
|
+
// connection-failure case (offline, refused).
|
|
401
|
+
req.on("timeout", () => {
|
|
402
|
+
req.destroy();
|
|
403
|
+
finish(null);
|
|
404
|
+
});
|
|
405
|
+
req.on("error", () => finish(null));
|
|
406
|
+
} catch (_) {
|
|
407
|
+
finish(null);
|
|
103
408
|
}
|
|
104
409
|
}
|
|
105
410
|
|
|
411
|
+
// Detached worker entrypoint: refresh the cache, then exit. Always stamps
|
|
412
|
+
// lastCheck (even on failure) so an offline machine doesn't respawn every run.
|
|
413
|
+
function runUpdateWorker() {
|
|
414
|
+
const previous = readCache();
|
|
415
|
+
const url = process.env.AIUR_REGISTRY_URL || REGISTRY_LATEST_URL;
|
|
416
|
+
fetchLatestVersion(url, (latest) => {
|
|
417
|
+
writeCache({
|
|
418
|
+
lastCheck: Date.now(),
|
|
419
|
+
// Keep the last known `latest` when the fetch fails so a transient outage
|
|
420
|
+
// doesn't drop a pending notice.
|
|
421
|
+
latest: latest || (previous && previous.latest) || null,
|
|
422
|
+
});
|
|
423
|
+
process.exit(0);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
106
427
|
// `init` and `--version` run as foreground one-shots that never start the
|
|
107
|
-
// tmux-backed UI, so their tmux/opencode
|
|
108
|
-
// legitimately be absent on a machine that only ever runs `aiur init`.
|
|
428
|
+
// tmux-backed UI, so their tmux/opencode provisioning is irrelevant — and tmux
|
|
429
|
+
// may legitimately be absent on a machine that only ever runs `aiur init`.
|
|
109
430
|
function isForegroundOneShot(argv) {
|
|
110
431
|
for (const arg of argv) {
|
|
111
432
|
if (arg === "--version") return true;
|
|
@@ -116,11 +437,19 @@ function isForegroundOneShot(argv) {
|
|
|
116
437
|
}
|
|
117
438
|
|
|
118
439
|
function main() {
|
|
440
|
+
// Detached refresh worker: do only the registry check, never the real launch.
|
|
441
|
+
if (process.env.AIUR_UPDATE_NOTIFIER_WORKER === "1") {
|
|
442
|
+
runUpdateWorker();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
maybeNotifyUpdate();
|
|
447
|
+
|
|
119
448
|
const releaseDir = resolveReleaseDir();
|
|
120
449
|
|
|
121
450
|
if (!isForegroundOneShot(process.argv.slice(2))) {
|
|
122
|
-
|
|
123
|
-
|
|
451
|
+
ensureTmux();
|
|
452
|
+
ensureOpencode();
|
|
124
453
|
}
|
|
125
454
|
|
|
126
455
|
const pkgRoot = path.resolve(__dirname, "..");
|