Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0dc1445fc |
@@ -463,7 +463,10 @@ export function UpdateNotifier() {
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p className="text-xs text-status-danger" role="alert">
|
||||
<p
|
||||
className="text-xs text-status-danger whitespace-pre-wrap break-words max-h-48 overflow-auto"
|
||||
role="alert"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -7174,7 +7174,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
||||
<span
|
||||
class="text-status-warning/90"
|
||||
>
|
||||
UI build v2.6.1 (rebuild client to match)
|
||||
UI build v2.6.2 (rebuild client to match)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agenthub",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.2",
|
||||
"bin": {
|
||||
"agenthub": "bin/agenthub.js"
|
||||
},
|
||||
@@ -55,7 +55,7 @@
|
||||
"dev:client": "cd client && npm run dev",
|
||||
"build": "cd client && npm run build",
|
||||
"start": "node server/index.js",
|
||||
"setup": "npm install && (cd client && npm install) && npm run link-cli",
|
||||
"setup": "npm install && npm --prefix client install && npm run link-cli",
|
||||
"update:pull-setup": "git pull --ff-only && npm run setup",
|
||||
"install-hooks": "node scripts/install-hooks.js",
|
||||
"seed": "node scripts/seed.js",
|
||||
|
||||
@@ -155,9 +155,10 @@ function reloadPanes() {
|
||||
timeout: 15_000,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
// psmux (Windows) ignores -F: `name: 1 windows (…)`; a name holds no `:`.
|
||||
names = out
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.map((s) => s.split(":")[0].trim())
|
||||
.filter((s) => s.startsWith("agenthub-lane-"));
|
||||
} catch {
|
||||
// No tmux server at all means no open sessions — nothing to reload, which
|
||||
|
||||
@@ -38,6 +38,19 @@ describe("tmux wrapper", () => {
|
||||
assert.deepEqual(calls, ["list-sessions"]);
|
||||
});
|
||||
|
||||
it("reads psmux's list-sessions, which ignores -F, as bare names (Windows)", () => {
|
||||
// psmux prints its default line whatever `-F` asks for; read as names,
|
||||
// every pane looked dead and the heal beat closed its run within seconds.
|
||||
tmux.__setExecImpl((args) =>
|
||||
args[0] === "list-sessions"
|
||||
? "agenthub-lane-2-5: 1 windows (created Sat Oct 10 11:44:39 2026)\r\n" +
|
||||
"agenthub-lane-2: 1 windows (created Sat Oct 10 10:43:16 2026)\r\n"
|
||||
: ""
|
||||
);
|
||||
assert.deepEqual([...tmux.liveSessions()], ["agenthub-lane-2-5", "agenthub-lane-2"]);
|
||||
assert.deepEqual(tmux.listSessions("agenthub-lane-"), ["agenthub-lane-2-5", "agenthub-lane-2"]);
|
||||
});
|
||||
|
||||
it("liveSessions is empty when tmux answers that no server runs (or is not installed)", () => {
|
||||
tmux.__setExecImpl(() => {
|
||||
throw Object.assign(new Error("no server running"), { status: 1 });
|
||||
|
||||
@@ -12,7 +12,7 @@ const os = require("os");
|
||||
const path = require("path");
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
const { getUpdatesStatus, npmInvocation } = require("../lib/update-check");
|
||||
const { getUpdatesStatus, npmInvocation, execNpm } = require("../lib/update-check");
|
||||
|
||||
// Strip GIT_* vars a parent git hook (e.g. the pre-commit hook running this
|
||||
// very suite) sets in its own environment — those leak to every child
|
||||
@@ -199,3 +199,49 @@ describe("npmInvocation (the checkout's Update now)", () => {
|
||||
assert.deepEqual(npm.args.slice(-2), ["run", "build"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("npm run setup (the checkout's Update now)", () => {
|
||||
it("never changes directory: cmd.exe's ( ) is no subshell, so a `cd` leaks into the next step", () => {
|
||||
// On Windows `(cd client && npm install) && npm run link-cli` ran
|
||||
// link-cli inside client/ — "Missing script" — and Update now failed.
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8"));
|
||||
assert.doesNotMatch(pkg.scripts.setup, /\bcd\b/);
|
||||
assert.match(pkg.scripts.setup, /npm --prefix client install/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("execNpm (the checkout's Update now)", () => {
|
||||
let dir;
|
||||
before(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), "exec-npm-"));
|
||||
const noise = "for (let i = 0; i < 60; i++) console.error('npm warn noise ' + i);";
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "fx",
|
||||
version: "1.0.0",
|
||||
scripts: {
|
||||
fail: `node -e "${noise} console.error('ERR the real cause'); process.exit(1)"`,
|
||||
slow: `node -e "setTimeout(() => {}, 60000)"`,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
it("names the failed step and keeps the end of npm's output, where the error is", async () => {
|
||||
await assert.rejects(execNpm(dir, ["run", "fail"]), (err) => {
|
||||
assert.match(err.message, /^npm run fail failed/);
|
||||
assert.match(err.message, /ERR the real cause/);
|
||||
assert.doesNotMatch(err.message, /noise 0\b/);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it("says a step was stopped at the time limit instead of echoing its warnings", async () => {
|
||||
await assert.rejects(execNpm(dir, ["run", "slow"], { timeout: 3000 }), (err) => {
|
||||
assert.match(err.message, /npm run slow did not finish within/);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+12
-3
@@ -196,6 +196,15 @@ function __reset() {
|
||||
// too and is shared; any other failure is not.
|
||||
const LIST_TTL_MS = 1000;
|
||||
const LIST_ARGS = ["list-sessions", "-F", "#{session_name}"];
|
||||
// psmux (Windows) ignores `-F` and prints `name: 1 windows (created …)`. A
|
||||
// session name never holds a `:` (tmux rewrites it), so the text before the
|
||||
// first one is the name in either form.
|
||||
const sessionNames = (out) =>
|
||||
String(out)
|
||||
.split("\n")
|
||||
.map((l) => l.split(":")[0].trim())
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
let listed = new Map(); // member login ("" = ours) → { at, out } | { at, err }
|
||||
let listGen = 0; // bumped by a change this process makes: drops lists in flight
|
||||
function listOutput(acct = null) {
|
||||
@@ -206,7 +215,7 @@ function listOutput(acct = null) {
|
||||
return hit.out;
|
||||
}
|
||||
try {
|
||||
const out = String(execImpl(LIST_ARGS));
|
||||
const out = sessionNames(execImpl(LIST_ARGS));
|
||||
listed.set("", { at: Date.now(), out });
|
||||
return out;
|
||||
} catch (err) {
|
||||
@@ -252,7 +261,7 @@ function refreshList(acct) {
|
||||
// about another pane) is not believed.
|
||||
(out) =>
|
||||
done({
|
||||
out: String(out)
|
||||
out: sessionNames(out)
|
||||
.split("\n")
|
||||
.filter((n) => ownedBy(n.trim(), acct))
|
||||
.join("\n"),
|
||||
@@ -308,7 +317,7 @@ function exact() {
|
||||
if (exactTargets !== null) return exactTargets;
|
||||
let first;
|
||||
try {
|
||||
first = String(execImpl(["list-sessions", "-F", "#{session_name}"]))
|
||||
first = sessionNames(execImpl(LIST_ARGS))
|
||||
.split("\n")
|
||||
.map((x) => x.trim())
|
||||
.find(Boolean);
|
||||
|
||||
@@ -94,19 +94,32 @@ function npmInvocation(args, env = process.env) {
|
||||
return require("../../scripts/plugin-bootstrap").npmCommand(args, { env: rest });
|
||||
}
|
||||
|
||||
// `npm run setup` installs the client's dependencies more than once and
|
||||
// `npm run build` type-checks before bundling; on Windows (Defender scanning
|
||||
// every file) each step ran past the old 5-minute cap.
|
||||
const NPM_TIMEOUT_MS = 20 * 60_000;
|
||||
|
||||
/** Same shape as {@link execGit}, for the `npm run setup`/`npm run build` steps
|
||||
* {@link applyUpdate} runs after fast-forwarding. */
|
||||
* {@link applyUpdate} runs after fast-forwarding. A failure names its step and
|
||||
* carries the END of npm's output — the error, not the warnings before it. */
|
||||
function execNpm(cwd, args, opts = {}) {
|
||||
const timeout = opts.timeout ?? 300_000;
|
||||
const timeout = opts.timeout ?? NPM_TIMEOUT_MS;
|
||||
const npm = npmInvocation(args);
|
||||
const step = `npm ${args.join(" ")}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
npm.file,
|
||||
npm.args,
|
||||
{ cwd, timeout, maxBuffer: 5_000_000, encoding: "utf8", env: npm.env, shell: npm.shell },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) reject(new Error(stderr || err.message || String(err)));
|
||||
else resolve(String(stdout).trim());
|
||||
if (!err) return resolve(String(stdout).trim());
|
||||
if (err.killed) {
|
||||
return reject(
|
||||
new Error(`${step} did not finish within ${Math.round(timeout / 60_000)} min and was stopped`)
|
||||
);
|
||||
}
|
||||
const tail = String(stderr || err.message || err).trim().split(/\r?\n/).slice(-15).join("\n");
|
||||
reject(new Error(`${step} failed:\n${tail}`));
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -880,4 +893,5 @@ module.exports = {
|
||||
repoUrlFor,
|
||||
isShallowClone,
|
||||
npmInvocation,
|
||||
execNpm,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user