@sealant/mend 0.30.0 → 0.31.0

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.
Files changed (79) hide show
  1. package/dist/{chunk-BMFB2UN7.js → chunk-J3TR4UAG.js} +81 -7
  2. package/dist/{dashboard-OF5UXII2.js → dashboard-3HNFSQZD.js} +60 -7
  3. package/dist/main.js +157 -11
  4. package/man/mend-accounts.1 +1 -1
  5. package/man/mend-adopt.1 +1 -1
  6. package/man/mend-attach.1 +1 -1
  7. package/man/mend-codex.1 +1 -1
  8. package/man/mend-completions.1 +1 -1
  9. package/man/mend-connect.1 +1 -1
  10. package/man/mend-continue.1 +1 -1
  11. package/man/mend-doctor.1 +1 -1
  12. package/man/mend-dotfiles-repo.1 +1 -1
  13. package/man/mend-dotfiles-sync.1 +1 -1
  14. package/man/mend-dotfiles.1 +1 -1
  15. package/man/mend-env-cluster.1 +1 -1
  16. package/man/mend-env-load.1 +1 -1
  17. package/man/mend-env-show.1 +1 -1
  18. package/man/mend-folder-create.1 +1 -1
  19. package/man/mend-folder-list.1 +1 -1
  20. package/man/mend-folder-push.1 +1 -1
  21. package/man/mend-folder-rm.1 +1 -1
  22. package/man/mend-git-author.1 +24 -0
  23. package/man/mend-help.1 +1 -1
  24. package/man/mend-invite.1 +1 -1
  25. package/man/mend-keys-init.1 +1 -1
  26. package/man/mend-keys-mode.1 +1 -1
  27. package/man/mend-keys-share.1 +1 -1
  28. package/man/mend-keys-show.1 +1 -1
  29. package/man/mend-land.1 +9 -2
  30. package/man/mend-login.1 +1 -1
  31. package/man/mend-logout.1 +1 -1
  32. package/man/mend-man.1 +1 -1
  33. package/man/mend-members.1 +1 -1
  34. package/man/mend-operator-exposure.1 +1 -1
  35. package/man/mend-operator-gate.1 +1 -1
  36. package/man/mend-operator-grant-owner.1 +1 -1
  37. package/man/mend-operator-org-create.1 +1 -1
  38. package/man/mend-operator-org-invite-owner.1 +1 -1
  39. package/man/mend-operator-org-list.1 +1 -1
  40. package/man/mend-operator-org-rename.1 +1 -1
  41. package/man/mend-operator-reset-link.1 +1 -1
  42. package/man/mend-pair.1 +1 -1
  43. package/man/mend-projects.1 +1 -1
  44. package/man/mend-pull.1 +1 -1
  45. package/man/mend-refresh.1 +1 -1
  46. package/man/mend-rejoin.1 +1 -1
  47. package/man/mend-resume.1 +1 -1
  48. package/man/mend-run.1 +1 -1
  49. package/man/mend-server-logs.1 +1 -1
  50. package/man/mend-server-restart.1 +1 -1
  51. package/man/mend-server-setup.1 +1 -1
  52. package/man/mend-server-start.1 +1 -1
  53. package/man/mend-server-status.1 +1 -1
  54. package/man/mend-server-stop.1 +1 -1
  55. package/man/mend-server-upgrade.1 +1 -1
  56. package/man/mend-server.1 +1 -1
  57. package/man/mend-service-add.1 +1 -1
  58. package/man/mend-service-connect.1 +1 -1
  59. package/man/mend-service-init.1 +1 -1
  60. package/man/mend-service-list.1 +1 -1
  61. package/man/mend-service-logs.1 +1 -1
  62. package/man/mend-service-restart.1 +1 -1
  63. package/man/mend-service-run.1 +1 -1
  64. package/man/mend-service-stop.1 +1 -1
  65. package/man/mend-session-share.1 +1 -1
  66. package/man/mend-sessions.1 +1 -1
  67. package/man/mend-shell.1 +1 -1
  68. package/man/mend-skills-push.1 +1 -1
  69. package/man/mend-skills.1 +1 -1
  70. package/man/mend-snake.1 +1 -1
  71. package/man/mend-ssh-setup.1 +1 -1
  72. package/man/mend-ssh.1 +1 -1
  73. package/man/mend-stop.1 +8 -1
  74. package/man/mend-ui.1 +2 -2
  75. package/man/mend-uninstall.1 +1 -1
  76. package/man/mend-version.1 +1 -1
  77. package/man/mend-worktrees.1 +1 -1
  78. package/man/mend.1 +5 -2
  79. package/package.json +1 -1
@@ -26817,7 +26817,9 @@ var AuditAction = Schema_exports.Literals([
26817
26817
  // `gh` spoke as them.
26818
26818
  "change.landed",
26819
26819
  "change.pull_request_refreshed",
26820
- "change.bundle_downloaded"
26820
+ "change.bundle_downloaded",
26821
+ // A pull request opened outside Mend was read with the owner's `gh` and recorded.
26822
+ "change.pull_request_adopted"
26821
26823
  ]);
