@sealant/mend 0.34.2 → 0.34.4
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/dist/{chunk-JJ66V76Y.js → chunk-AAAXHRWL.js} +72 -0
- package/dist/{dashboard-OADX3PHI.js → dashboard-DWXXYFPE.js} +32 -13
- package/dist/main.js +320 -89
- package/man/mend-accounts.1 +1 -1
- package/man/mend-adopt.1 +1 -1
- package/man/mend-attach.1 +1 -1
- package/man/mend-codex.1 +1 -1
- package/man/mend-completions.1 +1 -1
- package/man/mend-connect.1 +1 -1
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.1 +1 -1
- package/man/mend-dotfiles-repo.1 +1 -1
- package/man/mend-dotfiles-sync.1 +1 -1
- package/man/mend-dotfiles.1 +1 -1
- package/man/mend-env-cluster.1 +1 -1
- package/man/mend-env-load.1 +1 -1
- package/man/mend-env-show.1 +1 -1
- package/man/mend-folder-create.1 +1 -1
- package/man/mend-folder-list.1 +1 -1
- package/man/mend-folder-push.1 +1 -1
- package/man/mend-folder-rm.1 +1 -1
- package/man/mend-git-author.1 +1 -1
- package/man/mend-help.1 +1 -1
- package/man/mend-invite.1 +1 -1
- package/man/mend-keys-init.1 +1 -1
- package/man/mend-keys-mode.1 +1 -1
- package/man/mend-keys-share.1 +1 -1
- package/man/mend-keys-show.1 +1 -1
- package/man/mend-land.1 +1 -1
- package/man/mend-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-man.1 +1 -1
- package/man/mend-members.1 +1 -1
- package/man/mend-operator-exposure.1 +1 -1
- package/man/mend-operator-gate.1 +1 -1
- package/man/mend-operator-grant-owner.1 +1 -1
- package/man/mend-operator-org-create.1 +1 -1
- package/man/mend-operator-org-invite-owner.1 +1 -1
- package/man/mend-operator-org-list.1 +1 -1
- package/man/mend-operator-org-rename.1 +1 -1
- package/man/mend-operator-reset-link.1 +1 -1
- package/man/mend-pair.1 +1 -1
- package/man/mend-projects.1 +1 -1
- package/man/mend-pull.1 +1 -1
- package/man/mend-refresh.1 +1 -1
- package/man/mend-rejoin.1 +1 -1
- package/man/mend-resume.1 +1 -1
- package/man/mend-run.1 +1 -1
- package/man/mend-server-logs.1 +1 -1
- package/man/mend-server-restart.1 +1 -1
- package/man/mend-server-setup.1 +1 -1
- package/man/mend-server-start.1 +1 -1
- package/man/mend-server-status.1 +1 -1
- package/man/mend-server-stop.1 +1 -1
- package/man/mend-server-upgrade.1 +1 -1
- package/man/mend-server.1 +1 -1
- package/man/mend-service-add.1 +1 -1
- package/man/mend-service-connect.1 +1 -1
- package/man/mend-service-init.1 +1 -1
- package/man/mend-service-list.1 +1 -1
- package/man/mend-service-logs.1 +1 -1
- package/man/mend-service-restart.1 +1 -1
- package/man/mend-service-run.1 +1 -1
- package/man/mend-service-stop.1 +1 -1
- package/man/mend-session-share.1 +1 -1
- package/man/mend-sessions.1 +1 -1
- package/man/mend-shell.1 +1 -1
- package/man/mend-skills-push.1 +1 -1
- package/man/mend-skills.1 +1 -1
- package/man/mend-snake.1 +1 -1
- package/man/mend-ssh-setup.1 +1 -1
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-uninstall.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +1 -1
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
GATEWAY_STATUSES,
|
|
3
4
|
HARNESS_COMMANDS,
|
|
4
5
|
LIVE_STATUSES,
|
|
6
|
+
MendRequestError,
|
|
5
7
|
agentIsLive,
|
|
6
8
|
agentOutcome,
|
|
7
9
|
captureLineOf,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
landingFactsFromWire,
|
|
19
21
|
matchProjectByCwd,
|
|
20
22
|
narrowCredential,
|
|
23
|
+
noAnswerError,
|
|
21
24
|
normalizeGitAuthor,
|
|
22
25
|
normalizeProjectName,
|
|
23
26
|
normalizeRemoteUrl,
|
|
@@ -27,8 +30,9 @@ import {
|
|
|
27
30
|
sameGrant,
|
|
28
31
|
servicesHoldOf,
|
|
29
32
|
sessionDisplayName,
|
|
33
|
+
spoken,
|
|
30
34
|
trackBracketedPaste
|
|
31
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-AAAXHRWL.js";
|
|
32
36
|
|
|
33
37
|
// src/main.ts
|
|
34
38
|
import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -3655,6 +3659,78 @@ var pullCommand = async (api2, download2, args, cwd = process.cwd()) => {
|
|
|
3655
3659
|
for (const line of fetchedLines(fetched)) say(line);
|
|
3656
3660
|
};
|
|
3657
3661
|
|
|
3662
|
+
// src/launch-follow.ts
|
|
3663
|
+
var SETTLED = /* @__PURE__ */ new Set(["completed", "failed", "stopped", "stopping"]);
|
|
3664
|
+
var agentRuns = (agent) => agent !== null && agent.kind !== "agent-protocol" && agent.exitedAt === null && agent.status === "running" && agent.sealantSessionId !== null;
|
|
3665
|
+
var startingLineOf = (session) => {
|
|
3666
|
+
const summary = session.summary?.trim() ?? "";
|
|
3667
|
+
if (summary === "") return session.status;
|
|
3668
|
+
if (/^[a-z][a-z -]*·/u.test(summary)) return summary;
|
|
3669
|
+
return `${session.status} \xB7 ${summary}`;
|
|
3670
|
+
};
|
|
3671
|
+
var refusalOf = (error) => error instanceof MendRequestError && error.kind === "http" && (error.status === null || !GATEWAY_STATUSES.has(error.status)) ? error.message : null;
|
|
3672
|
+
var followStart = async (options) => {
|
|
3673
|
+
const pollMs = options.pollMs ?? 2e3;
|
|
3674
|
+
const unreachableAfterMs = options.unreachableAfterMs ?? 6e4;
|
|
3675
|
+
const launch2 = {
|
|
3676
|
+
state: options.start === null ? { kind: "no-answer" } : { kind: "pending" }
|
|
3677
|
+
};
|
|
3678
|
+
const start = options.start;
|
|
3679
|
+
const launched = (async () => {
|
|
3680
|
+
if (start === null) return;
|
|
3681
|
+
try {
|
|
3682
|
+
launch2.state = { kind: "answered", session: await start };
|
|
3683
|
+
} catch (error) {
|
|
3684
|
+
const refusal = refusalOf(error);
|
|
3685
|
+
launch2.state = refusal === null ? { kind: "no-answer" } : { kind: "refused", message: refusal };
|
|
3686
|
+
}
|
|
3687
|
+
})();
|
|
3688
|
+
let lastLine = null;
|
|
3689
|
+
let failingSince = null;
|
|
3690
|
+
let seenStarting = false;
|
|
3691
|
+
for (; ; ) {
|
|
3692
|
+
if (launch2.state.kind === "pending") await Promise.race([launched, options.sleep(pollMs)]);
|
|
3693
|
+
const state = launch2.state;
|
|
3694
|
+
if (state.kind === "refused") return { kind: "refused", message: state.message };
|
|
3695
|
+
if (state.kind === "answered" && state.session.status === "running") {
|
|
3696
|
+
return { kind: "live", session: state.session };
|
|
3697
|
+
}
|
|
3698
|
+
let detail;
|
|
3699
|
+
try {
|
|
3700
|
+
detail = await options.read();
|
|
3701
|
+
} catch (error) {
|
|
3702
|
+
const refusal = refusalOf(error);
|
|
3703
|
+
if (refusal !== null) return { kind: "refused", message: refusal };
|
|
3704
|
+
failingSince ??= options.now();
|
|
3705
|
+
if (state.kind !== "pending" && options.now() - failingSince >= unreachableAfterMs) {
|
|
3706
|
+
return {
|
|
3707
|
+
kind: "unreachable",
|
|
3708
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
3711
|
+
if (state.kind !== "pending") await options.sleep(pollMs);
|
|
3712
|
+
continue;
|
|
3713
|
+
}
|
|
3714
|
+
failingSince = null;
|
|
3715
|
+
const { session, currentAgent } = detail;
|
|
3716
|
+
if (agentRuns(currentAgent)) return { kind: "live", session };
|
|
3717
|
+
if (SETTLED.has(session.status)) {
|
|
3718
|
+
if (seenStarting || state.kind !== "pending") return { kind: "settled", session };
|
|
3719
|
+
} else {
|
|
3720
|
+
seenStarting = true;
|
|
3721
|
+
}
|
|
3722
|
+
if (currentAgent === null && state.kind === "answered" && state.session.status === "running" && session.status === "running") {
|
|
3723
|
+
return { kind: "live", session };
|
|
3724
|
+
}
|
|
3725
|
+
const line = startingLineOf(session);
|
|
3726
|
+
if (line !== lastLine) {
|
|
3727
|
+
lastLine = line;
|
|
3728
|
+
options.onLine(line);
|
|
3729
|
+
}
|
|
3730
|
+
if (state.kind !== "pending") await options.sleep(pollMs);
|
|
3731
|
+
}
|
|
3732
|
+
};
|
|
3733
|
+
|
|
3658
3734
|
// src/login.ts
|
|
3659
3735
|
import { spawn as spawn2 } from "node:child_process";
|
|
3660
3736
|
import * as os5 from "node:os";
|
|
@@ -7483,9 +7559,10 @@ var herdrAgentOf = (harness) => {
|
|
|
7483
7559
|
if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
|
|
7484
7560
|
return null;
|
|
7485
7561
|
};
|
|
7562
|
+
var noHerdrHint = () => void 0;
|
|
7486
7563
|
var hintHerdrAttachment = (harness) => {
|
|
7487
7564
|
const agent = herdrAgentOf(harness);
|
|
7488
|
-
if (process.env["HERDR_ENV"] !== "1" || agent === null) return
|
|
7565
|
+
if (process.env["HERDR_ENV"] !== "1" || agent === null) return noHerdrHint;
|
|
7489
7566
|
const carrier = spawn3(
|
|
7490
7567
|
process.execPath,
|
|
7491
7568
|
["-e", "process.stdin.resume();process.stdin.once('end',()=>process.exit(0))"],
|
|
@@ -7511,6 +7588,8 @@ var request = async (config, method, route, body) => {
|
|
|
7511
7588
|
const headers = { "content-type": "application/json" };
|
|
7512
7589
|
if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
|
|
7513
7590
|
let response;
|
|
7591
|
+
const started = Date.now();
|
|
7592
|
+
const call = `${method} ${route}`;
|
|
7514
7593
|
try {
|
|
7515
7594
|
response = await fetch(`${config.url}/api${route}`, {
|
|
7516
7595
|
method,
|
|
@@ -7518,23 +7597,30 @@ var request = async (config, method, route, body) => {
|
|
|
7518
7597
|
// Spread, not `body: null` — fresh oxlint rejects a body key on GETs.
|
|
7519
7598
|
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
7520
7599
|
});
|
|
7521
|
-
} catch {
|
|
7522
|
-
throw
|
|
7600
|
+
} catch (error) {
|
|
7601
|
+
throw noAnswerError(error, config.url, call, Date.now() - started);
|
|
7523
7602
|
}
|
|
7524
7603
|
if (response.status === 401) {
|
|
7525
|
-
throw new
|
|
7526
|
-
|
|
7604
|
+
throw new MendRequestError(
|
|
7605
|
+
"http",
|
|
7606
|
+
config.token === null ? `not signed in to ${config.url} \u2014 run: mend login` : `unauthorized at ${config.url} \u2014 the saved token was rejected; run: mend login`,
|
|
7607
|
+
401
|
|
7527
7608
|
);
|
|
7528
7609
|
}
|
|
7529
|
-
|
|
7610
|
+
let text;
|
|
7611
|
+
try {
|
|
7612
|
+
text = await response.text();
|
|
7613
|
+
} catch (error) {
|
|
7614
|
+
throw noAnswerError(error, config.url, call, Date.now() - started);
|
|
7615
|
+
}
|
|
7530
7616
|
if (!response.ok) {
|
|
7531
|
-
let message = `${
|
|
7617
|
+
let message = `${call} \u2192 ${response.status}`;
|
|
7532
7618
|
try {
|
|
7533
7619
|
const parsed = JSON.parse(text);
|
|
7534
7620
|
message = parsed.message ?? message;
|
|
7535
7621
|
} catch {
|
|
7536
7622
|
}
|
|
7537
|
-
throw new
|
|
7623
|
+
throw new MendRequestError("http", message, response.status);
|
|
7538
7624
|
}
|
|
7539
7625
|
try {
|
|
7540
7626
|
return JSON.parse(text);
|
|
@@ -7553,10 +7639,11 @@ var download = (config) => async (route) => {
|
|
|
7553
7639
|
const headers = {};
|
|
7554
7640
|
if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
|
|
7555
7641
|
let response;
|
|
7642
|
+
const started = Date.now();
|
|
7556
7643
|
try {
|
|
7557
7644
|
response = await fetch(`${config.url}/api${route}`, { headers });
|
|
7558
|
-
} catch {
|
|
7559
|
-
return fail6(
|
|
7645
|
+
} catch (error) {
|
|
7646
|
+
return fail6(noAnswerError(error, config.url, `GET ${route}`, Date.now() - started).message);
|
|
7560
7647
|
}
|
|
7561
7648
|
if (response.status === 401) {
|
|
7562
7649
|
return fail6(
|
|
@@ -7567,13 +7654,14 @@ var download = (config) => async (route) => {
|
|
|
7567
7654
|
return { status: response.status, header: (name) => response.headers.get(name), bytes };
|
|
7568
7655
|
};
|
|
7569
7656
|
var warnedLegacyBearer = false;
|
|
7570
|
-
var mintTicket = (config) => async (target, params) => {
|
|
7657
|
+
var mintTicket = (config, signal) => async (target, params) => {
|
|
7571
7658
|
const headers = { "content-type": "application/json" };
|
|
7572
7659
|
if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
|
|
7573
7660
|
const response = await fetch(`${config.url}/api/upgrade-tickets`, {
|
|
7574
7661
|
method: "POST",
|
|
7575
7662
|
headers,
|
|
7576
|
-
body: JSON.stringify({ target, ...params })
|
|
7663
|
+
body: JSON.stringify({ target, ...params }),
|
|
7664
|
+
...signal === void 0 ? {} : { signal }
|
|
7577
7665
|
});
|
|
7578
7666
|
if (response.status === 404) {
|
|
7579
7667
|
if (await mintRefusedInTransit(config.url)) throw new Error(MINT_REFUSED_IN_TRANSIT);
|
|
@@ -7591,12 +7679,12 @@ var mintTicket = (config) => async (target, params) => {
|
|
|
7591
7679
|
throw new Error("upgrade ticket missing from the answer");
|
|
7592
7680
|
return { kind: "ticket", ticket: minted.ticket };
|
|
7593
7681
|
};
|
|
7594
|
-
var socketUrl = (config, target, params, extra) => upgradeUrl({
|
|
7682
|
+
var socketUrl = (config, target, params, extra, signal) => upgradeUrl({
|
|
7595
7683
|
serverUrl: parseMendUrl(config.url).toString(),
|
|
7596
7684
|
target,
|
|
7597
7685
|
params,
|
|
7598
7686
|
...extra === void 0 ? {} : { extra },
|
|
7599
|
-
mint: mintTicket(config),
|
|
7687
|
+
mint: mintTicket(config, signal),
|
|
7600
7688
|
legacyToken: config.token
|
|
7601
7689
|
});
|
|
7602
7690
|
var boundApi = (config) => (method, route, body) => api(config, method, route, body);
|
|
@@ -7607,7 +7695,10 @@ var withSpinner = async (label, work) => {
|
|
|
7607
7695
|
let frame = 0;
|
|
7608
7696
|
const timer = setInterval(() => {
|
|
7609
7697
|
const seconds2 = Math.round((Date.now() - started) / 1e3);
|
|
7610
|
-
|
|
7698
|
+
const text = typeof label === "string" ? label : label();
|
|
7699
|
+
process.stdout.write(
|
|
7700
|
+
`\r\x1B[2K ${frames[frame % frames.length]} ${text} ${dim6(`${seconds2}s`)} `
|
|
7701
|
+
);
|
|
7611
7702
|
frame += 1;
|
|
7612
7703
|
}, 120);
|
|
7613
7704
|
try {
|
|
@@ -7617,6 +7708,52 @@ var withSpinner = async (label, work) => {
|
|
|
7617
7708
|
process.stdout.write("\r\x1B[2K");
|
|
7618
7709
|
}
|
|
7619
7710
|
};
|
|
7711
|
+
var followStarting = async (config, sessionId, label, start) => {
|
|
7712
|
+
let line = label;
|
|
7713
|
+
const timers = /* @__PURE__ */ new Set();
|
|
7714
|
+
const sleep3 = (ms) => new Promise((resolve7) => {
|
|
7715
|
+
const timer = setTimeout(() => {
|
|
7716
|
+
timers.delete(timer);
|
|
7717
|
+
resolve7();
|
|
7718
|
+
}, ms);
|
|
7719
|
+
timers.add(timer);
|
|
7720
|
+
});
|
|
7721
|
+
try {
|
|
7722
|
+
return await withSpinner(
|
|
7723
|
+
() => line,
|
|
7724
|
+
followStart({
|
|
7725
|
+
start,
|
|
7726
|
+
read: () => request(config, "GET", `/sessions/${sessionId}`),
|
|
7727
|
+
onLine: (next) => {
|
|
7728
|
+
line = next === "starting" ? label : next;
|
|
7729
|
+
if (process.stdout.isTTY !== true) say6(dim6(` ${next}`));
|
|
7730
|
+
},
|
|
7731
|
+
sleep: sleep3,
|
|
7732
|
+
now: Date.now
|
|
7733
|
+
})
|
|
7734
|
+
);
|
|
7735
|
+
} finally {
|
|
7736
|
+
for (const timer of timers) clearTimeout(timer);
|
|
7737
|
+
}
|
|
7738
|
+
};
|
|
7739
|
+
var startedOrExit = (config, sessionId, outcome) => {
|
|
7740
|
+
const id8 = sessionId.slice(0, 8);
|
|
7741
|
+
switch (outcome.kind) {
|
|
7742
|
+
case "live":
|
|
7743
|
+
return outcome.session;
|
|
7744
|
+
case "refused":
|
|
7745
|
+
return fail6(outcome.message);
|
|
7746
|
+
case "settled":
|
|
7747
|
+
return fail6(
|
|
7748
|
+
`session ${id8} \xB7 ${startingLineOf(outcome.session)} before its agent ran \xB7 ${config.url}/sessions/${sessionId}`
|
|
7749
|
+
);
|
|
7750
|
+
case "unreachable":
|
|
7751
|
+
return fail6(
|
|
7752
|
+
`${outcome.message} \xB7 the session may still be starting \xB7 follow it: mend attach ${id8}`
|
|
7753
|
+
);
|
|
7754
|
+
}
|
|
7755
|
+
};
|
|
7756
|
+
var startAndFollow = async (config, sessionId, label, start) => startedOrExit(config, sessionId, await followStarting(config, sessionId, label, start));
|
|
7620
7757
|
var adopt = async (config, args) => {
|
|
7621
7758
|
const nameFlag = args.indexOf("--name");
|
|
7622
7759
|
const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
|
|
@@ -7818,9 +7955,11 @@ var launch = async (config, harness, args) => {
|
|
|
7818
7955
|
...parsed.ask ? { permissionMode: "ask" } : {},
|
|
7819
7956
|
...parsed.fast ? { speed: "fast" } : {}
|
|
7820
7957
|
} : { argv };
|
|
7821
|
-
await
|
|
7958
|
+
await startAndFollow(
|
|
7959
|
+
config,
|
|
7960
|
+
session.id,
|
|
7822
7961
|
"provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
|
|
7823
|
-
|
|
7962
|
+
request(config, "POST", `/sessions/${session.id}/launch`, launchBody)
|
|
7824
7963
|
);
|
|
7825
7964
|
if (lifecycle === "detach") {
|
|
7826
7965
|
say6(`${green6("\u2713 recording")} \xB7 running detached`);
|
|
@@ -7842,58 +7981,67 @@ var autoLandLine = (override, project2) => {
|
|
|
7842
7981
|
}
|
|
7843
7982
|
return `${green6("\u2713")} automatic landing \xB7 ${override ? "on" : "off"} for this session ${dim6("\xB7 from this terminal, land with mend land")}`;
|
|
7844
7983
|
};
|
|
7845
|
-
var
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7984
|
+
var ATTACH_CONNECT_TIMEOUT_MS = (() => {
|
|
7985
|
+
const configured = Number(process.env["MEND_ATTACH_TIMEOUT_MS"]);
|
|
7986
|
+
return Number.isFinite(configured) && configured > 0 ? configured : 3e4;
|
|
7987
|
+
})();
|
|
7988
|
+
var connectingLine = (sessionId, elapsedMs) => `\r\x1B[2K connecting to ${sessionId.slice(0, 8)} \xB7 ${Math.round(elapsedMs / 1e3)}s \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C cancels`;
|
|
7989
|
+
var takeTerminal = () => {
|
|
7990
|
+
if (process.stdin.isTTY !== true) {
|
|
7991
|
+
console.error(
|
|
7992
|
+
"mend: stdin is not a terminal, so it cannot be put in raw mode \u2014 keys reach the session a line at a time and echo here"
|
|
7853
7993
|
);
|
|
7854
|
-
|
|
7855
|
-
return "unavailable";
|
|
7994
|
+
return { raw: false };
|
|
7856
7995
|
}
|
|
7857
|
-
const ws = new WebSocket(url);
|
|
7858
|
-
ws.binaryType = "arraybuffer";
|
|
7859
7996
|
try {
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
return
|
|
7997
|
+
process.stdin.setRawMode(true);
|
|
7998
|
+
} catch (error) {
|
|
7999
|
+
console.error(
|
|
8000
|
+
`mend: could not put this terminal in raw mode (${error instanceof Error ? error.message : String(error)}) \u2014 keys reach the session a line at a time and echo here`
|
|
8001
|
+
);
|
|
8002
|
+
return { raw: false };
|
|
7866
8003
|
}
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
finishAttachment = resolve7;
|
|
7871
|
-
});
|
|
7872
|
-
const onClose = () => finishAttachment?.();
|
|
7873
|
-
if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
|
|
7874
|
-
else ws.addEventListener("close", onClose, { once: true });
|
|
7875
|
-
const sendResize = () => {
|
|
7876
|
-
if (ws.readyState !== WebSocket.OPEN) return;
|
|
7877
|
-
ws.send(
|
|
7878
|
-
JSON.stringify({
|
|
7879
|
-
t: "resize",
|
|
7880
|
-
cols: process.stdout.columns ?? 80,
|
|
7881
|
-
rows: process.stdout.rows ?? 24
|
|
7882
|
-
})
|
|
8004
|
+
if (!process.stdin.isRaw) {
|
|
8005
|
+
console.error(
|
|
8006
|
+
"mend: this terminal did not accept raw mode \u2014 keys reach the session a line at a time and echo here"
|
|
7883
8007
|
);
|
|
8008
|
+
return { raw: false };
|
|
8009
|
+
}
|
|
8010
|
+
return { raw: true };
|
|
8011
|
+
};
|
|
8012
|
+
var terminalSize = () => ({
|
|
8013
|
+
cols: process.stdout.columns || 80,
|
|
8014
|
+
rows: process.stdout.rows || 24
|
|
8015
|
+
});
|
|
8016
|
+
var attachTty = async (config, sessionId, harness, from, processId, options) => {
|
|
8017
|
+
const interactive = options?.readOnly !== true;
|
|
8018
|
+
const rawModeEnabled = interactive ? takeTerminal().raw : false;
|
|
8019
|
+
const connecting = new AbortController();
|
|
8020
|
+
let abortReason = null;
|
|
8021
|
+
const abort = (reason) => {
|
|
8022
|
+
abortReason ??= reason;
|
|
8023
|
+
connecting.abort();
|
|
7884
8024
|
};
|
|
7885
|
-
|
|
7886
|
-
const rawTty = process.stdin.isTTY === true;
|
|
7887
|
-
let rawModeEnabled = false;
|
|
8025
|
+
let ws = null;
|
|
7888
8026
|
let detached = false;
|
|
7889
8027
|
let sawEnd = false;
|
|
7890
8028
|
let interrupted = false;
|
|
8029
|
+
let finishAttachment;
|
|
8030
|
+
const finished = new Promise((resolve7) => {
|
|
8031
|
+
finishAttachment = resolve7;
|
|
8032
|
+
});
|
|
7891
8033
|
const onSignal = () => {
|
|
7892
8034
|
interrupted = true;
|
|
7893
|
-
|
|
8035
|
+
abort("interrupted");
|
|
8036
|
+
ws?.close();
|
|
7894
8037
|
finishAttachment?.();
|
|
7895
8038
|
};
|
|
7896
8039
|
const signals = ["SIGHUP", "SIGINT", "SIGTERM"];
|
|
8040
|
+
const sendResize = (size = terminalSize()) => {
|
|
8041
|
+
if (ws === null || ws.readyState !== WebSocket.OPEN) return;
|
|
8042
|
+
ws.send(JSON.stringify({ t: "resize", cols: size.cols, rows: size.rows }));
|
|
8043
|
+
};
|
|
8044
|
+
const onWinch = () => sendResize();
|
|
7897
8045
|
let bracketedPaste = false;
|
|
7898
8046
|
const onTtyFrame = (event) => {
|
|
7899
8047
|
if (typeof event.data !== "string") {
|
|
@@ -7908,15 +8056,24 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
7908
8056
|
return;
|
|
7909
8057
|
}
|
|
7910
8058
|
sawEnd = true;
|
|
7911
|
-
ws
|
|
8059
|
+
ws?.close();
|
|
7912
8060
|
finishAttachment?.();
|
|
7913
8061
|
} catch {
|
|
7914
8062
|
}
|
|
7915
8063
|
};
|
|
8064
|
+
const forward = (bytes) => {
|
|
8065
|
+
if (ws !== null && ws.readyState === WebSocket.OPEN) ws.send(new Uint8Array(bytes).buffer);
|
|
8066
|
+
};
|
|
7916
8067
|
const onKeys = (data) => {
|
|
8068
|
+
if (ws === null) {
|
|
8069
|
+
if (detachKeyEnabled && isDetachChunk(data)) abort("detached");
|
|
8070
|
+
else if (data.includes(3)) abort("interrupted");
|
|
8071
|
+
return;
|
|
8072
|
+
}
|
|
7917
8073
|
if (detachKeyEnabled && isDetachChunk(data)) {
|
|
7918
8074
|
detached = true;
|
|
7919
8075
|
ws.close();
|
|
8076
|
+
finishAttachment?.();
|
|
7920
8077
|
return;
|
|
7921
8078
|
}
|
|
7922
8079
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
@@ -7926,9 +8083,6 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
7926
8083
|
}
|
|
7927
8084
|
ws.send(new Uint8Array(data).buffer);
|
|
7928
8085
|
};
|
|
7929
|
-
const forward = (bytes) => {
|
|
7930
|
-
if (ws.readyState === WebSocket.OPEN) ws.send(new Uint8Array(bytes).buffer);
|
|
7931
|
-
};
|
|
7932
8086
|
const pasteClipboardImage = async (keystroke) => {
|
|
7933
8087
|
const image = await readClipboardImage();
|
|
7934
8088
|
if (image === null) {
|
|
@@ -7947,36 +8101,88 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
|
|
|
7947
8101
|
process.stdout.write("\x07");
|
|
7948
8102
|
}
|
|
7949
8103
|
};
|
|
8104
|
+
const connect2 = async () => {
|
|
8105
|
+
let url;
|
|
8106
|
+
try {
|
|
8107
|
+
url = await socketUrl(
|
|
8108
|
+
config,
|
|
8109
|
+
"tty",
|
|
8110
|
+
processId === void 0 ? { session: sessionId } : { process: processId },
|
|
8111
|
+
{ from: from.toString() },
|
|
8112
|
+
connecting.signal
|
|
8113
|
+
);
|
|
8114
|
+
} catch {
|
|
8115
|
+
return connecting.signal.aborted ? "aborted" : "unavailable";
|
|
8116
|
+
}
|
|
8117
|
+
if (connecting.signal.aborted) return "aborted";
|
|
8118
|
+
const socket = new WebSocket(url);
|
|
8119
|
+
socket.binaryType = "arraybuffer";
|
|
8120
|
+
const opened = await new Promise((resolve7) => {
|
|
8121
|
+
const onAbort = () => resolve7(false);
|
|
8122
|
+
connecting.signal.addEventListener("abort", onAbort, { once: true });
|
|
8123
|
+
const settle = (value) => {
|
|
8124
|
+
connecting.signal.removeEventListener("abort", onAbort);
|
|
8125
|
+
resolve7(value);
|
|
8126
|
+
};
|
|
8127
|
+
socket.addEventListener("open", () => settle(true), { once: true });
|
|
8128
|
+
socket.addEventListener("error", () => settle(false), { once: true });
|
|
8129
|
+
});
|
|
8130
|
+
if (opened && !connecting.signal.aborted) return socket;
|
|
8131
|
+
socket.close();
|
|
8132
|
+
return connecting.signal.aborted ? "aborted" : "unavailable";
|
|
8133
|
+
};
|
|
8134
|
+
const started = Date.now();
|
|
8135
|
+
let connectingShown = false;
|
|
8136
|
+
const connectingTimer = setInterval(() => {
|
|
8137
|
+
if (process.stdout.isTTY !== true) return;
|
|
8138
|
+
connectingShown = true;
|
|
8139
|
+
process.stdout.write(connectingLine(sessionId, Date.now() - started));
|
|
8140
|
+
}, 1e3);
|
|
8141
|
+
const deadline = setTimeout(() => abort("no-answer"), ATTACH_CONNECT_TIMEOUT_MS);
|
|
8142
|
+
const stopConnecting = () => {
|
|
8143
|
+
for (const timer of [connectingTimer, deadline]) clearTimeout(timer);
|
|
8144
|
+
if (connectingShown) process.stdout.write("\r\x1B[2K");
|
|
8145
|
+
connectingShown = false;
|
|
8146
|
+
};
|
|
8147
|
+
let stopHerdrHint = noHerdrHint;
|
|
7950
8148
|
try {
|
|
7951
|
-
sendResize();
|
|
7952
|
-
process.stdout.on("resize", onWinch);
|
|
7953
8149
|
if (options?.handleSignals === true) for (const signal of signals) process.on(signal, onSignal);
|
|
7954
|
-
if (
|
|
7955
|
-
if (rawTty) {
|
|
7956
|
-
process.stdin.setRawMode(true);
|
|
7957
|
-
rawModeEnabled = true;
|
|
7958
|
-
}
|
|
7959
|
-
process.stdin.resume();
|
|
8150
|
+
if (interactive) {
|
|
7960
8151
|
process.stdin.on("data", onKeys);
|
|
8152
|
+
process.stdin.resume();
|
|
7961
8153
|
}
|
|
8154
|
+
const connected = await connect2();
|
|
8155
|
+
stopConnecting();
|
|
8156
|
+
if (connected === "unavailable") return "unavailable";
|
|
8157
|
+
if (connected === "aborted") return abortReason ?? "interrupted";
|
|
8158
|
+
ws = connected;
|
|
8159
|
+
stopHerdrHint = hintHerdrAttachment(harness);
|
|
8160
|
+
const onClose = () => finishAttachment?.();
|
|
8161
|
+
if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
|
|
8162
|
+
else ws.addEventListener("close", onClose, { once: true });
|
|
7962
8163
|
ws.addEventListener("message", onTtyFrame);
|
|
8164
|
+
const size = terminalSize();
|
|
8165
|
+
if (size.rows > 1) sendResize({ cols: size.cols, rows: size.rows - 1 });
|
|
8166
|
+
setTimeout(() => sendResize(), 80);
|
|
8167
|
+
process.stdout.on("resize", onWinch);
|
|
7963
8168
|
await finished;
|
|
7964
8169
|
return detached ? "detached" : interrupted ? "interrupted" : sawEnd ? "ended" : "dropped";
|
|
7965
8170
|
} finally {
|
|
8171
|
+
stopConnecting();
|
|
7966
8172
|
for (const signal of signals) process.off(signal, onSignal);
|
|
7967
8173
|
process.stdin.off("data", onKeys);
|
|
7968
8174
|
process.stdout.off("resize", onWinch);
|
|
7969
|
-
ws
|
|
7970
|
-
ws.removeEventListener("close", onClose);
|
|
8175
|
+
ws?.removeEventListener("message", onTtyFrame);
|
|
7971
8176
|
if (rawModeEnabled) process.stdin.setRawMode(false);
|
|
7972
|
-
if (
|
|
8177
|
+
if (interactive && ws !== null && process.stdout.isTTY === true) {
|
|
7973
8178
|
process.stdout.write(
|
|
7974
8179
|
"\x1B[<u\x1B[=0;1u\x1B[?2004l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1049l\x1B[?25h"
|
|
7975
8180
|
// show the cursor
|
|
7976
8181
|
);
|
|
7977
8182
|
}
|
|
7978
8183
|
process.stdin.pause();
|
|
7979
|
-
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)
|
|
8184
|
+
if (ws !== null && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING))
|
|
8185
|
+
ws.close();
|
|
7980
8186
|
stopHerdrHint();
|
|
7981
8187
|
}
|
|
7982
8188
|
};
|
|
@@ -8004,11 +8210,18 @@ var stopAndExit = async (config, sessionId) => {
|
|
|
8004
8210
|
say6(`${amber2("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
|
|
8005
8211
|
process.exit(1);
|
|
8006
8212
|
};
|
|
8213
|
+
var noAnswerLine = (config, id8) => `${amber2("no answer")} \u2014 ${config.url} did not open the terminal within ${spoken(ATTACH_CONNECT_TIMEOUT_MS)}; the session keeps running \xB7 try again: mend attach ${id8} \xB7 check the server: mend doctor`;
|
|
8007
8214
|
var finishAttach = async (config, sessionId, outcome, mode = "background") => {
|
|
8008
8215
|
const id8 = sessionId.slice(0, 8);
|
|
8009
8216
|
if (outcome === "unavailable") {
|
|
8010
8217
|
return fail6(`tty attach unavailable: could not connect to ${config.url}`);
|
|
8011
8218
|
}
|
|
8219
|
+
if (outcome === "no-answer") {
|
|
8220
|
+
say6("");
|
|
8221
|
+
say6(noAnswerLine(config, id8));
|
|
8222
|
+
if (mode === "foreground") return stopAndExit(config, sessionId);
|
|
8223
|
+
process.exit(1);
|
|
8224
|
+
}
|
|
8012
8225
|
if (outcome === "detached") {
|
|
8013
8226
|
say6("");
|
|
8014
8227
|
say6(`${amber2("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
|
|
@@ -8070,6 +8283,9 @@ var attachPicked = async (config, session, tunnels) => {
|
|
|
8070
8283
|
say6(
|
|
8071
8284
|
`${green6("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim6(session.id.slice(0, 8))}${detachHint()}`
|
|
8072
8285
|
);
|
|
8286
|
+
if (session.status === "starting") {
|
|
8287
|
+
await startAndFollow(config, session.id, "starting \u2014 attaching once the agent runs\u2026", null);
|
|
8288
|
+
}
|
|
8073
8289
|
await tunnels?.start(session.id);
|
|
8074
8290
|
say6("");
|
|
8075
8291
|
const outcome = await attachWithTunnels(
|
|
@@ -8082,9 +8298,11 @@ var attachPicked = async (config, session, tunnels) => {
|
|
|
8082
8298
|
const detail = await api(config, "GET", `/sessions/${session.id}`);
|
|
8083
8299
|
if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
|
|
8084
8300
|
say6(`${amber2("taking over")} from the protocol session`);
|
|
8085
|
-
await
|
|
8301
|
+
await startAndFollow(
|
|
8302
|
+
config,
|
|
8303
|
+
session.id,
|
|
8086
8304
|
"reopening as a terminal \u2014 same conversation\u2026",
|
|
8087
|
-
|
|
8305
|
+
request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
|
|
8088
8306
|
);
|
|
8089
8307
|
say6("");
|
|
8090
8308
|
await attachOrExit(config, session.id, session.harness, "background", tunnels);
|
|
@@ -8240,6 +8458,10 @@ var shellCommand = async (config, args) => {
|
|
|
8240
8458
|
return fail6(`tty attach unavailable: could not connect to ${config.url}`);
|
|
8241
8459
|
}
|
|
8242
8460
|
say6("");
|
|
8461
|
+
if (outcome === "no-answer") {
|
|
8462
|
+
say6(noAnswerLine(config, session.id.slice(0, 8)));
|
|
8463
|
+
process.exit(1);
|
|
8464
|
+
}
|
|
8243
8465
|
if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
|
|
8244
8466
|
say6(`${amber2("detached")} \u2014 the shell keeps running and holds the workspace open`);
|
|
8245
8467
|
process.exit(0);
|
|
@@ -9627,9 +9849,11 @@ var completionsCommand = (args) => {
|
|
|
9627
9849
|
}
|
|
9628
9850
|
};
|
|
9629
9851
|
var supervisedRun = async (config, session, argv) => {
|
|
9630
|
-
const launched = await
|
|
9852
|
+
const launched = await startAndFollow(
|
|
9853
|
+
config,
|
|
9854
|
+
session.id,
|
|
9631
9855
|
"provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
|
|
9632
|
-
|
|
9856
|
+
request(config, "POST", `/sessions/${session.id}/launch`, { argv })
|
|
9633
9857
|
);
|
|
9634
9858
|
say6(
|
|
9635
9859
|
`${green6("\u2713 recording")} \xB7 run ${dim6(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
|
|
@@ -9765,9 +9989,13 @@ var resumeCommand = async (config, args) => {
|
|
|
9765
9989
|
say6(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
|
|
9766
9990
|
const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
|
|
9767
9991
|
const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
|
|
9768
|
-
await
|
|
9992
|
+
await startAndFollow(
|
|
9993
|
+
config,
|
|
9994
|
+
match.id,
|
|
9769
9995
|
protocolPrior ? "reopening as a terminal \u2014 same conversation\u2026" : "resuming \u2014 a fresh workspace restores the saved session state\u2026",
|
|
9770
|
-
protocolPrior ?
|
|
9996
|
+
protocolPrior ? request(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : request(config, "POST", `/sessions/${match.id}/resume`, {
|
|
9997
|
+
harness: withHarness
|
|
9998
|
+
})
|
|
9771
9999
|
);
|
|
9772
10000
|
say6(`${green6("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
|
|
9773
10001
|
say6("");
|
|
@@ -9775,17 +10003,18 @@ var resumeCommand = async (config, args) => {
|
|
|
9775
10003
|
exitAfterSessionEnd(config, match.id);
|
|
9776
10004
|
};
|
|
9777
10005
|
var resumeForRejoin = async (config, sessionId, label) => {
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
)
|
|
9783
|
-
|
|
9784
|
-
|
|
10006
|
+
const outcome = await followStarting(
|
|
10007
|
+
config,
|
|
10008
|
+
sessionId,
|
|
10009
|
+
label,
|
|
10010
|
+
request(config, "POST", `/sessions/${sessionId}/resume`, { harness: null })
|
|
10011
|
+
);
|
|
10012
|
+
if (outcome.kind === "refused") {
|
|
9785
10013
|
const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
|
|
9786
10014
|
if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
|
|
9787
|
-
return fail6(error instanceof Error ? error.message : String(error));
|
|
9788
10015
|
}
|
|
10016
|
+
startedOrExit(config, sessionId, outcome);
|
|
10017
|
+
return true;
|
|
9789
10018
|
};
|
|
9790
10019
|
var rejoinCommand = async (config, args) => {
|
|
9791
10020
|
const harnessFlag = args.indexOf("--harness");
|
|
@@ -9816,9 +10045,11 @@ var rejoinCommand = async (config, args) => {
|
|
|
9816
10045
|
let restored = false;
|
|
9817
10046
|
const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
|
|
9818
10047
|
if (currentAgent?.kind === "agent-protocol") {
|
|
9819
|
-
await
|
|
10048
|
+
await startAndFollow(
|
|
10049
|
+
config,
|
|
10050
|
+
session.id,
|
|
9820
10051
|
alreadyLive ? "taking over from the protocol session \u2014 same conversation\u2026" : "reopening as a terminal \u2014 same conversation\u2026",
|
|
9821
|
-
|
|
10052
|
+
request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
|
|
9822
10053
|
);
|
|
9823
10054
|
restored = true;
|
|
9824
10055
|
} else if (!alreadyLive) {
|
|
@@ -10149,7 +10380,7 @@ var dashboard = async (config, options = {}) => {
|
|
|
10149
10380
|
);
|
|
10150
10381
|
process.exit(rerun.status ?? 0);
|
|
10151
10382
|
}
|
|
10152
|
-
const { runDashboard } = await import("./dashboard-
|
|
10383
|
+
const { runDashboard } = await import("./dashboard-DWXXYFPE.js");
|
|
10153
10384
|
const agentShare = await startShareIfBridge(config);
|
|
10154
10385
|
const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
|
|
10155
10386
|
listServices: () => fetchServices(config),
|
package/man/mend-accounts.1
CHANGED
package/man/mend-adopt.1
CHANGED
package/man/mend-attach.1
CHANGED