@sealant/mend 0.36.0-next.628 → 0.36.0-next.643

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 (89) hide show
  1. package/dist/{chunk-OTYH35OK.js → chunk-2TDRYRGO.js} +78 -3
  2. package/dist/{dashboard-P73HKQOT.js → dashboard-2HF3KTTU.js} +9 -1
  3. package/dist/main.js +392 -21
  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-pi.1 +1 -1
  10. package/man/mend-connect.1 +1 -1
  11. package/man/mend-continue.1 +1 -1
  12. package/man/mend-doctor.1 +1 -1
  13. package/man/mend-dotfiles-repo.1 +1 -1
  14. package/man/mend-dotfiles-sync.1 +1 -1
  15. package/man/mend-dotfiles.1 +1 -1
  16. package/man/mend-env-cluster.1 +1 -1
  17. package/man/mend-env-load.1 +1 -1
  18. package/man/mend-env-show.1 +1 -1
  19. package/man/mend-folder-create.1 +1 -1
  20. package/man/mend-folder-list.1 +1 -1
  21. package/man/mend-folder-push.1 +1 -1
  22. package/man/mend-folder-rm.1 +1 -1
  23. package/man/mend-git-author.1 +1 -1
  24. package/man/mend-help.1 +1 -1
  25. package/man/mend-invite.1 +1 -1
  26. package/man/mend-keys-init.1 +1 -1
  27. package/man/mend-keys-mode.1 +1 -1
  28. package/man/mend-keys-share.1 +1 -1
  29. package/man/mend-keys-show.1 +1 -1
  30. package/man/mend-land.1 +1 -1
  31. package/man/mend-login.1 +1 -1
  32. package/man/mend-logout.1 +1 -1
  33. package/man/mend-man.1 +1 -1
  34. package/man/mend-members.1 +1 -1
  35. package/man/mend-memory-import.1 +1 -1
  36. package/man/mend-memory-rm.1 +1 -1
  37. package/man/mend-memory-show.1 +1 -1
  38. package/man/mend-memory.1 +1 -1
  39. package/man/mend-models.1 +1 -1
  40. package/man/mend-operator-exposure.1 +1 -1
  41. package/man/mend-operator-gate.1 +1 -1
  42. package/man/mend-operator-grant-owner.1 +1 -1
  43. package/man/mend-operator-org-create.1 +1 -1
  44. package/man/mend-operator-org-invite-owner.1 +1 -1
  45. package/man/mend-operator-org-list.1 +1 -1
  46. package/man/mend-operator-org-rename.1 +1 -1
  47. package/man/mend-operator-reset-link.1 +1 -1
  48. package/man/mend-pair.1 +1 -1
  49. package/man/mend-projects.1 +1 -1
  50. package/man/mend-pull.1 +1 -1
  51. package/man/mend-refresh.1 +1 -1
  52. package/man/mend-rejoin.1 +1 -1
  53. package/man/mend-resume.1 +1 -1
  54. package/man/mend-run.1 +1 -1
  55. package/man/mend-secrets-add.1 +1 -1
  56. package/man/mend-secrets-rm.1 +1 -1
  57. package/man/mend-secrets.1 +1 -1
  58. package/man/mend-server-logs.1 +1 -1
  59. package/man/mend-server-restart.1 +1 -1
  60. package/man/mend-server-setup.1 +1 -1
  61. package/man/mend-server-start.1 +1 -1
  62. package/man/mend-server-status.1 +1 -1
  63. package/man/mend-server-stop.1 +1 -1
  64. package/man/mend-server-upgrade.1 +9 -2
  65. package/man/mend-server.1 +1 -1
  66. package/man/mend-service-add.1 +1 -1
  67. package/man/mend-service-connect.1 +1 -1
  68. package/man/mend-service-init.1 +1 -1
  69. package/man/mend-service-list.1 +1 -1
  70. package/man/mend-service-logs.1 +1 -1
  71. package/man/mend-service-restart.1 +1 -1
  72. package/man/mend-service-run.1 +1 -1
  73. package/man/mend-service-stop.1 +1 -1
  74. package/man/mend-session-share.1 +1 -1
  75. package/man/mend-sessions.1 +1 -1
  76. package/man/mend-shell.1 +1 -1
  77. package/man/mend-skills-push.1 +1 -1
  78. package/man/mend-skills.1 +1 -1
  79. package/man/mend-snake.1 +1 -1
  80. package/man/mend-ssh-setup.1 +1 -1
  81. package/man/mend-ssh.1 +1 -1
  82. package/man/mend-stop.1 +1 -1
  83. package/man/mend-ui.1 +1 -1
  84. package/man/mend-uninstall.1 +1 -1
  85. package/man/mend-version.1 +1 -1
  86. package/man/mend-worktrees-rm.1 +1 -1
  87. package/man/mend-worktrees.1 +1 -1
  88. package/man/mend.1 +1 -1
  89. package/package.json +2 -2
