Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0dc1445fc |
@@ -463,7 +463,10 @@ export function UpdateNotifier() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{error ? (
|
{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}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -7174,7 +7174,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-status-warning/90"
|
class="text-status-warning/90"
|
||||||
>
|
>
|
||||||
UI build v2.6.1 (rebuild client to match)
|
UI build v2.6.2 (rebuild client to match)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.6.1",
|
"version": "2.6.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.6.1",
|
"version": "2.6.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.6.1",
|
"version": "2.6.2",
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "bin/agenthub.js"
|
"agenthub": "bin/agenthub.js"
|
||||||
},
|
},
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"dev:client": "cd client && npm run dev",
|
"dev:client": "cd client && npm run dev",
|
||||||
"build": "cd client && npm run build",
|
"build": "cd client && npm run build",
|
||||||
"start": "node server/index.js",
|
"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",
|
"update:pull-setup": "git pull --ff-only && npm run setup",
|
||||||
"install-hooks": "node scripts/install-hooks.js",
|
"install-hooks": "node scripts/install-hooks.js",
|
||||||
"seed": "node scripts/seed.js",
|
"seed": "node scripts/seed.js",
|
||||||
|
|||||||
@@ -155,9 +155,10 @@ function reloadPanes() {
|
|||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
// psmux (Windows) ignores -F: `name: 1 windows (…)`; a name holds no `:`.
|
||||||
names = out
|
names = out
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.split(":")[0].trim())
|
||||||
.filter((s) => s.startsWith("agenthub-lane-"));
|
.filter((s) => s.startsWith("agenthub-lane-"));
|
||||||
} catch {
|
} catch {
|
||||||
// No tmux server at all means no open sessions — nothing to reload, which
|
// 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"]);
|
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)", () => {
|
it("liveSessions is empty when tmux answers that no server runs (or is not installed)", () => {
|
||||||
tmux.__setExecImpl(() => {
|
tmux.__setExecImpl(() => {
|
||||||
throw Object.assign(new Error("no server running"), { status: 1 });
|
throw Object.assign(new Error("no server running"), { status: 1 });
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const os = require("os");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { execFileSync } = require("child_process");
|
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
|
// 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
|
// 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"]);
|
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.
|
// too and is shared; any other failure is not.
|
||||||
const LIST_TTL_MS = 1000;
|
const LIST_TTL_MS = 1000;
|
||||||
const LIST_ARGS = ["list-sessions", "-F", "#{session_name}"];
|
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 listed = new Map(); // member login ("" = ours) → { at, out } | { at, err }
|
||||||
let listGen = 0; // bumped by a change this process makes: drops lists in flight
|
let listGen = 0; // bumped by a change this process makes: drops lists in flight
|
||||||
function listOutput(acct = null) {
|
function listOutput(acct = null) {
|
||||||
@@ -206,7 +215,7 @@ function listOutput(acct = null) {
|
|||||||
return hit.out;
|
return hit.out;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const out = String(execImpl(LIST_ARGS));
|
const out = sessionNames(execImpl(LIST_ARGS));
|
||||||
listed.set("", { at: Date.now(), out });
|
listed.set("", { at: Date.now(), out });
|
||||||
return out;
|
return out;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -252,7 +261,7 @@ function refreshList(acct) {
|
|||||||
// about another pane) is not believed.
|
// about another pane) is not believed.
|
||||||
(out) =>
|
(out) =>
|
||||||
done({
|
done({
|
||||||
out: String(out)
|
out: sessionNames(out)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((n) => ownedBy(n.trim(), acct))
|
.filter((n) => ownedBy(n.trim(), acct))
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
@@ -308,7 +317,7 @@ function exact() {
|
|||||||
if (exactTargets !== null) return exactTargets;
|
if (exactTargets !== null) return exactTargets;
|
||||||
let first;
|
let first;
|
||||||
try {
|
try {
|
||||||
first = String(execImpl(["list-sessions", "-F", "#{session_name}"]))
|
first = sessionNames(execImpl(LIST_ARGS))
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((x) => x.trim())
|
.map((x) => x.trim())
|
||||||
.find(Boolean);
|
.find(Boolean);
|
||||||
|
|||||||
@@ -94,19 +94,32 @@ function npmInvocation(args, env = process.env) {
|
|||||||
return require("../../scripts/plugin-bootstrap").npmCommand(args, { env: rest });
|
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
|
/** 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 = {}) {
|
function execNpm(cwd, args, opts = {}) {
|
||||||
const timeout = opts.timeout ?? 300_000;
|
const timeout = opts.timeout ?? NPM_TIMEOUT_MS;
|
||||||
const npm = npmInvocation(args);
|
const npm = npmInvocation(args);
|
||||||
|
const step = `npm ${args.join(" ")}`;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
execFile(
|
execFile(
|
||||||
npm.file,
|
npm.file,
|
||||||
npm.args,
|
npm.args,
|
||||||
{ cwd, timeout, maxBuffer: 5_000_000, encoding: "utf8", env: npm.env, shell: npm.shell },
|
{ cwd, timeout, maxBuffer: 5_000_000, encoding: "utf8", env: npm.env, shell: npm.shell },
|
||||||
(err, stdout, stderr) => {
|
(err, stdout, stderr) => {
|
||||||
if (err) reject(new Error(stderr || err.message || String(err)));
|
if (!err) return resolve(String(stdout).trim());
|
||||||
else 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,
|
repoUrlFor,
|
||||||
isShallowClone,
|
isShallowClone,
|
||||||
npmInvocation,
|
npmInvocation,
|
||||||
|
execNpm,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user