fix: v2.6.2 — Windows panes no longer read as dead (psmux ignores -F), Update now's setup works under cmd.exe
- psmux prints `name: 1 windows (created …)` whatever `list-sessions -F`
asks for. Read as names, every live pane looked gone and the heal beat
closed its run (and its tab) within seconds while the session kept
running. tmux.js (and the Update all helper) now take the text before the
first `:` — a session name never holds one.
- `npm run setup` used `(cd client && npm install)`; cmd.exe's ( ) is no
subshell, so `npm run link-cli` ran inside client/ ("Missing script") and
Update now failed on Windows. Now `npm --prefix client install`.
- Update now: an npm step names itself in its error and shows the END of
npm's output (the error, not the warnings), says when it hit the time
limit, and gets 20 min instead of 5.
This commit is contained in:
@@ -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