@@ -26965,13 +26965,17 @@ var sameGrant = (left, right) => {
26965
26965
  var LAUNCH_WAITING_PREFIX = "waiting \xB7 the previous session in this worktree";
26966
26966
  var LAUNCH_WAITING_SAVING = `${LAUNCH_WAITING_PREFIX} is saving`;
26967
26967
  var LAUNCH_BOOTING = "booting";
26968
+ var LAUNCH_QUEUED = "queued \xB7 waiting for a worker";
26969
+ var LAUNCH_BUILDING_PREFIX = "building the workspace image";
26968
26970
  var LAUNCH_PREPARING = "preparing the workspace \xB7 no runtime yet \xB7 after an update, building the image takes about 8 minutes";
26969
26971
  var LAUNCH_BUILDING_IMAGE_LEGACY = "building the workspace image (first launch after an update, ~8 min)";
26970
26972
  var LAUNCH_PHASE_PREFIXES = [
26971
26973
  LAUNCH_WAITING_PREFIX,
26972
26974
  LAUNCH_BOOTING,
26975
+ LAUNCH_QUEUED,
26973
26976
  LAUNCH_PREPARING,
26974
- LAUNCH_BUILDING_IMAGE_LEGACY
26977
+ LAUNCH_BUILDING_IMAGE_LEGACY,
26978
+ LAUNCH_BUILDING_PREFIX
26975
26979
  ];
26976
26980
  var phaseWordsOf = (summary) => {
26977
26981
  for (const prefix of LAUNCH_PHASE_PREFIXES) {
@@ -26995,6 +26999,12 @@ var launchPhaseOf = (summary) => {
26995
26999
  return { kind: "waiting-previous", words: phase.words };
26996
27000
  }
26997
27001
  if (phase.words.startsWith(LAUNCH_BOOTING)) return { kind: "booting", words: phase.words };
27002
+ if (phase.words.startsWith(LAUNCH_QUEUED)) return { kind: "queued", words: phase.words };
27003
+ if (phase.words === LAUNCH_BUILDING_IMAGE_LEGACY)
27004
+ return { kind: "preparing", words: phase.words };
27005
+ if (phase.words.startsWith(LAUNCH_BUILDING_PREFIX)) {
27006
+ return { kind: "building", words: phase.words };
27007
+ }
26998
27008
  return { kind: "preparing", words: phase.words };
26999
27009
  };
27000
27010
 
@@ -27127,6 +27137,9 @@ var AuditAction = Schema_exports.Literals([
27127
27137
  "session.shared_control_off",
27128
27138
  // The owner ended a session's workspace with captures still pending: work was discarded.
27129
27139
  "session.unsaved_discarded",
27140
+ // Mend stopped a session whose owner no longer has access to where it ran (mend#558): a removed
27141
+ // member, or a joiner who can no longer see the project. The session's owner is the actor.
27142
+ "session.stopped_no_access",
27130
27143
  "member.password_reset_issued",
27131
27144
  "organization.created",
27132
27145
  "organization.renamed",
@@ -28105,6 +28118,14 @@ var Session = class extends Schema_exports.Class("Session")({
28105
28118
  sharedControlEnabledAt: Schema_exports.NullOr(Timestamp).pipe(
28106
28119
  Schema_exports.withConstructorDefault(Effect_exports.succeed(null))
28107
28120
  ),
28121
+ /**
28122
+ * When shared control was first turned on, kept after it is turned off (docs/adr/0016,
28123
+ * decision 9): a session once shared runs on no one's memory and credits nobody until it ends.
28124
+ */
28125
+ sharedControlEverAt: Schema_exports.NullOr(Timestamp).pipe(
28126
+ Schema_exports.withDecodingDefaultKey(Effect_exports.succeed(null)),
28127
+ Schema_exports.withConstructorDefault(Effect_exports.succeed(null))
28128
+ ),
28108
28129
  /**
28109
28130
  * Whether the harness left a conversation behind — a transcript Mend captured at settle or
28110
28131
  * found in the live harness home. False is a dead end: nothing to resume, nothing to hand
@@ -28436,7 +28457,7 @@ var ProjectClusterBindingsSnapshot = class extends Schema_exports.Class(
28436
28457
  }) {
28437
28458
  };
28438
28459
 
28439
- // ../../node_modules/.pnpm/@sealant+api-contracts-next@0.39.0-next.694_effect@4.0.0-beta.93/node_modules/@sealant/api-contracts-next/dist/workspace-environment.js
28460
+ // ../../node_modules/.pnpm/@sealant+api-contracts-next@0.39.0-next.706_effect@4.0.0-beta.93/node_modules/@sealant/api-contracts-next/dist/workspace-environment.js
28440
28461
  var RESERVED_NAMES = new Map([
28441
28462
  // Identity/process roots the daemon and shells own.
28442
28463
  ...[
@@ -28607,7 +28628,17 @@ var SessionProcess = class extends Schema_exports.Class("SessionProcess")({
28607
28628
  Schema_exports.withConstructorDefault(Effect_exports.succeed(null))
28608
28629
  ),
28609
28630
  exitedAt: Schema_exports.NullOr(Timestamp),
28610
- updatedAt: Timestamp
28631
+ updatedAt: Timestamp,
28632
+ /**
28633
+ * The Mend account whose Linux user the process runs as in a person-layout executor
28634
+ * (docs/adr/0016, decision 1: "the person a process runs as is recorded on the process when it
28635
+ * starts"). Null for a process that runs as root (a `shared` executor), on rows from before it
28636
+ * was recorded, and where older servers omit it.
28637
+ */
28638
+ runsAs: Schema_exports.NullOr(Schema_exports.String).pipe(
28639
+ Schema_exports.withDecodingDefaultKey(Effect_exports.succeed(null)),
28640
+ Schema_exports.withConstructorDefault(Effect_exports.succeed(null))
28641
+ )
28611
28642
  }) {
28612
28643
  };
28613
28644
 
@@ -29064,6 +29095,50 @@ var SlackLinkedMentionJob = Schema_exports.Struct({
29064
29095
  linkedAt: Schema_exports.String
29065
29096
  });
29066
29097
 
29098
+ // ../../packages/domain/src/workbench/steering.ts
29099
+ var ConversationWaitWork = Schema_exports.Struct({
29100
+ kind: Schema_exports.Literals([
29101
+ "task",
29102
+ "paused-task",
29103
+ "sub-agent",
29104
+ "terminal",
29105
+ "goal",
29106
+ "wakeup",
29107
+ "monitor",
29108
+ "cron",
29109
+ "unknown"
29110
+ ]),
29111
+ /** The harness's own id: what ending it names. */
29112
+ id: Schema_exports.String,
29113
+ description: Schema_exports.NullOr(Schema_exports.String),
29114
+ /**
29115
+ * The person the process runs as, or the session's owner, can end it from the waiting line
29116
+ * (Claude's task stop, a monitor's included, Codex's terminal terminate and goal clear). A
29117
+ * wakeup ends on its own within an hour; a scheduled prompt and what the harness would not say
29118
+ * are waited for at most `CONVERSATION_WAIT_BOUNDS_MS`.
29119
+ */
29120
+ endable: Schema_exports.Boolean
29121
+ });
29122
+ var ConversationWait = class extends Schema_exports.Class("ConversationWait")({
29123
+ sessionId: SessionId,
29124
+ /** The turn that waits; the turns behind it wait with it. */
29125
+ turnId: AgentTurnId,
29126
+ /** Whose process finishes its work: the person it runs as. */
29127
+ runsAs: Schema_exports.NullOr(Schema_exports.String),
29128
+ /** Whose turn waits. */
29129
+ sender: Schema_exports.String,
29130
+ /** A turn still runs in the process. */
29131
+ openTurn: Schema_exports.Boolean,
29132
+ work: Schema_exports.Array(ConversationWaitWork),
29133
+ /** The waiting line, in words: "Waits for Alice's 2 background tasks … before Bob's turn starts." */
29134
+ line: Schema_exports.String,
29135
+ since: Timestamp
29136
+ }) {
29137
+ };
29138
+ var CONVERSATION_WAIT_BOUNDS_MS = {
29139
+ cron: 10 * 6e4
29140
+ };
29141
+
29067
29142
  // ../../packages/domain/src/workbench/tour.ts
29068
29143
  var TourStop = class extends Schema_exports.Class("TourStop")({
29069
29144
  title: Schema_exports.String,
@@ -17,7 +17,7 @@ import {
17
17
  repositoryCloneUrlIssue,
18
18
  servicesHoldLine,
19
19
  withoutLaunchPhase
20
- } from "./chunk-OTYH35OK.js";
20
+ } from "./chunk-2TDRYRGO.js";
21
21
 
22
22
  // src/dashboard.tsx
23
23
  import { CliRenderEvents, createCliRenderer } from "@opentui/core";
@@ -271,6 +271,10 @@ var startingWordsOf = (session) => {
271
271
  return phase.words.endsWith("is saving") ? "waiting for the previous save" : "waiting for the previous session";
272
272
  case "booting":
273
273
  return "booting";
274
+ case "queued":
275
+ return "queued";
276
+ case "building":
277
+ return "building the image";
274
278
  case "preparing":
275
279
  return "preparing the workspace";
276
280
  }
@@ -283,6 +287,10 @@ var startingExplanationOf = (session) => {
283
287
  return phase.words.endsWith("is saving") ? "the previous session in this worktree is still saving \xB7 this one starts from its save" : phase.words;
284
288
  case "booting":
285
289
  return "the workspace is booting \xB7 the agent starts when it is up";
290
+ case "queued":
291
+ return "the launch waits for a worker \xB7 it starts when one takes it";
292
+ case "building":
293
+ return `${phase.words} \xB7 the agent starts once it is built and booted`;
286
294
  case "preparing":
287
295
  return "the workspace has no runtime yet \xB7 after an update, building its image takes about 8 minutes";
288
296
  }
package/dist/main.js CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  trackBracketedPaste,
53
53
  validatePiProfile,
54
54
  validateSecretFilePath
55
- } from "./chunk-OTYH35OK.js";
55
+ } from "./chunk-2TDRYRGO.js";
56
56
 
57
57
  // src/main.ts
58
58
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
@@ -2814,12 +2814,16 @@ var COMMANDS = [
2814
2814
  name: "server upgrade",
2815
2815
  section: "this machine",
2816
2816
  summary: "upgrade to an explicit version with a database backup",
2817
- synopsis: ["--version <target|latest> [--assets-dir <dir>] [--offline] [--from-preview]"],
2817
+ synopsis: [
2818
+ "--version <target|latest> [--assets-dir <dir>] [--offline] [--from-preview] [--keep-backups <n>]"
2819
+ ],
2818
2820
  description: [
2819
2821
  "Preflights release assets and the canonical image's version label before interrupting Mend. Downgrades are refused. latest is resolved only when explicitly requested; a same-version request does nothing.",
2820
2822
  "The installed configuration stays: the bind, the origin, the ports, the edge host and the declared exposure and tenancy are carried into the new generation, which renders the same overlays beside the new compose.yaml. An upgrade never drops the edge or the posture; mend server setup is where they change.",
2821
2823
  "Stops app writers, starts official Postgres if needed, and streams pg_dumpall into a private database backup before selecting and starting the target. Connections are interrupted. Workspace containers and data are retained, but active work can lose connectivity and need reconnection. Stop any external database writers before upgrading.",
2822
- "Preflight or backup failure retains the old pin and attempts to recover the old app if it was running. Once target startup may have begun, Mend never automatically downgrades or restores the database. The target pin, old generation and backup remain. Inspect logs, fix the target, then use mend server start. Recovery records are under the installation's backups/upgrade-UUID directory."
2824
+ "Preflight or backup failure retains the old pin and attempts to recover the old app if it was running. Once target startup may have begun, Mend never automatically downgrades or restores the database. The target pin, old generation and backup remain. Inspect logs, fix the target, then use mend server start. Recovery records are under the installation's backups/upgrade-UUID directory.",
2825
+ "After the target answers health, its recovery record is marked completed and older completed backups are removed, keeping the newest two (this one included) in the order Mend wrote them; each removal is printed with the space it freed. A failed upgrade removes nothing. A backup this release recorded as pending, or whose dump is incomplete, is always kept. Each dump holds the whole database, so it can run to gigabytes.",
2826
+ 'Backups written by releases before 0.36 carry no outcome: the first upgrade on 0.36 or later treats each one whose dump is whole as completed and keeps only the newest N, so copy any you want to keep out of backups/ first. Their removals are marked "from before 0.36, no recorded outcome".'
2823
2827
  ],
2824
2828
  options: [
2825
2829
  {
@@ -2837,6 +2841,10 @@ var COMMANDS = [
2837
2841
  {
2838
2842
  flag: "--from-preview",
2839
2843
  text: "once, from a preview numbered X.Y.Z-preview.K to a next build X.Y.Z-next.N; refused unless the target carries every migration the server applied"
2844
+ },
2845
+ {
2846
+ flag: "--keep-backups <n>",
2847
+ text: "completed upgrade backups to keep after a healthy upgrade, this one included; 0 keeps all. Default: 2"
2840
2848
  }
2841
2849
  ],
2842
2850
  examples: [
@@ -5993,6 +6001,286 @@ var commitGeneration = (paths, files) => {
5993
6001
  activateGeneration(paths, generation);
5994
6002
  return generation;
5995
6003
  };
6004
+ var renderRecovery = (record2) => `${JSON.stringify(record2, null, 2)}
6005
+ `;
6006
+ var BACKUP_NAME = /^upgrade-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
6007
+ var REMOVING_NAME = /^upgrade-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.removing$/;
6008
+ var RECOVERY_TEMPORARY = /^\.recovery-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
6009
+ var lstatOptional = (file) => {
6010
+ try {
6011
+ return fs14.lstatSync(file);
6012
+ } catch (cause) {
6013
+ if (hasCode(cause, "ENOENT")) return null;
6014
+ throw cause;
6015
+ }
6016
+ };
6017
+ var freedBy = (stat) => stat.nlink === 1 ? stat.size : 0;
6018
+ var GENERATION_NAME = /^gen-[0-9a-f-]{36}$/;
6019
+ var VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
6020
+ var generationVersion = (configDir, generation) => {
6021
+ if (!GENERATION_NAME.test(generation)) return void 0;
6022
+ try {
6023
+ const parsed = JSON.parse(
6024
+ fs14.readFileSync(path15.join(configDir, "generations", generation, "server.json"), "utf8")
6025
+ );
6026
+ return typeof parsed === "object" && parsed !== null && "serverVersion" in parsed && typeof parsed.serverVersion === "string" && VERSION.test(parsed.serverVersion) ? parsed.serverVersion : void 0;
6027
+ } catch {
6028
+ return void 0;
6029
+ }
6030
+ };
6031
+ var readRecord = (configDir, file, stat) => {
6032
+ let parsed;
6033
+ try {
6034
+ parsed = JSON.parse(fs14.readFileSync(file, "utf8"));
6035
+ } catch {
6036
+ return null;
6037
+ }
6038
+ if (typeof parsed !== "object" || parsed === null || !("previousGeneration" in parsed) || !("targetGeneration" in parsed) || typeof parsed.previousGeneration !== "string" || typeof parsed.targetGeneration !== "string")
6039
+ return null;
6040
+ const sequence = "sequence" in parsed && typeof parsed.sequence === "number" && Number.isSafeInteger(parsed.sequence) && parsed.sequence > 0 ? parsed.sequence : void 0;
6041
+ const stamped = "createdAt" in parsed && typeof parsed.createdAt === "string" ? Date.parse(parsed.createdAt) : Number.NaN;
6042
+ const targetGeneration = path15.basename(parsed.targetGeneration);
6043
+ return {
6044
+ previousGeneration: path15.basename(parsed.previousGeneration),
6045
+ targetGeneration,
6046
+ state: "state" in parsed ? parsed.state : void 0,
6047
+ sequence,
6048
+ recordsOutcome: "state" in parsed || "sequence" in parsed,
6049
+ targetVersion: generationVersion(configDir, targetGeneration),
6050
+ clock: Number.isFinite(stamped) ? stamped : stat.mtimeMs
6051
+ };
6052
+ };
6053
+ var DUMP_TRAILER = /-- PostgreSQL database cluster dump complete\r?\n(?:--\r?\n)?\s*$/;
6054
+ var dumpIsWhole = (file, size) => {
6055
+ const length = Math.min(size, 256);
6056
+ if (length === 0) return false;
6057
+ const buffer = Buffer.alloc(length);
6058
+ const fd = fs14.openSync(file, "r");
6059
+ try {
6060
+ fs14.readSync(fd, buffer, 0, length, size - length);
6061
+ } finally {
6062
+ fs14.closeSync(fd);
6063
+ }
6064
+ const tail = buffer.toString("utf8");
6065
+ const match = DUMP_TRAILER.exec(tail);
6066
+ if (match === null) return false;
6067
+ return match.index === 0 ? length === size : tail[match.index - 1] === "\n";
6068
+ };
6069
+ var readBackup = (configDir, directory) => {
6070
+ const stat = lstatOptional(directory);
6071
+ if (stat === null || !stat.isDirectory()) return { kind: "foreign" };
6072
+ if (REMOVING_NAME.test(path15.basename(directory))) return readRemoving(directory);
6073
+ if (!BACKUP_NAME.test(path15.basename(directory))) return { kind: "foreign" };
6074
+ const entries = fs14.readdirSync(directory);
6075
+ if (entries.length === 0) return { kind: "interrupted", bytes: 0 };
6076
+ const recoveryFile = path15.join(directory, "recovery.json");
6077
+ const recovery = lstatOptional(recoveryFile);
6078
+ if (recovery === null || !recovery.isFile()) return { kind: "foreign" };
6079
+ const record2 = readRecord(configDir, recoveryFile, recovery);
6080
+ if (record2 === null) return { kind: "foreign" };
6081
+ if (entries.includes("database.sql.partial"))
6082
+ return { kind: "held", reason: "unfinished", record: record2 };
6083
+ if (entries.some(
6084
+ (name) => name !== "database.sql" && name !== "recovery.json" && !RECOVERY_TEMPORARY.test(name)
6085
+ ))
6086
+ return { kind: "foreign" };
6087
+ if (record2.recordsOutcome && (record2.sequence === void 0 || record2.state === void 0))
6088
+ return {
6089
+ kind: "held",
6090
+ reason: "unreadable",
6091
+ detail: "recovery.json has a state without a valid sequence"
6092
+ };
6093
+ const dump = lstatOptional(path15.join(directory, "database.sql"));
6094
+ if (dump !== null && !dump.isFile()) return { kind: "foreign" };
6095
+ if (dump === null)
6096
+ return record2.state === "completed" ? { kind: "interrupted", bytes: freedBy(recovery) } : { kind: "held", reason: "unfinished", record: record2 };
6097
+ if (!dumpIsWhole(path15.join(directory, "database.sql"), dump.size))
6098
+ return { kind: "held", reason: "unfinished", record: record2 };
6099
+ if (record2.recordsOutcome && record2.state !== "completed")
6100
+ return { kind: "held", reason: "pending", record: record2 };
6101
+ return {
6102
+ kind: "completed",
6103
+ record: record2,
6104
+ bytes: freedBy(dump) + freedBy(recovery),
6105
+ legacy: !record2.recordsOutcome
6106
+ };
6107
+ };
6108
+ var readRemoving = (directory) => {
6109
+ let bytes = 0;
6110
+ for (const name of fs14.readdirSync(directory)) {
6111
+ const file = lstatOptional(path15.join(directory, name));
6112
+ if (file === null || !file.isFile() || !(name === "database.sql" || name === "database.sql.partial" || name === "recovery.json" || RECOVERY_TEMPORARY.test(name)))
6113
+ return { kind: "foreign" };
6114
+ bytes += freedBy(file);
6115
+ }
6116
+ return { kind: "interrupted", bytes };
6117
+ };
6118
+ var nextSequence = (configDir, backups) => {
6119
+ let highest = 0;
6120
+ for (const name of fs14.readdirSync(backups)) {
6121
+ if (!BACKUP_NAME.test(name)) continue;
6122
+ try {
6123
+ const directory = path15.join(backups, name);
6124
+ const stat = lstatOptional(directory);
6125
+ if (stat === null || !stat.isDirectory()) continue;
6126
+ const file = path15.join(directory, "recovery.json");
6127
+ const recovery = lstatOptional(file);
6128
+ if (recovery === null || !recovery.isFile()) continue;
6129
+ highest = Math.max(highest, readRecord(configDir, file, recovery)?.sequence ?? 0);
6130
+ } catch {
6131
+ continue;
6132
+ }
6133
+ }
6134
+ return highest + 1;
6135
+ };
6136
+ var versionOrder = (compareVersions, a, b) => {
6137
+ if (a === void 0 || b === void 0) return 0;
6138
+ try {
6139
+ return compareVersions(a, b);
6140
+ } catch {
6141
+ return 0;
6142
+ }
6143
+ };
6144
+ var chainDepths = (records) => {
6145
+ const depths = /* @__PURE__ */ new Map();
6146
+ const depth = (record2, visiting) => {
6147
+ const known = depths.get(record2);
6148
+ if (known !== void 0) return known;
6149
+ const next = new Set(visiting).add(record2);
6150
+ const before = records.filter(
6151
+ (other) => !next.has(other) && other.targetGeneration === record2.previousGeneration && other.targetGeneration !== other.previousGeneration
6152
+ );
6153
+ const value = Math.max(-1, ...before.map((other) => depth(other, next))) + 1;
6154
+ depths.set(record2, value);
6155
+ return value;
6156
+ };
6157
+ return (record2) => depth(record2, /* @__PURE__ */ new Set());
6158
+ };
6159
+ var newestFirst = (records, compareVersions) => {
6160
+ const depth = chainDepths(records.filter((record2) => record2.sequence === void 0));
6161
+ return (a, b) => {
6162
+ if (a.sequence !== void 0 || b.sequence !== void 0) {
6163
+ if (a.sequence === void 0) return 1;
6164
+ if (b.sequence === void 0) return -1;
6165
+ if (a.sequence !== b.sequence) return b.sequence - a.sequence;
6166
+ } else {
6167
+ const byVersion = versionOrder(compareVersions, b.targetVersion, a.targetVersion);
6168
+ if (byVersion !== 0) return byVersion;
6169
+ const byChain = depth(b) - depth(a);
6170
+ if (byChain !== 0) return byChain;
6171
+ }
6172
+ return b.clock - a.clock;
6173
+ };
6174
+ };
6175
+ var writtenAfterOutcomes = (record2, records, current, compareVersions) => {
6176
+ const sequenced = records.filter((other) => other.sequence !== void 0);
6177
+ const unsequenced = records.filter((other) => other.sequence === void 0);
6178
+ const seen = /* @__PURE__ */ new Set();
6179
+ const chainsFromSequenced = (candidate) => {
6180
+ if (seen.has(candidate)) return false;
6181
+ seen.add(candidate);
6182
+ if (sequenced.some((other) => other.targetGeneration === candidate.previousGeneration))
6183
+ return true;
6184
+ return unsequenced.some(
6185
+ (other) => other.targetGeneration === candidate.previousGeneration && chainsFromSequenced(other)
6186
+ );
6187
+ };
6188
+ return chainsFromSequenced(record2) || sequenced.some(
6189
+ (other) => other !== current && versionOrder(compareVersions, record2.targetVersion, other.targetVersion) > 0
6190
+ );
6191
+ };
6192
+ var removeBackup = (directory) => {
6193
+ const removing = REMOVING_NAME.test(path15.basename(directory)) ? directory : `${directory}.removing`;
6194
+ if (removing !== directory) fs14.renameSync(directory, removing);
6195
+ for (const name of fs14.readdirSync(removing))
6196
+ if (name === "database.sql" || name === "database.sql.partial" || name === "recovery.json" || RECOVERY_TEMPORARY.test(name))
6197
+ fs14.unlinkSync(path15.join(removing, name));
6198
+ fs14.rmdirSync(removing);
6199
+ };
6200
+ var entryOf = ({ directory, bytes, legacy }) => ({
6201
+ directory,
6202
+ bytes,
6203
+ legacy,
6204
+ interrupted: false
6205
+ });
6206
+ var failureOf = (cause) => cause instanceof Error ? cause.message : "unknown filesystem error";
6207
+ var pruneBackups = (configDir, keep, current, compareVersions) => {
6208
+ if (!Number.isSafeInteger(keep) || keep < 1)
6209
+ throw new ServerStoreError("Pruning keeps at least one upgrade backup.");
6210
+ const backups = path15.join(configDir, "backups");
6211
+ if (path15.dirname(current.directory) !== backups)
6212
+ throw new ServerStoreError("The current upgrade backup is outside this installation.");
6213
+ const currentRead = readBackup(configDir, current.directory);
6214
+ if (currentRead.kind !== "completed" || currentRead.record.sequence === void 0)
6215
+ throw new ServerStoreError("The current upgrade backup is not completed; nothing was pruned.");
6216
+ const completed = [];
6217
+ const interrupted = [];
6218
+ const held = [];
6219
+ const records = [currentRead.record];
6220
+ for (const name of fs14.readdirSync(backups).toSorted()) {
6221
+ const directory = path15.join(backups, name);
6222
+ if (directory === current.directory) continue;
6223
+ let read;
6224
+ try {
6225
+ read = readBackup(configDir, directory);
6226
+ } catch (cause) {
6227
+ if (BACKUP_NAME.test(name) || REMOVING_NAME.test(name))
6228
+ held.push({ directory, reason: "unreadable", detail: failureOf(cause) });
6229
+ continue;
6230
+ }
6231
+ if (read.kind === "held") {
6232
+ held.push({
6233
+ directory,
6234
+ reason: read.reason,
6235
+ ...read.detail === void 0 ? {} : { detail: read.detail }
6236
+ });
6237
+ if (read.record !== void 0) records.push(read.record);
6238
+ }
6239
+ if (read.kind === "interrupted")
6240
+ interrupted.push({ directory, bytes: read.bytes, legacy: false, interrupted: true });
6241
+ if (read.kind === "completed") {
6242
+ completed.push({
6243
+ directory,
6244
+ bytes: read.bytes,
6245
+ legacy: read.legacy,
6246
+ interrupted: false,
6247
+ record: read.record
6248
+ });
6249
+ records.push(read.record);
6250
+ }
6251
+ }
6252
+ const candidates = completed.filter((entry) => {
6253
+ if (!entry.legacy || !writtenAfterOutcomes(entry.record, records, currentRead.record, compareVersions))
6254
+ return true;
6255
+ held.push({ directory: entry.directory, reason: "no-outcome" });
6256
+ return false;
6257
+ });
6258
+ const compare = newestFirst(records, compareVersions);
6259
+ candidates.sort((a, b) => compare(a.record, b.record));
6260
+ const kept = [
6261
+ { directory: current.directory, bytes: currentRead.bytes, legacy: false, interrupted: false },
6262
+ ...candidates.slice(0, keep - 1).map(entryOf)
6263
+ ];
6264
+ const removed = [];
6265
+ const failed = [];
6266
+ for (const candidate of [...interrupted, ...candidates.slice(keep - 1).map(entryOf)]) {
6267
+ try {
6268
+ removeBackup(candidate.directory);
6269
+ removed.push(candidate);
6270
+ } catch (cause) {
6271
+ failed.push({ directory: candidate.directory, message: failureOf(cause) });
6272
+ }
6273
+ }
6274
+ let unsynced;
6275
+ if (removed.length > 0 || failed.length > 0) {
6276
+ try {
6277
+ syncDirectory(backups);
6278
+ } catch (cause) {
6279
+ unsynced = failureOf(cause);
6280
+ }
6281
+ }
6282
+ return { removed, kept, held, failed, ...unsynced === void 0 ? {} : { unsynced } };
6283
+ };
5996
6284
  var createStore = (configDir, lock) => {
5997
6285
  const paths = {
5998
6286
  configDir,
@@ -6014,22 +6302,19 @@ var createStore = (configDir, lock) => {
6014
6302
  const backups = path15.join(configDir, "backups");
6015
6303
  fs14.mkdirSync(backups, { recursive: true, mode: 448 });
6016
6304
  fs14.chmodSync(backups, 448);
6305
+ const sequence = nextSequence(configDir, backups);
6017
6306
  const directory = path15.join(backups, `upgrade-${randomUUID2()}`);
6018
6307
  fs14.mkdirSync(directory, { mode: 448 });
6019
- writeDurable(
6020
- path15.join(directory, "recovery.json"),
6021
- `${JSON.stringify(
6022
- {
6023
- previousGeneration: previous.directory,
6024
- targetGeneration: target.directory,
6025
- database: "database.sql",
6026
- policy: "If target is active, migrations may have begun. Never downgrade or restore automatically."
6027
- },
6028
- null,
6029
- 2
6030
- )}
6031
- `
6032
- );
6308
+ const record2 = {
6309
+ previousGeneration: previous.directory,
6310
+ targetGeneration: target.directory,
6311
+ database: "database.sql",
6312
+ sequence,
6313
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6314
+ policy: "If target is active, migrations may have begun. Never downgrade or restore automatically."
6315
+ };
6316
+ const recoveryFile = path15.join(directory, "recovery.json");
6317
+ writeDurable(recoveryFile, renderRecovery({ ...record2, state: "pending" }));
6033
6318
  syncDirectory(directory);
6034
6319
  syncDirectory(backups);
6035
6320
  syncDirectory(configDir);
@@ -6049,9 +6334,16 @@ var createStore = (configDir, lock) => {
6049
6334
  }
6050
6335
  fs14.renameSync(partialFile, path15.join(directory, "database.sql"));
6051
6336
  syncDirectory(directory);
6337
+ }),
6338
+ markCompleted: () => whileOwned(() => {
6339
+ const temporary = path15.join(directory, `.recovery-${randomUUID2()}`);
6340
+ writeDurable(temporary, renderRecovery({ ...record2, state: "completed" }));
6341
+ fs14.renameSync(temporary, recoveryFile);
6342
+ syncDirectory(directory);
6052
6343
  })
6053
6344
  };
6054
- })
6345
+ }),
6346
+ pruneBackups: (keep, current, compareVersions) => whileOwned(() => pruneBackups(configDir, keep, current, compareVersions))
6055
6347
  };
6056
6348
  };
6057
6349
  var refuseFlatLayout = (configDir) => {
@@ -7406,9 +7698,26 @@ var startInstallation = async (runtime, installation, secrets) => {
7406
7698
  await probeHealth(runtime, healthOrigin(installation.config), installation.config.serverVersion);
7407
7699
  runtime.writeLine(reachableLine(installation.config));
7408
7700
  };
7701
+ var DEFAULT_KEEP_BACKUPS = 2;
7409
7702
  var parseUpgradeOptions = (args) => {
7410
7703
  const fromPreview = args.includes("--from-preview");
7411
- const rest = args.filter((arg) => arg !== "--from-preview");
7704
+ const rest = [];
7705
+ let keepBackups = DEFAULT_KEEP_BACKUPS;
7706
+ for (let index = 0; index < args.length; index += 1) {
7707
+ const flag = args[index];
7708
+ if (flag === void 0 || flag === "--from-preview") continue;
7709
+ if (flag === "--keep-backups") {
7710
+ const value = args[index + 1];
7711
+ if (value === void 0 || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
7712
+ throw setupError(
7713
+ "--keep-backups takes a whole number: how many upgrade backups to keep, 0 for all."
7714
+ );
7715
+ keepBackups = Number(value);
7716
+ index += 1;
7717
+ continue;
7718
+ }
7719
+ rest.push(flag);
7720
+ }
7412
7721
  for (let index = 0; index < rest.length; index += 1) {
7413
7722
  const flag = rest[index];
7414
7723
  if (flag === "--offline") continue;
@@ -7421,7 +7730,63 @@ var parseUpgradeOptions = (args) => {
7421
7730
  throw setupError(
7422
7731
  "Upgrade requires --version TARGET. Use --version latest only to request the latest release explicitly."
7423
7732
  );
7424
- return { ...options, fromPreview };
7733
+ return { ...options, fromPreview, keepBackups };
7734
+ };
7735
+ var formatBytes3 = (bytes) => {
7736
+ if (bytes < 1024) return `${bytes} B`;
7737
+ const units = ["KiB", "MiB", "GiB", "TiB"];
7738
+ let value = bytes / 1024;
7739
+ let unit = 0;
7740
+ while (value >= 1024 && unit < units.length - 1) {
7741
+ value /= 1024;
7742
+ unit += 1;
7743
+ }
7744
+ return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;
7745
+ };
7746
+ var heldReason = (entry) => {
7747
+ switch (entry.reason) {
7748
+ case "pending":
7749
+ return "pending: its upgrade never recorded a healthy target";
7750
+ case "unfinished":
7751
+ return "unfinished: no complete database dump";
7752
+ case "no-outcome":
7753
+ return "no recorded outcome: written after a 0.36 backup by a CLI that records none";
7754
+ case "unreadable":
7755
+ return `unreadable: ${entry.detail ?? "not read"}`;
7756
+ }
7757
+ };
7758
+ var pruneUpgradeBackups = (runtime, store, backup, keep) => {
7759
+ const marked = backup.markCompleted();
7760
+ if (marked._tag === "error") {
7761
+ runtime.writeLine(
7762
+ `Could not record the upgrade as completed in ${backup.directory}/recovery.json; no backups were removed. ${marked.error.message}`
7763
+ );
7764
+ return;
7765
+ }
7766
+ if (keep === 0) {
7767
+ runtime.writeLine("Upgrade backups \xB7 all kept (--keep-backups 0)");
7768
+ return;
7769
+ }
7770
+ const pruned = store.pruneBackups(keep, backup, upgradeOrder);
7771
+ if (pruned._tag === "error") {
7772
+ runtime.writeLine(`Upgrade backups \xB7 not pruned: ${pruned.error.message}`);
7773
+ return;
7774
+ }
7775
+ const { removed, kept, held, failed, unsynced } = pruned.value;
7776
+ for (const entry of removed)
7777
+ runtime.writeLine(
7778
+ `Removed upgrade backup ${entry.directory} \xB7 ${formatBytes3(entry.bytes)}${entry.interrupted ? " \xB7 finishing a removal a crash cut short" : entry.legacy ? " \xB7 from before 0.36, no recorded outcome" : ""}`
7779
+ );
7780
+ const freed = removed.reduce((total, entry) => total + entry.bytes, 0);
7781
+ runtime.writeLine(
7782
+ `Upgrade backups \xB7 removed ${removed.length}${removed.length > 0 ? ` \xB7 ${formatBytes3(freed)} freed` : ""} \xB7 kept ${kept.length} (--keep-backups ${keep})`
7783
+ );
7784
+ for (const entry of failed)
7785
+ runtime.writeLine(`Could not remove upgrade backup ${entry.directory}: ${entry.message}`);
7786
+ if (unsynced !== void 0)
7787
+ runtime.writeLine(`Upgrade backups \xB7 removals made but not fsynced: ${unsynced}`);
7788
+ for (const entry of held)
7789
+ runtime.writeLine(`Kept upgrade backup ${entry.directory} \xB7 ${heldReason(entry)}`);
7425
7790
  };
7426
7791
  var LEGACY_PREVIEW = /^(\d+\.\d+\.\d+)-preview\.(0|[1-9]\d*)$/;
7427
7792
  var NEXT_BUILD = /^(\d+\.\d+\.\d+)-next\.(0|[1-9]\d*)(\.preview\.[1-9]\d*)?$/;
@@ -7430,6 +7795,11 @@ var isPreviewToNext = (from, to) => {
7430
7795
  const next = NEXT_BUILD.exec(to);
7431
7796
  return preview !== null && next !== null && preview[1] === next[1];
7432
7797
  };
7798
+ var upgradeOrder = (a, b) => {
7799
+ if (isPreviewToNext(a, b)) return -1;
7800
+ if (isPreviewToNext(b, a)) return 1;
7801
+ return compareServerVersions(a, b);
7802
+ };
7433
7803
  var drizzleMillis = (folder) => {
7434
7804
  const at = (start, end) => Number(folder.slice(start, end));
7435
7805
  return String(Date.UTC(at(0, 4), at(4, 6) - 1, at(6, 8), at(8, 10), at(10, 12), at(12, 14)));
@@ -7669,6 +8039,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
7669
8039
  );
7670
8040
  }
7671
8041
  runtime.writeLine(`Upgraded to ${version}. Retained database backup: ${backup.directory}`);
8042
+ pruneUpgradeBackups(runtime, store, backup, options.keepBackups);
7672
8043
  };
7673
8044
  var observeEdgeCertificate = async (runtime, installation, host) => {
7674
8045
  const listed = await runtime.run(
@@ -12537,7 +12908,7 @@ var dashboard = async (config, options = {}) => {
12537
12908
  );
12538
12909
  process.exit(rerun.status ?? 0);
12539
12910
  }
12540
- const { runDashboard } = await import("./dashboard-P73HKQOT.js");
12911
+ const { runDashboard } = await import("./dashboard-2HF3KTTU.js");
12541
12912
  const agentShare = await startShareIfBridge(config);
12542
12913
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
12543
12914
  listServices: () => fetchServices(config),
@@ -1,4 +1,4 @@
1
- .TH MEND-ACCOUNTS 1 "" "mend 0.36.0\-next.628" "mend manual"
1
+ .TH MEND-ACCOUNTS 1 "" "mend 0.36.0\-next.643" "mend manual"
2
2
  .SH NAME
3
3
  mend\-accounts \- your connected accounts on the platform
4
4
  .SH SYNOPSIS
package/man/mend-adopt.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-ADOPT 1 "" "mend 0.36.0\-next.628" "mend manual"
1
+ .TH MEND-ADOPT 1 "" "mend 0.36.0\-next.643" "mend manual"
2
2
  .SH NAME
3
3
  mend\-adopt \- adopt a repository into the store
4
4
  .SH SYNOPSIS