26822
26824
  var AuditData = Schema_exports.Record(
26823
26825
  Schema_exports.String,
@@ -26841,8 +26843,14 @@ var AuditEvent = class extends Schema_exports.Class("AuditEvent")({
26841
26843
  };
26842
26844
 
26843
26845
  // ../../packages/domain/src/workbench/landing.ts
26844
- var LandingTrigger = Schema_exports.Literals(["manual", "automatic"]);
26845
- var LandingOutcome = Schema_exports.Literals(["pushed", "pull-request", "refused", "failed"]);
26846
+ var LandingTrigger = Schema_exports.Literals(["manual", "automatic", "adopted"]);
26847
+ var LandingOutcome = Schema_exports.Literals([
26848
+ "pushed",
26849
+ "pull-request",
26850
+ "refused",
26851
+ "failed",
26852
+ "adopted"
26853
+ ]);
26846
26854
  var PullRequestState = Schema_exports.Literals(["open", "closed", "merged"]);
26847
26855
  var LandedPullRequest = Schema_exports.Struct({
26848
26856
  number: Schema_exports.Int,
@@ -26868,6 +26876,19 @@ var ChangeLanding = class extends Schema_exports.Class("ChangeLanding")({
26868
26876
  pushedSha: Schema_exports.NullOr(Sha),
26869
26877
  trigger: LandingTrigger,
26870
26878
  pullRequest: Schema_exports.NullOr(LandedPullRequest),
26879
+ /**
26880
+ * The pull request's head is in another repository (a fork): Mend pushes to origin only, so it
26881
+ * never updates such a pull request. Older servers omit it.
26882
+ */
26883
+ pullRequestCrossRepository: Schema_exports.Boolean.pipe(
26884
+ Schema_exports.withDecodingDefaultKey(Effect_exports.succeed(false)),
26885
+ Schema_exports.withConstructorDefault(Effect_exports.succeed(false))
26886
+ ),
26887
+ /** Who owns the repository the pull request's head is in, as `gh` reported it; null unknown. */
26888
+ pullRequestHeadOwner: Schema_exports.NullOr(Schema_exports.String).pipe(
26889
+ Schema_exports.withDecodingDefaultKey(Effect_exports.succeed(null)),
26890
+ Schema_exports.withConstructorDefault(Effect_exports.succeed(null))
26891
+ ),
26871
26892
  outcome: LandingOutcome,
26872
26893
  /** The remote's or `gh`'s own words for a refusal or a failure; null otherwise. */
26873
26894
  message: Schema_exports.NullOr(Schema_exports.String),
@@ -26915,7 +26936,11 @@ var LandingFact = Schema_exports.Union([
26915
26936
  Schema_exports.TaggedStruct("pull-request", {
26916
26937
  number: Schema_exports.Int,
26917
26938
  state: PullRequestState,
26918
- observedAt: Timestamp
26939
+ observedAt: Timestamp,
26940
+ /** Opened outside Mend and adopted. Older servers omit it. */
26941
+ outside: Schema_exports.Boolean.pipe(Schema_exports.withDecodingDefaultKey(Effect_exports.succeed(false))),
26942
+ /** The fork's owner when its head is in another repository; null for origin's own. */
26943
+ fork: Schema_exports.NullOr(Schema_exports.String).pipe(Schema_exports.withDecodingDefaultKey(Effect_exports.succeed(null)))
26919
26944
  }),
26920
26945
  /** A fetch found commits on origin's branch that Mend's branch does not have. */
26921
26946
  Schema_exports.TaggedStruct("origin-moved", { branch: Schema_exports.String, commits: Schema_exports.Int }),
@@ -26956,7 +26981,7 @@ var landingFactLine = (fact, now2) => {
26956
26981
  case "pushed":
26957
26982
  return `pushed \xB7 ${fact.branch} \xB7 ${shortSha(fact.sha)} \xB7 observed`;
26958
26983
  case "pull-request":
26959
- return `pull request #${fact.number} \xB7 ${fact.state} \xB7 observed ${observedAgo(fact.observedAt, now2)}`;
26984
+ return `pull request #${fact.number} \xB7 ${fact.state} \xB7 observed ${observedAgo(fact.observedAt, now2)}` + (fact.outside ? " \xB7 opened outside Mend" : "") + (fact.fork === null ? "" : ` \xB7 from ${fact.fork === "" ? "a fork" : `${fact.fork}'s fork`}`);
26960
26985
  case "origin-moved":
26961
26986
  return `origin has moved \xB7 ${fact.branch} has ${plural(fact.commits, "commit", "commits")} Mend has not seen`;
26962
26987
  case "changed-since-landing":
@@ -27258,6 +27283,37 @@ var FolderFile = class extends Schema_exports.Class("FolderFile")({
27258
27283
  var FOLDER_MAX_FILE_BYTES = 1024 * 1024;
27259
27284
  var FOLDER_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
27260
27285
 
27286
+ // ../../packages/domain/src/workbench/git-author.ts
27287
+ var GitAuthorSource = Schema_exports.Literals(["setting", "account"]);
27288
+ var GitAuthor = class extends Schema_exports.Class("GitAuthor")({
27289
+ name: Schema_exports.String,
27290
+ email: Schema_exports.String
27291
+ }) {
27292
+ };
27293
+ var ResolvedGitAuthor = class extends Schema_exports.Class("ResolvedGitAuthor")({
27294
+ name: Schema_exports.String,
27295
+ email: Schema_exports.String,
27296
+ source: GitAuthorSource
27297
+ }) {
27298
+ };
27299
+ var GIT_AUTHOR_MAX_LENGTH = 200;
27300
+ var normalizeGitAuthor = (author) => new GitAuthor({ name: author.name.trim(), email: author.email.trim() });
27301
+ var gitAuthorIssue = (author) => {
27302
+ const { name, email } = normalizeGitAuthor(author);
27303
+ if (name === "") return "the name is empty";
27304
+ if (email === "") return "the email is empty";
27305
+ if (name.length > GIT_AUTHOR_MAX_LENGTH) {
27306
+ return `the name is longer than ${GIT_AUTHOR_MAX_LENGTH} characters`;
27307
+ }
27308
+ if (email.length > GIT_AUTHOR_MAX_LENGTH) {
27309
+ return `the email is longer than ${GIT_AUTHOR_MAX_LENGTH} characters`;
27310
+ }
27311
+ if (/[<>\r\n]/.test(name)) return "the name contains <, > or a line break";
27312
+ if (!/^[^\s<>@]+@[^\s<>@]+$/.test(email))
27313
+ return "the email is not an address like you@example.com";
27314
+ return null;
27315
+ };
27316
+
27261
27317
  // ../../packages/domain/src/workbench/review-slice.ts
27262
27318
  var DiffDigest = Schema_exports.String.pipe(
27263
27319
  Schema_exports.check(Schema_exports.isPattern(/^[0-9a-f]{64}$/)),
@@ -27563,13 +27619,14 @@ var ProjectLink = class extends Schema_exports.Class("ProjectLink")({
27563
27619
 
27564
27620
  // ../../packages/domain/src/workbench/pass.ts
27565
27621
  var PassKind = Schema_exports.Literals(["tour", "read", "suggest"]);
27566
- var PassStatus = Schema_exports.Literals(["running", "completed", "failed"]);
27622
+ var PassStatus = Schema_exports.Literals(["queued", "running", "completed", "failed"]);
27567
27623
  var ChangePass = class extends Schema_exports.Class("ChangePass")({
27568
27624
  changeId: ChangeId,
27569
27625
  kind: PassKind,
27570
27626
  status: PassStatus,
27571
27627
  detail: Schema_exports.NullOr(Schema_exports.String),
27572
27628
  findings: Schema_exports.NullOr(Schema_exports.Int),
27629
+ /** When it began running; for a `queued` row, when it was queued. */
27573
27630
  startedAt: Timestamp,
27574
27631
  finishedAt: Schema_exports.NullOr(Timestamp)
27575
27632
  }) {
@@ -27599,7 +27656,7 @@ var ProjectClusterBindingsSnapshot = class extends Schema_exports.Class(
27599
27656
  }) {
27600
27657
  };
27601
27658
 
27602
- // ../../node_modules/.pnpm/@sealant+api-contracts@0.37.0_effect@4.0.0-beta.93/node_modules/@sealant/api-contracts/dist/workspace-environment.js
27659
+ // ../../node_modules/.pnpm/@sealant+api-contracts@0.37.1_effect@4.0.0-beta.93/node_modules/@sealant/api-contracts/dist/workspace-environment.js
27603
27660
  var RESERVED_NAMES = new Map([
27604
27661
  // Identity/process roots the daemon and shells own.
27605
27662
  ...[
@@ -27934,12 +27991,20 @@ var ServiceView = class extends Schema_exports.Class("ServiceView")({
27934
27991
  }) {
27935
27992
  };
27936
27993
 
27994
+ // ../../packages/domain/src/workbench/session-fold.ts
27995
+ var servicesHoldLine = (input) => {
27996
+ if (input.agentLive || input.liveServices <= 0) return null;
27997
+ const hold = input.liveServices === 1 ? "1 service keeps the workspace up" : `${input.liveServices} services keep the workspace up`;
27998
+ return input.agentOutcome === null ? hold : `agent ${input.agentOutcome} \xB7 ${hold}`;
27999
+ };
28000
+
27937
28001
  // ../../packages/domain/src/workbench/session-control.ts
27938
28002
  var SessionControlKind = Schema_exports.Literals([
27939
28003
  "interrupt",
27940
28004
  "terminal-attach",
27941
28005
  "shell-open",
27942
28006
  "stop",
28007
+ "services-stop",
27943
28008
  "shared-control-on",
27944
28009
  "shared-control-off"
27945
28010
  ]);
@@ -28315,6 +28380,11 @@ var agentOutcome = (currentAgent) => {
28315
28380
  if (currentAgent.harness === "shell") return "completed";
28316
28381
  return currentAgent.exitCode === null || currentAgent.exitCode === 0 ? "completed" : "failed";
28317
28382
  };
28383
+ var servicesHoldOf = (session, currentAgent, liveServices) => servicesHoldLine({
28384
+ agentLive: agentIsLive(session, currentAgent),
28385
+ agentOutcome: agentOutcome(currentAgent),
28386
+ liveServices
28387
+ });
28318
28388
  var normalizeRemoteUrl = (raw) => {
28319
28389
  if (raw === null) return null;
28320
28390
  const trimmed = raw.trim();
@@ -28454,6 +28524,9 @@ export {
28454
28524
  narrowCredential,
28455
28525
  claudeGrantFacts,
28456
28526
  sameGrant,
28527
+ normalizeGitAuthor,
28528
+ gitAuthorIssue,
28529
+ servicesHoldLine,
28457
28530
  HARNESS_COMMANDS,
28458
28531
  pendingId,
28459
28532
  isPendingId,
@@ -28463,6 +28536,7 @@ export {
28463
28536
  LIVE_STATUSES,
28464
28537
  agentIsLive,
28465
28538
  agentOutcome,
28539
+ servicesHoldOf,
28466
28540
  normalizeRemoteUrl,
28467
28541
  matchProjectByCwd,
28468
28542
  cwdFacts,
@@ -2,6 +2,8 @@ import {
2
2
  HARNESS_COMMANDS,
3
3
  LIVE_STATUSES,
4
4
  RepositoryCloneUrl,
5
+ agentIsLive,
6
+ agentOutcome,
5
7
  createSseParser,
6
8
  cwdFacts,
7
9
  eventFamilies,
@@ -9,8 +11,9 @@ import {
9
11
  matchProjectByCwd,
10
12
  normalizeProjectName,
11
13
  pendingId,
12
- repositoryCloneUrlIssue
13
- } from "./chunk-BMFB2UN7.js";
14
+ repositoryCloneUrlIssue,
15
+ servicesHoldLine
16
+ } from "./chunk-J3TR4UAG.js";
14
17
 
15
18
  // src/dashboard.tsx
16
19
  import { CliRenderEvents, createCliRenderer } from "@opentui/core";
@@ -267,17 +270,31 @@ var createLaunchGate = () => {
267
270
  var markSessionStopped = (data, sessionId) => {
268
271
  const processesBySession = new Map(data.processesBySession);
269
272
  processesBySession.delete(sessionId);
270
- const servicesBySession = new Map(data.servicesBySession);
271
- servicesBySession.delete(sessionId);
272
273
  return {
273
274
  ...mapWorkbenchSessions(
274
275
  data,
275
276
  (session) => session.id === sessionId ? { ...session, status: "stopped" } : session
276
277
  ),
277
- processesBySession,
278
- servicesBySession
278
+ processesBySession
279
279
  };
280
280
  };
281
+ var markServicesStopped = (data, sessionId) => {
282
+ const servicesBySession = new Map(data.servicesBySession);
283
+ servicesBySession.delete(sessionId);
284
+ return { ...data, servicesBySession };
285
+ };
286
+ var AGENT_WORKING = /* @__PURE__ */ new Set(["starting", "running", "waiting"]);
287
+ var sessionHold = (item) => {
288
+ const agent = item.annotation?.currentAgent ?? null;
289
+ const agentProcessLive = item.processes.some(
290
+ (process2) => process2.kind !== "shell" && process2.exitedAt === null
291
+ );
292
+ return servicesHoldLine({
293
+ agentLive: agent === null ? agentProcessLive || AGENT_WORKING.has(item.session.status) : agentIsLive(item.session, agent),
294
+ agentOutcome: agentOutcome(agent),
295
+ liveServices: Math.max(item.annotation?.liveServices ?? 0, item.services.length)
296
+ });
297
+ };
281
298
  var removeWorktreeGroup = (data, projectId, group) => {
282
299
  const memberIds = new Set(group.sessions.map((item) => item.session.id));
283
300
  const detail = data.details.get(projectId);
@@ -995,6 +1012,7 @@ var stateColor = (state) => state === "draft" || state === "open" ? INK : state
995
1012
  var anchorOf = (comment) => comment.file === null ? "change" : `${comment.file}${comment.line === null ? "" : `:${comment.line}${comment.endLine === null || comment.endLine === comment.line ? "" : `-${comment.endLine}`}`}`;
996
1013
  var passFact = (pass) => {
997
1014
  const label = pass.kind === "suggest" ? "suggestions" : pass.kind;
1015
+ if (pass.status === "queued") return { text: `${label} queued`, color: MUTED };
998
1016
  if (pass.status === "running") return { text: `${label} running`, color: INK_2 };
999
1017
  if (pass.status === "failed") return { text: `${label} failed`, color: ERROR };
1000
1018
  return {
@@ -2257,13 +2275,15 @@ var SessionRow = ({
2257
2275
  const shells = processes.filter((process2) => process2.kind === "shell").length;
2258
2276
  const nameWidth = Math.max(6, width - 5 - status.length);
2259
2277
  const factWidth = Math.max(8, width - 3);
2278
+ const hold = sessionHold(item);
2260
2279
  const facts = fitHints(
2261
2280
  [
2281
+ ...hold === null ? [] : [hold],
2262
2282
  session.harness,
2263
2283
  shortAge(session.createdAt),
2264
2284
  ...shells > 0 ? [`${shells} shell`] : [],
2265
2285
  ...agents > 0 ? [`${agents} agent`] : [],
2266
- ...services.length > 0 ? [`${services.length} service`] : []
2286
+ ...hold === null && services.length > 0 ? [`${services.length} service`] : []
2267
2287
  ],
2268
2288
  factWidth
2269
2289
  );
@@ -2315,6 +2335,7 @@ var SessionFacts = ({
2315
2335
  }) => {
2316
2336
  const { session, annotation, services } = item;
2317
2337
  const color = STATUS_COLOR[session.status] ?? MUTED;
2338
+ const hold = sessionHold(item);
2318
2339
  const summary = session.summary?.split("\n")[0] ?? null;
2319
2340
  const change = annotation ?? group?.annotation;
2320
2341
  const lines = [
@@ -2326,6 +2347,10 @@ var SessionFacts = ({
2326
2347
  /* @__PURE__ */ jsxs3("text", { height: 1, bg: "transparent", children: [
2327
2348
  /* @__PURE__ */ jsx3("span", { children: " " }),
2328
2349
  /* @__PURE__ */ jsx3("span", { fg: color, children: session.status }),
2350
+ hold === null ? null : /* @__PURE__ */ jsxs3(Fragment3, { children: [
2351
+ /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " \xB7 " }),
2352
+ /* @__PURE__ */ jsx3("span", { fg: INK_2, children: hold })
2353
+ ] }),
2329
2354
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " \xB7 " }),
2330
2355
  /* @__PURE__ */ jsx3("span", { fg: MUTED, children: session.branch }),
2331
2356
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: session.baseSha === "" ? "" : ` vs ${session.baseRef ?? session.baseSha.slice(0, 12)}` })
@@ -2779,6 +2804,23 @@ ${tunneled}
2779
2804
  },
2780
2805
  onSettled: settleRefetch
2781
2806
  });
2807
+ const stopServicesMutation = useMutation2({
2808
+ mutationFn: (session) => ctx.api("POST", `/sessions/${session.id}/services/stop`),
2809
+ onMutate: async (session) => {
2810
+ await queryClient.cancelQueries({ queryKey: WORKBENCH_KEY });
2811
+ patchWorkbench((current) => markServicesStopped(current, session.id));
2812
+ },
2813
+ onSuccess: (result, session) => {
2814
+ say(
2815
+ `stopped ${result.stopped} service${result.stopped === 1 ? "" : "s"} \xB7 ${sessionDisplayName(session)} \u2014 the workspace ends once nothing is live`
2816
+ );
2817
+ },
2818
+ onError: (error) => {
2819
+ say(errorText(error));
2820
+ refetch();
2821
+ },
2822
+ onSettled: settleRefetch
2823
+ });
2782
2824
  const stopMutation = useMutation2({
2783
2825
  mutationFn: (session) => ctx.api("POST", `/sessions/${session.id}/stop`),
2784
2826
  onMutate: async (session) => {
@@ -2943,6 +2985,17 @@ ${tunneled}
2943
2985
  }
2944
2986
  const session = selectedSession;
2945
2987
  if (session === null || isPendingId(session.id)) return;
2988
+ const hold = selectedItem === null ? null : sessionHold(selectedItem);
2989
+ if (hold !== null) {
2990
+ if (stopArmed === `svc:${session.id}` && confirmationVisible("press \u21E7K again")) {
2991
+ setStopArmed(null);
2992
+ stopServicesMutation.mutate(session);
2993
+ return;
2994
+ }
2995
+ setStopArmed(`svc:${session.id}`);
2996
+ say(`press \u21E7K again to stop the services \xB7 ${sessionDisplayName(session)} \u2014 ${hold}`);
2997
+ return;
2998
+ }
2946
2999
  if (!LIVE_STATUSES.has(session.status)) {
2947
3000
  say("nothing to stop \u2014 the session is settled");
2948
3001
  return;
package/dist/main.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  claudeGrantFacts,
8
8
  createSseParser,
9
9
  cwdFacts,
10
+ gitAuthorIssue,
10
11
  gitCurrentBranch,
11
12
  gitOriginUrl,
12
13
  gitTopLevel,
@@ -16,15 +17,17 @@ import {
16
17
  landingFactsFromWire,
17
18
  matchProjectByCwd,
18
19
  narrowCredential,
20
+ normalizeGitAuthor,
19
21
  normalizeProjectName,
20
22
  normalizeRemoteUrl,
21
23
  parseLaunchArgs,
22
24
  pasteBytes,
23
25
  repositoryCloneUrlIssue,
24
26
  sameGrant,
27
+ servicesHoldOf,
25
28
  sessionDisplayName,
26
29
  trackBracketedPaste
27
- } from "./chunk-BMFB2UN7.js";
30
+ } from "./chunk-J3TR4UAG.js";
28
31
 
29
32
  // src/main.ts
30
33
  import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
@@ -698,6 +701,21 @@ var formatLoadReport = (report, paint6) => {
698
701
  return lines;
699
702
  };
700
703
 
704
+ // src/git-author.ts
705
+ var parseGitAuthorArgs = (args) => {
706
+ const clear = args.includes("--clear");
707
+ const words = args.filter((arg) => !arg.startsWith("--"));
708
+ if (clear) return words.length === 0 ? { kind: "clear" } : { kind: "usage" };
709
+ if (words.length === 0) return { kind: "show" };
710
+ const [name, email] = words;
711
+ if (words.length !== 2 || name === void 0 || email === void 0) return { kind: "usage" };
712
+ const issue = gitAuthorIssue({ name, email });
713
+ if (issue !== null) return { kind: "invalid", message: `git author not saved \xB7 ${issue}` };
714
+ const author = normalizeGitAuthor({ name, email });
715
+ return { kind: "set", name: author.name, email: author.email };
716
+ };
717
+ var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.source === "setting" ? "your setting" : "your account's name and email"}`;
718
+
701
719
  // src/help.ts
702
720
  var SECTIONS = [
703
721
  "start",
@@ -862,7 +880,7 @@ var COMMANDS = [
862
880
  synopsis: ["[--no-tunnel]"],
863
881
  description: [
864
882
  "A full-screen view of every project and session, updating live. The session pane takes three quarters of the screen: a read-only detail for the selected session with the conversation record it has written so far. The remaining quarter is a sidebar of three stacked sections, projects then worktrees then sessions, where the section you are in stands open and the other two fold to the line that says what is selected. Bare mend with no command opens the same thing.",
865
- "Moving the selection only ever navigates: arrows or j/k move inside the open section, enter and the arrows move between the sidebar and the session pane, and nothing takes this terminal until you ask. Reading the record leaves the sidebar as it was. The verbs are a to attach a live session, r to resume a settled one, n for another session in the selected worktree, w for a new worktree, e to rename, v to review the change, o to open it in the browser, Shift+K to stop, Shift+D to remove, and Shift+R to refresh.",
883
+ "Moving the selection only ever navigates: arrows or j/k move inside the open section, enter and the arrows move between the sidebar and the session pane, and nothing takes this terminal until you ask. Reading the record leaves the sidebar as it was. The verbs are a to attach a live session, r to resume a settled one, n for another session in the selected worktree, w for a new worktree, e to rename, v to review the change, o to open it in the browser, Shift+K to stop (on a row whose agent has stopped while its Services keep the workspace up, Shift+K stops those Services), Shift+D to remove, and Shift+R to refresh.",
866
884
  "Nothing is ever squeezed. A terminal too narrow for both gives the whole width to the side you are on, a terminal too short for three drawn panes shows the open section alone, and a one-line breadcrumb states whatever did not fit.",
867
885
  "On a server that is not this machine, the selected session's live Services declared --http or --https are tunneled to this machine's loopback while it stays selected, and the session pane shows where each one opens, for example web \u2192 http://localhost:5173.",
868
886
  "The dashboard needs Node 26 or newer for its terminal. Every other command works on Node 22."
@@ -899,12 +917,17 @@ var COMMANDS = [
899
917
  name: "stop",
900
918
  section: "sessions",
901
919
  summary: "stop the agent; the record and the review remain",
902
- synopsis: ["[session-id-prefix]", "--all [--project <p>]"],
920
+ synopsis: ["[session-id-prefix]", "--all [--project <p>]", "--services [session-id-prefix]"],
903
921
  description: [
904
922
  "Ends the agent process. The workspace harvests the harness state and closes. The worktree, the record, and the review stay, and mend resume brings the conversation back.",
923
+ "Services keep running after a stop, and a running Service keeps the workspace up. The stop then prints, for example, agent stopped \xB7 3 services keep the workspace up. --services stops all of them. Once nothing is live, the workspace closes.",
905
924
  sessionArg
906
925
  ],
907
- options: [{ flag: "--all", text: "every live session" }, project("limit --all to one project")],
926
+ options: [
927
+ { flag: "--all", text: "every live session" },
928
+ project("limit --all to one project"),
929
+ { flag: "--services", text: "stop the session's Services instead of its agent" }
930
+ ],
908
931
  see: ["resume", "attach"]
909
932
  },
910
933
  {
@@ -1017,18 +1040,26 @@ var COMMANDS = [
1017
1040
  name: "land",
1018
1041
  section: "sessions",
1019
1042
  summary: "push a session's change to origin and open its pull request",
1020
- synopsis: ["<session> [--branch <name>] [--no-pr] [--title <text>] [--project <p>]"],
1043
+ synopsis: [
1044
+ "<session> [--branch <name>] [--no-pr] [--title <text>] [--project <p>]",
1045
+ "<session> --check [--project <p>]"
1046
+ ],
1021
1047
  description: [
1022
1048
  "Only the change's owner lands it: the owner of the worktree's first session, not someone who started a session in it later. Mend takes a checkpoint, commits what the agent left uncommitted on top of the agent's own commits and the last landing, and pushes that to origin with the project's git access. The agent's commits are pushed as they are. The session's branch, and the worktree's files, index and HEAD, are not touched.",
1023
1049
  "The push only fast-forwards. When origin's branch has commits Mend has not seen, or origin refuses the push, nothing is pushed and the command prints the remote's own words. Mend never force-pushes.",
1024
1050
  "When origin is on GitHub, Mend then opens a pull request into the session's base branch, or updates the one it opened before. That call runs in a workspace with your GitHub account (mend connect github). The description holds Mend's review summary when there is one, the changed files and links back to the session, and an update replaces only Mend's section of it. For any other origin the push happens and the pull request is reported as unavailable, with the reason.",
1025
1051
  "Landing again after more work adds commits to the same branch and updates the same pull request. When nothing is new since the last landing, nothing is pushed and the command says so. The branch on origin is never the project's default branch or the pull request's base. The command then prints what Mend observed: the push, the pull request and its state, what changed since, and pushes the agent made itself. It exits 1 when the push was refused or a step failed.",
1052
+ "A pull request someone opened outside Mend, the agent through gh included, is found and recorded as the change's own: Mend looks after the agent pushes a branch and when its turn ends, and --check looks now. It asks GitHub, as you, about the worktree's branch, every branch the agent pushed, and the agent's head commit. A landing then pushes that pull request's branch and updates it. A pull request from a fork is shown and never updated: Mend pushes to origin only, and opens no second pull request beside it.",
1026
1053
  "<session> is a prefix of the session id or the worktree's name. Settled sessions count."
1027
1054
  ],
1028
1055
  options: [
1029
1056
  {
1030
1057
  flag: "--branch <name>",
1031
- text: "the branch on origin. Default: where the change last pushed to, else mend/<worktree>"
1058
+ text: "the branch on origin. Default: where the change last pushed to, the branch the agent pushed, an adopted pull request's branch, else mend/<worktree>"
1059
+ },
1060
+ {
1061
+ flag: "--check",
1062
+ text: "only look on GitHub for a pull request opened outside Mend, and record it; push nothing"
1032
1063
  },
1033
1064
  { flag: "--no-pr", text: "push only; open or update no pull request" },
1034
1065
  {
@@ -1293,6 +1324,22 @@ var COMMANDS = [
1293
1324
  options: [{ flag: "--all", text: "every known config file" }],
1294
1325
  see: ["dotfiles", "dotfiles repo"]
1295
1326
  },
1327
+ {
1328
+ name: "git-author",
1329
+ section: "project setup",
1330
+ summary: "the name and email your workspaces commit as",
1331
+ synopsis: ["[<name> <email>]", "--clear"],
1332
+ description: [
1333
+ "Commits the agent makes in your workspaces name this author. Until you set one, it is the name and email you registered with. Without arguments, prints it and where it comes from.",
1334
+ "Each workspace receives it as system git config before the agent starts, so a user section in your dotfiles' .gitconfig or in a repository's own config still decides. Sessions launched after a change commit as the new author."
1335
+ ],
1336
+ options: [{ flag: "--clear", text: "go back to your account's name and email" }],
1337
+ examples: [
1338
+ { command: 'mend git-author "Anna Example" anna@example.com', text: "" },
1339
+ { command: "mend git-author --clear", text: "" }
1340
+ ],
1341
+ see: ["dotfiles", "keys mode"]
1342
+ },
1296
1343
  {
1297
1344
  name: "keys init",
1298
1345
  section: "project setup",
@@ -2038,17 +2085,22 @@ var parseFlags = (args, valued, switches) => {
2038
2085
  return { positional, values, on };
2039
2086
  };
2040
2087
  var parseLandArgs = (args) => {
2041
- const flags = parseFlags(args, ["--branch", "--title", "--project"], ["--no-pr"]);
2088
+ const flags = parseFlags(args, ["--branch", "--title", "--project"], ["--no-pr", "--check"]);
2042
2089
  if ("error" in flags) return flags;
2043
2090
  const [session, extra] = flags.positional;
2044
2091
  if (session === void 0) return { error: "name the session to land" };
2045
2092
  if (extra !== void 0) return { error: `one session only; "${extra}" is extra` };
2093
+ const check = flags.on.has("--check");
2094
+ if (check && (flags.values.has("--branch") || flags.values.has("--title") || flags.on.has("--no-pr"))) {
2095
+ return { error: "--check pushes nothing; it takes no --branch, --title or --no-pr" };
2096
+ }
2046
2097
  const branch = flags.values.get("--branch")?.trim() ?? null;
2047
2098
  if (branch === "") return { error: "--branch needs a name" };
2048
2099
  const title = flags.values.get("--title")?.trim() ?? null;
2049
2100
  return {
2050
2101
  args: {
2051
2102
  session,
2103
+ check,
2052
2104
  branch,
2053
2105
  pullRequest: !flags.on.has("--no-pr"),
2054
2106
  title: title === "" ? null : title,
@@ -2157,10 +2209,40 @@ var readFacts = (wire) => {
2157
2209
  return fail("the server's landing facts are in a shape this CLI cannot read \xB7 update mend");
2158
2210
  }
2159
2211
  };
2212
+ var checkLine = (check) => {
2213
+ const pullRequest = check.landing?.pullRequest ?? null;
2214
+ switch (check.outcome) {
2215
+ case "adopted":
2216
+ return pullRequest === null ? "pull request recorded \xB7 opened outside Mend" : `pull request #${pullRequest.number} recorded \xB7 opened outside Mend \xB7 ${pullRequest.url}`;
2217
+ case "observed":
2218
+ return pullRequest === null ? "pull request already recorded" : `pull request #${pullRequest.number} already recorded \xB7 ${pullRequest.state} \xB7 observed`;
2219
+ case "none":
2220
+ return "no pull request on GitHub for the change's branches or the agent's commit";
2221
+ case "skipped":
2222
+ return `GitHub not checked \xB7 ${check.reason ?? "no reason given"}`;
2223
+ }
2224
+ };
2160
2225
  var landCommand = async (api2, args) => {
2161
2226
  const parsed = parseLandArgs(args);
2162
2227
  if ("error" in parsed) return fail(parsed.error);
2163
2228
  const { session, project: project2 } = await findSession(api2, parsed.args.session, parsed.args.project);
2229
+ if (parsed.args.check) {
2230
+ const before = await api2("GET", `/sessions/${session.id}/landings`);
2231
+ if (before.changeId === null) return fail("the session's worktree holds no change yet");
2232
+ const check = await api2(
2233
+ "POST",
2234
+ `/changes/${before.changeId}/pull-request/check`
2235
+ );
2236
+ const mark = check.outcome === "adopted" || check.outcome === "observed" ? green("\u2713") : dim("\xB7");
2237
+ say(`${mark} ${checkLine(check)}`);
2238
+ const after = await api2("GET", `/sessions/${session.id}/landings`);
2239
+ const facts = readFacts(after.facts);
2240
+ if (facts.length > 0) {
2241
+ say(dim(" observed"));
2242
+ for (const fact of facts) say(` ${landingFactLine(fact, /* @__PURE__ */ new Date())}`);
2243
+ }
2244
+ return;
2245
+ }
2164
2246
  const steps = parsed.args.pullRequest ? "checkpoint \xB7 commit \xB7 push \xB7 pull request" : "checkpoint \xB7 commit \xB7 push";
2165
2247
  say(
2166
2248
  dim(
@@ -6899,6 +6981,12 @@ var attachPicked = async (config, session, tunnels) => {
6899
6981
  return exitAfterSessionEnd(config, session.id);
6900
6982
  };
6901
6983
  var stopCommand = async (config, args) => {
6984
+ if (args.includes("--services")) {
6985
+ const prefix2 = args.find((arg) => !arg.startsWith("--"));
6986
+ const session = await resolveLiveSession(config, prefix2, "stop --services");
6987
+ await stopServicesOf(config, session);
6988
+ return;
6989
+ }
6902
6990
  const all = args.includes("--all");
6903
6991
  const projectFlag = args.indexOf("--project");
6904
6992
  const projectName = projectFlag !== -1 && args[projectFlag + 1] !== void 0 ? String(args[projectFlag + 1]) : null;
@@ -6944,12 +7032,33 @@ var stopSessions = async (config, targets, summarise) => {
6944
7032
  say6(
6945
7033
  `${green6("\u2713")} stopped \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))} \xB7 ${session.branch}`
6946
7034
  );
7035
+ const after = await api(config, "GET", `/sessions/${session.id}`).catch(
7036
+ () => null
7037
+ );
7038
+ const hold = after === null ? null : servicesHoldOf(after.session, after.currentAgent, after.liveServices ?? 0);
7039
+ if (hold !== null) {
7040
+ say6(`${amber2(" " + hold)} \xB7 mend stop --services ${session.id.slice(0, 8)}`);
7041
+ }
6947
7042
  say6(`${cobalt2(" review")} \xB7 ${config.url}/sessions/${session.id}`);
6948
7043
  }
6949
7044
  if (summarise) {
6950
7045
  say6(`${green6("\u2713")} stopped ${targets.length} session${targets.length === 1 ? "" : "s"}`);
6951
7046
  }
6952
7047
  };
7048
+ var stopServicesOf = async (config, session) => {
7049
+ const result = await api(
7050
+ config,
7051
+ "POST",
7052
+ `/sessions/${session.id}/services/stop`
7053
+ );
7054
+ if (result.stopped === 0) {
7055
+ say6(`no live services \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))}`);
7056
+ return;
7057
+ }
7058
+ say6(
7059
+ `${green6("\u2713")} stopped ${result.stopped} service${result.stopped === 1 ? "" : "s"} \xB7 ${session.harness} \xB7 ${dim6(session.id.slice(0, 8))} \xB7 ${session.branch}`
7060
+ );
7061
+ };
6953
7062
  var pickSessionInteractively = async (rows) => {
6954
7063
  say6(dim6("more than one live session \u2014 pick one:"));
6955
7064
  rows.forEach((row, index) => {
@@ -7845,6 +7954,34 @@ var keysShare = async (config) => {
7845
7954
  }
7846
7955
  });
7847
7956
  };
7957
+ var gitAuthorCommand = async (config, args) => {
7958
+ const asked = parseGitAuthorArgs(args);
7959
+ switch (asked.kind) {
7960
+ case "usage":
7961
+ return fail6(usageOf("git-author"));
7962
+ case "invalid":
7963
+ return fail6(asked.message);
7964
+ case "show":
7965
+ say6(gitAuthorLine(await api(config, "GET", "/me/git-author")));
7966
+ return;
7967
+ case "clear": {
7968
+ const author = await api(config, "DELETE", "/me/git-author");
7969
+ say6(`${green6("\u2713")} git author \xB7 ${gitAuthorLine(author)}`);
7970
+ return;
7971
+ }
7972
+ case "set": {
7973
+ const author = await api(config, "PUT", "/me/git-author", {
7974
+ name: asked.name,
7975
+ email: asked.email
7976
+ });
7977
+ say6(`${green6("\u2713")} git author \xB7 ${gitAuthorLine(author)}`);
7978
+ say6(
7979
+ dim6(" sessions launched from now on commit as this, unless their dotfiles say otherwise")
7980
+ );
7981
+ return;
7982
+ }
7983
+ }
7984
+ };
7848
7985
  var keysMode = async (config, args) => {
7849
7986
  const [value] = args;
7850
7987
  if (value !== void 0 && value !== "mend-key" && value !== "bridge") {
@@ -8248,6 +8385,7 @@ _mend() {
8248
8385
  'server:local server setup, lifecycle and upgrades'
8249
8386
  'uninstall:remove the server, local Mend files, or both'
8250
8387
  'keys:the machine Mend deploy key \u2014 init, show, share'
8388
+ 'git-author:the name and email your workspaces commit as'
8251
8389
  'skills:skill libraries \u2014 list, push'
8252
8390
  'accounts:your connected accounts on the platform'
8253
8391
  'pair:pair a phone or a second machine' 'doctor:read-only checklist of this setup'
@@ -8288,7 +8426,7 @@ _mend "$@"
8288
8426
  var BASH_COMPLETIONS = `_mend() {
8289
8427
  local cur=\${COMP_WORDS[COMP_CWORD]}
8290
8428
  if [ "$COMP_CWORD" -eq 1 ]; then
8291
- COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server uninstall keys skills pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
8429
+ COMPREPLY=( $(compgen -W "adopt codex claude opencode run attach stop shell service server uninstall keys git-author skills pair doctor continue resume rejoin land pull refresh projects sessions status ui help" -- "$cur") )
8292
8430
  return
8293
8431
  fi
8294
8432
  case \${COMP_WORDS[1]} in
@@ -8600,11 +8738,14 @@ var refreshCommand = async (config, args) => {
8600
8738
  }
8601
8739
  if (branches.length > 12) say6(dim6(` \u2026 ${branches.length - 12} more`));
8602
8740
  };
8741
+ var rowHold = (row) => row.annotation === void 0 ? null : servicesHoldOf(row.session, row.annotation.currentAgent, row.annotation.liveServices ?? 0);
8603
8742
  var printSessionRow = (row) => {
8604
8743
  const { session, annotation } = row;
8605
8744
  const live = ACTIVE_STATUSES.has(session.status);
8606
8745
  const status = session.status.padEnd(9);
8607
8746
  const facts = [];
8747
+ const hold = rowHold(row);
8748
+ if (hold !== null) facts.push(amber2(hold));
8608
8749
  if (session.label !== null) facts.push(session.label);
8609
8750
  if (annotation !== void 0 && annotation.openComments > 0) {
8610
8751
  facts.push(amber2(`${annotation.openComments} open`));
@@ -8633,19 +8774,22 @@ var sessionsCommand = async (config, args) => {
8633
8774
  return fail6(`no adopted project named "${projectName}"`);
8634
8775
  }
8635
8776
  let rows;
8636
- if (all || projectName !== null) {
8777
+ if (all || projectName !== null || !json) {
8637
8778
  const details = await Promise.all(
8638
8779
  scope.map(
8639
8780
  (p) => api(config, "GET", `/projects/${p.id}${all ? "?deadEnds=include" : ""}`)
8640
8781
  )
8641
8782
  );
8642
- rows = details.flatMap(
8783
+ const detailed = details.flatMap(
8643
8784
  (detail) => detail.sessions.map((session) => ({
8644
8785
  session,
8645
8786
  projectName: detail.project.name,
8646
8787
  annotation: detail.annotations.find((a) => a.sessionId === session.id)
8647
8788
  }))
8648
8789
  );
8790
+ rows = all || projectName !== null ? detailed : detailed.filter(
8791
+ (row) => ACTIVE_STATUSES.has(row.session.status) || rowHold(row) !== null
8792
+ );
8649
8793
  } else {
8650
8794
  const active = await api(config, "GET", "/sessions");
8651
8795
  const nameById = new Map(projects.map((p) => [p.id, p.name]));
@@ -8831,7 +8975,7 @@ var dashboard = async (config, options = {}) => {
8831
8975
  );
8832
8976
  process.exit(rerun.status ?? 0);
8833
8977
  }
8834
- const { runDashboard } = await import("./dashboard-OF5UXII2.js");
8978
+ const { runDashboard } = await import("./dashboard-3HNFSQZD.js");
8835
8979
  const agentShare = await startShareIfBridge(config);
8836
8980
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
8837
8981
  listServices: () => fetchServices(config),
@@ -8949,6 +9093,8 @@ var main = async () => {
8949
9093
  return uninstallCommand(config, rest);
8950
9094
  case "keys":
8951
9095
  return keysCommand(config, rest);
9096
+ case "git-author":
9097
+ return gitAuthorCommand(config, rest);
8952
9098
  case "dotfiles":
8953
9099
  return dotfilesCommand(config, rest);
8954
9100
  case "skills":