@sealant/mend 0.14.0 → 0.15.2
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/dashboard-model.js +297 -0
- package/dist/dashboard.js +538 -181
- package/dist/main.js +166 -8
- package/dist/shared.js +32 -1
- package/dist/tui-theme.js +5 -0
- package/dist/workbench-events.js +1 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -11,7 +11,7 @@ import { formatLoadReport } from "./env.js";
|
|
|
11
11
|
import { loginCommand } from "./login.js";
|
|
12
12
|
import { pairCommand, qrCommand } from "./pair.js";
|
|
13
13
|
import { isComposeFile, proposeFromCompose, proposeFromPackageJson, proposeFromWorkspacePackage, renderMendToml, workspaceGlobs, } from "./service-init.js";
|
|
14
|
-
import { agentIsLive, agentOutcome, cwdFacts, gitTopLevel, HARNESS_COMMANDS, isDetachChunk, LIVE_STATUSES, matchProjectByCwd, normalizeProjectName, parseLaunchArgs, } from "./shared.js";
|
|
14
|
+
import { agentIsLive, agentOutcome, cwdFacts, gitTopLevel, HARNESS_COMMANDS, isDetachChunk, LIVE_STATUSES, sessionDisplayName, matchProjectByCwd, normalizeProjectName, gitCurrentBranch, parseLaunchArgs, } from "./shared.js";
|
|
15
15
|
import { sshCommand } from "./ssh-setup.js";
|
|
16
16
|
// `$XDG_CONFIG_HOME/mend`, default `~/.config/mend`; a pre-XDG `~/.mend` stays authoritative
|
|
17
17
|
// when it is the only one present (mirrors @mend/store's resolver — the CLI stays dependency-light).
|
|
@@ -284,7 +284,37 @@ const launch = async (config, harness, args) => {
|
|
|
284
284
|
say(`${green("✓")} project ${project.name} ${dim(`· ${project.defaultBranch}${parsed.project === null ? " · from cwd" : ""}`)}`);
|
|
285
285
|
// The worktree's name comes first, then the session details — it is the
|
|
286
286
|
// identity every list leads with. `mend run` stays scriptable: flag only.
|
|
287
|
-
|
|
287
|
+
// `--worktree` insists on joining: the name must already exist here.
|
|
288
|
+
let worktreeName;
|
|
289
|
+
if (parsed.worktree !== null) {
|
|
290
|
+
const detail = await api(config, "GET", `/projects/${project.id}`);
|
|
291
|
+
const existing = detail.worktrees ?? [];
|
|
292
|
+
if (detail.worktrees === undefined) {
|
|
293
|
+
return fail("this server predates shared worktrees — use --name instead");
|
|
294
|
+
}
|
|
295
|
+
const match = existing.find((worktree) => worktree.name === parsed.worktree) ??
|
|
296
|
+
(existing.filter((worktree) => worktree.name.startsWith(parsed.worktree ?? "")).length === 1
|
|
297
|
+
? existing.find((worktree) => worktree.name.startsWith(parsed.worktree ?? ""))
|
|
298
|
+
: undefined);
|
|
299
|
+
if (match === undefined) {
|
|
300
|
+
const names = existing.map((worktree) => worktree.name).join(", ");
|
|
301
|
+
return fail(`no worktree matches "${parsed.worktree}" in ${project.name}${names === "" ? "" : ` — have: ${names}`}`);
|
|
302
|
+
}
|
|
303
|
+
worktreeName = match.name;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
worktreeName = harness === "run" ? parsed.name : (parsed.name ?? (await askWorktreeName()));
|
|
307
|
+
}
|
|
308
|
+
// Say when the name joins an existing worktree — a join is a fact worth
|
|
309
|
+
// stating before the session exists, not a surprise in the tree later.
|
|
310
|
+
if (worktreeName !== null) {
|
|
311
|
+
const detail = await api(config, "GET", `/projects/${project.id}`).catch(() => null);
|
|
312
|
+
const joined = detail?.worktrees?.find((worktree) => worktree.name === worktreeName);
|
|
313
|
+
if (joined !== undefined) {
|
|
314
|
+
const members = (detail?.sessions ?? []).filter((candidate) => candidate.worktreeId === joined.id).length;
|
|
315
|
+
say(`${green("✓")} joins worktree ${joined.name} ${dim(`· ${members} session${members === 1 ? "" : "s"} · branch ${joined.branch}`)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
288
318
|
const lifecycle = harness === "run"
|
|
289
319
|
? "background"
|
|
290
320
|
: parsed.detach
|
|
@@ -593,7 +623,7 @@ const attach = async (config, args) => {
|
|
|
593
623
|
const match = sessions.find((s) => s.id.startsWith(prefix));
|
|
594
624
|
if (match === undefined)
|
|
595
625
|
return fail(`no active session matches "${prefix}"`);
|
|
596
|
-
say(`${green("✓")} attaching to ${match
|
|
626
|
+
say(`${green("✓")} attaching to ${sessionDisplayName(match)} · ${match.harness} ${dim(match.id.slice(0, 8))}${detachHint()}`);
|
|
597
627
|
say("");
|
|
598
628
|
await attachOrExit(config, match.id, match.harness);
|
|
599
629
|
exitAfterSessionEnd(config, match.id);
|
|
@@ -2067,7 +2097,7 @@ const rejoinCommand = async (config, args) => {
|
|
|
2067
2097
|
if (session === undefined)
|
|
2068
2098
|
return fail("session selection failed");
|
|
2069
2099
|
const alreadyLive = agentLiveIn(detail, session);
|
|
2070
|
-
say(`${green("✓")} rejoining ${session
|
|
2100
|
+
say(`${green("✓")} rejoining ${sessionDisplayName(session)} · ${session.harness} ${dim(session.id.slice(0, 8))} · ${alreadyLive ? "already live" : "restoring"}`);
|
|
2071
2101
|
say(`${cobalt(" watch")} · ${config.url}/sessions/${session.id}`);
|
|
2072
2102
|
let restored = false;
|
|
2073
2103
|
const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ??
|
|
@@ -2168,6 +2198,15 @@ const printSessionRow = (row) => {
|
|
|
2168
2198
|
*/
|
|
2169
2199
|
const sessionsCommand = async (config, args) => {
|
|
2170
2200
|
const all = args.includes("--all");
|
|
2201
|
+
// v1 stays byte-stable for pinned integrations; v2 is the worktree envelope.
|
|
2202
|
+
if (args.includes("--json=v2")) {
|
|
2203
|
+
const projectFlag = args.indexOf("--project");
|
|
2204
|
+
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== undefined
|
|
2205
|
+
? String(args[projectFlag + 1])
|
|
2206
|
+
: null;
|
|
2207
|
+
say(JSON.stringify(await buildWorktreesJson(config, projectName), null, 2));
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2171
2210
|
const json = args.includes("--json");
|
|
2172
2211
|
const projectFlag = args.indexOf("--project");
|
|
2173
2212
|
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== undefined
|
|
@@ -2243,6 +2282,117 @@ const sessionsCommand = async (config, args) => {
|
|
|
2243
2282
|
for (const row of rows)
|
|
2244
2283
|
printSessionRow(row);
|
|
2245
2284
|
};
|
|
2285
|
+
/**
|
|
2286
|
+
* The worktree-grouped view both `mend worktrees --json` and
|
|
2287
|
+
* `mend sessions --json=v2` emit. Against a pre-worktree server every session
|
|
2288
|
+
* becomes its own pseudo worktree (`id: null`) — the envelope shape is stable
|
|
2289
|
+
* either way, so integrations pin on `version`, not server age.
|
|
2290
|
+
*/
|
|
2291
|
+
const toWorktreeJsonSession = (session) => ({
|
|
2292
|
+
id: session.id,
|
|
2293
|
+
harness: session.harness,
|
|
2294
|
+
label: session.label,
|
|
2295
|
+
status: session.status,
|
|
2296
|
+
summary: session.summary,
|
|
2297
|
+
createdAt: session.createdAt,
|
|
2298
|
+
});
|
|
2299
|
+
const buildWorktreesJson = async (config, projectName) => {
|
|
2300
|
+
const projects = await api(config, "GET", "/projects");
|
|
2301
|
+
const scope = projectName === null ? projects : projects.filter((p) => p.name === projectName);
|
|
2302
|
+
const details = await Promise.all(scope.map((p) => api(config, "GET", `/projects/${p.id}`)));
|
|
2303
|
+
const worktrees = [];
|
|
2304
|
+
for (const detail of details) {
|
|
2305
|
+
const reviewOf = (sessionIds) => {
|
|
2306
|
+
const facts = detail.annotations.find((a) => sessionIds.has(a.sessionId));
|
|
2307
|
+
return facts === undefined
|
|
2308
|
+
? null
|
|
2309
|
+
: {
|
|
2310
|
+
openComments: facts.openComments,
|
|
2311
|
+
totalComments: facts.totalComments,
|
|
2312
|
+
pendingFollowUp: detail.annotations.some((a) => sessionIds.has(a.sessionId) && a.pendingFollowUp),
|
|
2313
|
+
};
|
|
2314
|
+
};
|
|
2315
|
+
const urlOf = (sessionIds) => {
|
|
2316
|
+
const first = [...sessionIds][0];
|
|
2317
|
+
return first === undefined ? null : `${config.url.replace(/\/$/, "")}/sessions/${first}`;
|
|
2318
|
+
};
|
|
2319
|
+
if (detail.worktrees !== undefined) {
|
|
2320
|
+
for (const worktree of detail.worktrees) {
|
|
2321
|
+
const members = detail.sessions.filter((session) => session.worktreeId === worktree.id);
|
|
2322
|
+
const ids = new Set(members.map((session) => session.id));
|
|
2323
|
+
worktrees.push({
|
|
2324
|
+
id: worktree.id,
|
|
2325
|
+
name: worktree.name,
|
|
2326
|
+
projectId: detail.project.id,
|
|
2327
|
+
projectName: detail.project.name,
|
|
2328
|
+
branch: worktree.branch,
|
|
2329
|
+
baseSha: worktree.baseSha,
|
|
2330
|
+
baseRef: worktree.baseRef,
|
|
2331
|
+
createdAt: worktree.createdAt,
|
|
2332
|
+
reviewUrl: urlOf(ids),
|
|
2333
|
+
review: reviewOf(ids),
|
|
2334
|
+
sessions: members
|
|
2335
|
+
.toSorted((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
2336
|
+
.map(toWorktreeJsonSession),
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
else {
|
|
2341
|
+
for (const session of detail.sessions) {
|
|
2342
|
+
const ids = new Set([session.id]);
|
|
2343
|
+
worktrees.push({
|
|
2344
|
+
id: null,
|
|
2345
|
+
name: session.worktree,
|
|
2346
|
+
projectId: detail.project.id,
|
|
2347
|
+
projectName: detail.project.name,
|
|
2348
|
+
branch: session.branch,
|
|
2349
|
+
baseSha: session.baseSha,
|
|
2350
|
+
baseRef: session.baseRef,
|
|
2351
|
+
createdAt: session.createdAt,
|
|
2352
|
+
reviewUrl: urlOf(ids),
|
|
2353
|
+
review: reviewOf(ids),
|
|
2354
|
+
sessions: [toWorktreeJsonSession(session)],
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return { version: 2, worktrees };
|
|
2360
|
+
};
|
|
2361
|
+
/** `mend worktrees [--project <p>] [--json]` — the container-first listing. */
|
|
2362
|
+
const worktreesCommand = async (config, args) => {
|
|
2363
|
+
const json = args.includes("--json");
|
|
2364
|
+
const projectFlag = args.indexOf("--project");
|
|
2365
|
+
const projectName = projectFlag !== -1 && args[projectFlag + 1] !== undefined
|
|
2366
|
+
? String(args[projectFlag + 1])
|
|
2367
|
+
: null;
|
|
2368
|
+
const payload = await buildWorktreesJson(config, projectName);
|
|
2369
|
+
if (json) {
|
|
2370
|
+
say(JSON.stringify(payload, null, 2));
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
if (payload.worktrees.length === 0) {
|
|
2374
|
+
say(dim("no worktrees yet — mend claude --name <worktree> starts one"));
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
for (const worktree of payload.worktrees) {
|
|
2378
|
+
const live = worktree.sessions.filter((session) => ACTIVE_STATUSES.has(session.status)).length;
|
|
2379
|
+
const facts = [
|
|
2380
|
+
`${worktree.sessions.length} session${worktree.sessions.length === 1 ? "" : "s"}`,
|
|
2381
|
+
];
|
|
2382
|
+
if (live > 0)
|
|
2383
|
+
facts.push(green(`${live} live`));
|
|
2384
|
+
if (worktree.review !== null && worktree.review.openComments > 0) {
|
|
2385
|
+
facts.push(amber(`${worktree.review.openComments} open`));
|
|
2386
|
+
}
|
|
2387
|
+
const base = worktree.baseRef ?? worktree.baseSha.slice(0, 12);
|
|
2388
|
+
say(`${worktree.name.padEnd(24)} ${worktree.projectName} ${dim(`${worktree.branch} · base ${base}`)} ${facts.join(dim(" · "))}`);
|
|
2389
|
+
for (const session of worktree.sessions) {
|
|
2390
|
+
const status = session.status.padEnd(9);
|
|
2391
|
+
const name = session.label ?? `session ${session.id.slice(0, 8)}`;
|
|
2392
|
+
say(` └ ${session.harness.padEnd(8)} ${dim(session.id.slice(0, 8))} ${ACTIVE_STATUSES.has(session.status) ? green(status) : dim(status)} ${name}`);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2246
2396
|
// ─── dashboard: the live TUI on bare `mend` ─────────────────────────────────
|
|
2247
2397
|
const hasNodeFfi = () => {
|
|
2248
2398
|
try {
|
|
@@ -2276,6 +2426,7 @@ const dashboard = async (config) => {
|
|
|
2276
2426
|
await runDashboard({
|
|
2277
2427
|
config,
|
|
2278
2428
|
cwd: process.cwd(),
|
|
2429
|
+
cwdBranch: gitCurrentBranch(process.cwd()),
|
|
2279
2430
|
api: (method, route, body) => request(config, method, route, body),
|
|
2280
2431
|
attachTty: (sessionId, harness, processId) => attachTty(config, sessionId, harness, 0n, processId),
|
|
2281
2432
|
});
|
|
@@ -2295,11 +2446,13 @@ start
|
|
|
2295
2446
|
mend adopt [source] [--name <name>] [--auth ambient|mend-key|bridge]
|
|
2296
2447
|
adopt a repository into the store (default: cwd; any git
|
|
2297
2448
|
URL — GitHub, GitLab, self-hosted, ssh://, a local path)
|
|
2298
|
-
mend codex|claude|opencode ["prompt"] [--name <worktree>] [--
|
|
2299
|
-
[--effort low|medium|high|xhigh|max]
|
|
2449
|
+
mend codex|claude|opencode ["prompt"] [--name <worktree>] [--worktree <existing>]
|
|
2450
|
+
[--model <id>] [--effort low|medium|high|xhigh|max]
|
|
2300
2451
|
[--base <ref>] [--ask] [--fast] [--detach|-d] [--foreground]
|
|
2301
|
-
|
|
2302
|
-
|
|
2452
|
+
launch the harness in a worktree; the worktree's name is
|
|
2453
|
+
asked first (--name skips the ask — an EXISTING name joins
|
|
2454
|
+
that worktree as a new session; --worktree joins only,
|
|
2455
|
+
failing if absent), a
|
|
2303
2456
|
quoted prompt becomes its first message,
|
|
2304
2457
|
--ask restores the harness's permission prompts, --fast
|
|
2305
2458
|
requests priority processing (codex service tier),
|
|
@@ -2325,6 +2478,9 @@ everything else
|
|
|
2325
2478
|
mend keys share relay THIS machine's ssh-agent to the server (bridge mode:
|
|
2326
2479
|
hardware keys sign here; Ctrl-C stops sharing)
|
|
2327
2480
|
mend run -- <command...> same, with an arbitrary command
|
|
2481
|
+
mend worktrees [--project <p>] [--json]
|
|
2482
|
+
every worktree and the sessions inside it (mend sessions
|
|
2483
|
+
--json stays the v1 flat list; --json=v2 groups like this)
|
|
2328
2484
|
mend attach <session-id-prefix> reattach this terminal to a running session
|
|
2329
2485
|
mend stop <session-id-prefix> | --all [--project <p>]
|
|
2330
2486
|
stop the agent — the workspace harvests and closes; the
|
|
@@ -2421,6 +2577,8 @@ const main = async () => {
|
|
|
2421
2577
|
case "sessions":
|
|
2422
2578
|
case "status":
|
|
2423
2579
|
return sessionsCommand(config, rest);
|
|
2580
|
+
case "worktrees":
|
|
2581
|
+
return worktreesCommand(config, rest);
|
|
2424
2582
|
case undefined:
|
|
2425
2583
|
case "ui":
|
|
2426
2584
|
return dashboard(config);
|
package/dist/shared.js
CHANGED
|
@@ -35,6 +35,7 @@ const LAUNCH_ERROR = {
|
|
|
35
35
|
effort: null,
|
|
36
36
|
base: null,
|
|
37
37
|
name: null,
|
|
38
|
+
worktree: null,
|
|
38
39
|
ask: false,
|
|
39
40
|
fast: false,
|
|
40
41
|
detach: false,
|
|
@@ -58,6 +59,7 @@ export const parseLaunchArgs = (args) => {
|
|
|
58
59
|
let effort = null;
|
|
59
60
|
let base = null;
|
|
60
61
|
let workName = null;
|
|
62
|
+
let joinWorktree = null;
|
|
61
63
|
let ask = false;
|
|
62
64
|
let fast = false;
|
|
63
65
|
let detach = false;
|
|
@@ -84,7 +86,8 @@ export const parseLaunchArgs = (args) => {
|
|
|
84
86
|
arg === "--model" ||
|
|
85
87
|
arg === "--effort" ||
|
|
86
88
|
arg === "--base" ||
|
|
87
|
-
arg === "--name"
|
|
89
|
+
arg === "--name" ||
|
|
90
|
+
arg === "--worktree") {
|
|
88
91
|
const value = flagArgs[index + 1];
|
|
89
92
|
if (value === undefined)
|
|
90
93
|
return { ...LAUNCH_ERROR, error: `${arg} needs a value` };
|
|
@@ -96,6 +99,8 @@ export const parseLaunchArgs = (args) => {
|
|
|
96
99
|
effort = value;
|
|
97
100
|
else if (arg === "--name")
|
|
98
101
|
workName = normalizeProjectName(value);
|
|
102
|
+
else if (arg === "--worktree")
|
|
103
|
+
joinWorktree = value;
|
|
99
104
|
else
|
|
100
105
|
base = value;
|
|
101
106
|
index += 1;
|
|
@@ -123,6 +128,12 @@ export const parseLaunchArgs = (args) => {
|
|
|
123
128
|
if (detach && foreground) {
|
|
124
129
|
return { ...LAUNCH_ERROR, error: "--detach and --foreground contradict — pick one" };
|
|
125
130
|
}
|
|
131
|
+
if (workName !== null && joinWorktree !== null) {
|
|
132
|
+
return {
|
|
133
|
+
...LAUNCH_ERROR,
|
|
134
|
+
error: "--name and --worktree contradict — --name names (or joins), --worktree only joins",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
126
137
|
return {
|
|
127
138
|
project,
|
|
128
139
|
prompt,
|
|
@@ -130,6 +141,7 @@ export const parseLaunchArgs = (args) => {
|
|
|
130
141
|
effort,
|
|
131
142
|
base,
|
|
132
143
|
name: workName,
|
|
144
|
+
worktree: joinWorktree,
|
|
133
145
|
ask,
|
|
134
146
|
fast,
|
|
135
147
|
detach,
|
|
@@ -161,6 +173,19 @@ export const isDetachChunk = (data) => {
|
|
|
161
173
|
}
|
|
162
174
|
return false;
|
|
163
175
|
};
|
|
176
|
+
/**
|
|
177
|
+
* What to call a session's worktree in banners and rows: a NAMED worktree is
|
|
178
|
+
* its branch minus the `mend/` prefix; an unnamed one (`mend/session/<uuid>`)
|
|
179
|
+
* is called by its auto-name label, or its short id before one lands.
|
|
180
|
+
*/
|
|
181
|
+
export const sessionDisplayName = (session) => {
|
|
182
|
+
if (session.branch.startsWith("mend/") && !session.branch.startsWith("mend/session/")) {
|
|
183
|
+
return session.branch.slice("mend/".length);
|
|
184
|
+
}
|
|
185
|
+
if (!session.branch.startsWith("mend/session/"))
|
|
186
|
+
return session.branch;
|
|
187
|
+
return session.label ?? `session ${session.id.slice(0, 8)}`;
|
|
188
|
+
};
|
|
164
189
|
export const LIVE_STATUSES = new Set([
|
|
165
190
|
"starting",
|
|
166
191
|
"running",
|
|
@@ -238,6 +263,12 @@ export const cwdFacts = (cwd) => ({
|
|
|
238
263
|
repoRoot: gitTopLevel(cwd),
|
|
239
264
|
originUrl: gitOriginUrl(cwd),
|
|
240
265
|
});
|
|
266
|
+
/** The branch checked out at `cwd`, or null outside a repo or on a detached HEAD. */
|
|
267
|
+
export const gitCurrentBranch = (cwd) => {
|
|
268
|
+
const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, encoding: "utf8" });
|
|
269
|
+
const out = result.status === 0 ? result.stdout.trim() : "";
|
|
270
|
+
return out === "" || out === "HEAD" ? null : out;
|
|
271
|
+
};
|
|
241
272
|
/** The cwd repo's origin remote, or null when there is no repo or no origin. */
|
|
242
273
|
export const gitOriginUrl = (cwd) => {
|
|
243
274
|
const result = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
package/dist/tui-theme.js
CHANGED
|
@@ -23,6 +23,11 @@ export const RULE = "#37374a";
|
|
|
23
23
|
export const COBALT = "#6f96f5";
|
|
24
24
|
/** The selection wash: a neutral lifted row — visible, not colorful. */
|
|
25
25
|
export const WASH = "#2c2c35";
|
|
26
|
+
/**
|
|
27
|
+
* The one exception to the no-painted-background rule: a floating modal
|
|
28
|
+
* must occlude the content beneath it, so it owns its ground.
|
|
29
|
+
*/
|
|
30
|
+
export const SURFACE = "#17171d";
|
|
26
31
|
export const AMBER = "#e2ab42";
|
|
27
32
|
export const GREEN = "#69c189";
|
|
28
33
|
export const RED = "#e87f76";
|
package/dist/workbench-events.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/mend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "The mend CLI: adopt a repository into Mend's store, launch your coding agent in a recorded session worktree, detach and reattach from any terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|