@sealant/mend 0.36.0-next.663 → 0.36.0-next.665

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 (92) hide show
  1. package/dist/{chunk-VOP6USOZ.js → chunk-K5WNRH4V.js} +13 -0
  2. package/dist/{dashboard-CEJN6IOO.js → dashboard-DQB5RNXJ.js} +1 -1
  3. package/dist/main.js +173 -25
  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-logs.1 +1 -1
  34. package/man/mend-man.1 +1 -1
  35. package/man/mend-members.1 +1 -1
  36. package/man/mend-memory-import.1 +1 -1
  37. package/man/mend-memory-rm.1 +1 -1
  38. package/man/mend-memory-show.1 +1 -1
  39. package/man/mend-memory.1 +1 -1
  40. package/man/mend-models.1 +1 -1
  41. package/man/mend-operator-exposure.1 +2 -2
  42. package/man/mend-operator-gate.1 +1 -1
  43. package/man/mend-operator-grant-owner.1 +1 -1
  44. package/man/mend-operator-org-create.1 +1 -1
  45. package/man/mend-operator-org-invite-owner.1 +1 -1
  46. package/man/mend-operator-org-list.1 +1 -1
  47. package/man/mend-operator-org-rename.1 +1 -1
  48. package/man/mend-operator-reset-link.1 +1 -1
  49. package/man/mend-pair.1 +1 -1
  50. package/man/mend-projects.1 +1 -1
  51. package/man/mend-pull.1 +1 -1
  52. package/man/mend-refresh.1 +1 -1
  53. package/man/mend-rejoin.1 +1 -1
  54. package/man/mend-resume.1 +1 -1
  55. package/man/mend-run.1 +1 -1
  56. package/man/mend-secrets-add.1 +1 -1
  57. package/man/mend-secrets-rm.1 +1 -1
  58. package/man/mend-secrets.1 +1 -1
  59. package/man/mend-server-logs.1 +1 -1
  60. package/man/mend-server-restart.1 +1 -1
  61. package/man/mend-server-setup.1 +12 -2
  62. package/man/mend-server-start.1 +1 -1
  63. package/man/mend-server-status.1 +1 -1
  64. package/man/mend-server-stop.1 +1 -1
  65. package/man/mend-server-upgrade.1 +1 -1
  66. package/man/mend-server.1 +1 -1
  67. package/man/mend-service-add.1 +1 -1
  68. package/man/mend-service-connect.1 +1 -1
  69. package/man/mend-service-init.1 +1 -1
  70. package/man/mend-service-list.1 +1 -1
  71. package/man/mend-service-logs.1 +1 -1
  72. package/man/mend-service-restart.1 +1 -1
  73. package/man/mend-service-run.1 +1 -1
  74. package/man/mend-service-stop.1 +1 -1
  75. package/man/mend-session-share.1 +1 -1
  76. package/man/mend-sessions.1 +1 -1
  77. package/man/mend-shell.1 +1 -1
  78. package/man/mend-skills-push.1 +1 -1
  79. package/man/mend-skills.1 +1 -1
  80. package/man/mend-snake.1 +1 -1
  81. package/man/mend-ssh-setup.1 +1 -1
  82. package/man/mend-ssh.1 +1 -1
  83. package/man/mend-stop.1 +1 -1
  84. package/man/mend-ui.1 +1 -1
  85. package/man/mend-uninstall.1 +1 -1
  86. package/man/mend-version.1 +1 -1
  87. package/man/mend-wait.1 +1 -1
  88. package/man/mend-workspace-replace.1 +1 -1
  89. package/man/mend-worktrees-rm.1 +1 -1
  90. package/man/mend-worktrees.1 +1 -1
  91. package/man/mend.1 +1 -1
  92. package/package.json +2 -2
@@ -28227,6 +28227,19 @@ var Session = class extends Schema_exports.Class("Session")({
28227
28227
  Schema_exports.withDecodingDefaultKey(Effect_exports.succeed(null)),
28228
28228
  Schema_exports.withConstructorDefault(Effect_exports.succeed(null))
28229
28229
  ),
28230
+ /**
28231
+ * Who launched the session's executor: the owner of the session whose own launch made it
28232
+ * (`SessionsRepo.executorSessionOf`), which a joined session is not. Remote-SSH into the workspace
28233
+ * admits that person only (docs/adr/0016), in either harness layout, so an editor asks before it
28234
+ * opens. Filled on every API read that hands sessions out (the session list, retained additions
28235
+ * included, the session view and the project view): the view reads in their own statement, the
28236
+ * plain reads from one batched read (`SessionsRepo.launchersOf`). Null with no executor, on the
28237
+ * repository's other reads, and from older servers.
28238
+ */
28239
+ workspaceLauncherUserId: Schema_exports.NullOr(Schema_exports.String).pipe(
28240
+ Schema_exports.withDecodingDefaultKey(Effect_exports.succeed(null)),
28241
+ Schema_exports.withConstructorDefault(Effect_exports.succeed(null))
28242
+ ),
28230
28243
  /**
28231
28244
  * Whether the harness left a conversation behind — a transcript Mend captured at settle or
28232
28245
  * found in the live harness home. False is a dead end: nothing to resume, nothing to hand
@@ -23,7 +23,7 @@ import {
23
23
  workspaceReadsOf,
24
24
  worktreeJoinLine,
25
25
  wrapWords
26
- } from "./chunk-VOP6USOZ.js";
26
+ } from "./chunk-K5WNRH4V.js";
27
27
 
28
28
  // src/dashboard.tsx
29
29
  import { CliRenderEvents, createCliRenderer } from "@opentui/core";
package/dist/main.js CHANGED
@@ -68,7 +68,7 @@ import {
68
68
  workspaceRetirementLine,
69
69
  worktreeJoinLine,
70
70
  worktreeListsPeople
71
- } from "./chunk-VOP6USOZ.js";
71
+ } from "./chunk-K5WNRH4V.js";
72
72
 
73
73
  // src/main.ts
74
74
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
@@ -861,6 +861,9 @@ var isTenancy = (value) => TENANCIES.some((tenancy) => tenancy === value);
861
861
  var EDGE_IMAGE = "caddy:2.10-alpine";
862
862
  var EDGE_COMPOSE_FILE = "compose.edge.yaml";
863
863
  var POSTURE_COMPOSE_FILE = "compose.posture.yaml";
864
+ var DECLARABLE_ITEMS = ["core-private", "edge-tls", "workspace-ssh"];
865
+ var isDeclarableItem = (value) => DECLARABLE_ITEMS.some((item) => item === value);
866
+ var publishedAddress = (address, port) => `${net2.isIP(address) === 6 ? `[${address}]` : address}:${port}`;
864
867
  var parseEdgeHost = (value) => {
865
868
  const host = value.trim().toLowerCase().replace(/\.$/, "");
866
869
  const labels = host.split(".");
@@ -876,7 +879,11 @@ var postureEnvironment = (posture) => {
876
879
  ...posture.exposure === void 0 ? [] : [["MEND_EXPOSURE", posture.exposure]],
877
880
  ...posture.tenancy === void 0 ? [] : [["MEND_TENANCY", posture.tenancy]],
878
881
  ...gate ? [["MEND_SOURCE_POLICY", "tenant"], ["MEND_CAPTURE_REQUIRE_SIZES", "true"]] : [],
879
- ...posture.exposure === "public" ? [["MEND_URL_BEARERS", "refuse"]] : []
882
+ ...posture.exposure === "public" ? [["MEND_URL_BEARERS", "refuse"]] : [],
883
+ // The gate's workspace-ssh item reads where SSH is published apart from the web port; the
884
+ // container cannot see what its host publishes.
885
+ ...posture.sshBind === void 0 || posture.sshPort === void 0 ? [] : [["MEND_SSH_PUBLISHED", publishedAddress(posture.sshBind, posture.sshPort)]],
886
+ ...posture.declared === void 0 || posture.declared.length === 0 ? [] : [["MEND_EXPOSURE_DECLARED", posture.declared.join(",")]]
880
887
  ];
881
888
  };
882
889
  var postureEnvLines = (posture) => [
@@ -927,7 +934,11 @@ var healthPosture = (body) => {
927
934
  var declaredPostureLines = (posture) => [
928
935
  ...posture.edgeHost === void 0 ? [] : [`edge \xB7 ${posture.edgeHost} \xB7 ${EDGE_IMAGE} on 80 and 443 \xB7 Mend's own port on loopback`],
929
936
  `exposure \xB7 declared ${posture.exposure ?? "private"}${posture.exposure === void 0 ? " \xB7 the default, not set on this install" : ""}`,
930
- `tenancy \xB7 declared ${posture.tenancy ?? "single"}${posture.tenancy === void 0 ? " \xB7 the default, not set on this install" : ""}`
937
+ `tenancy \xB7 declared ${posture.tenancy ?? "single"}${posture.tenancy === void 0 ? " \xB7 the default, not set on this install" : ""}`,
938
+ ...posture.sshBind === void 0 || posture.sshPort === void 0 ? [] : [
939
+ `workspace ssh \xB7 published on ${publishedAddress(posture.sshBind, posture.sshPort)} apart from the web port`
940
+ ],
941
+ ...posture.declared === void 0 || posture.declared.length === 0 ? [] : [`stated verified from outside \xB7 ${posture.declared.join(", ")}`]
931
942
  ];
932
943
  var observedEdgeLine = (host, observed) => [
933
944
  `edge \xB7 ${host}`,
@@ -2710,7 +2721,7 @@ var COMMANDS = [
2710
2721
  summary: "how this instance is exposed, as declared and as observed",
2711
2722
  synopsis: [],
2712
2723
  description: [
2713
- "Operator only. States MEND_EXPOSURE as declared (loopback, private or public) and one line per item of the public exposure gate, with how it was established: observed (this server read it), carried (this build contains it, and the server cannot see it in effect), declared (you stated it and the server cannot check it), or open. MEND_EXPOSURE=public refuses to start while an item the server can observe is open. Items it cannot observe say what would verify them; once you have verified core-private or edge-tls from outside, name it in MEND_EXPOSURE_DECLARED. The report is what was observed; it is not a statement that the instance is fit to expose."
2724
+ "Operator only. States MEND_EXPOSURE as declared (loopback, private or public) and one line per item of the public exposure gate, with how it was established: observed (this server read it), carried (this build contains it, and the server cannot see it in effect), declared (you stated it and the server cannot check it), or open. MEND_EXPOSURE=public refuses to start while an item that blocks a start is open: every item the server can observe, and workspace-ssh until you declare it. Items it cannot observe say what would verify them; once you have verified core-private, edge-tls or workspace-ssh (SSH published apart from the web port) from outside, name it in MEND_EXPOSURE_DECLARED, or with mend server setup --declare. The report is what was observed; it is not a statement that the instance is fit to expose."
2714
2725
  ],
2715
2726
  see: ["operator gate", "doctor"]
2716
2727
  },
@@ -2796,7 +2807,7 @@ var COMMANDS = [
2796
2807
  section: "this machine",
2797
2808
  summary: "install or repair the local Mend server",
2798
2809
  synopsis: [
2799
- "[--context <name>] [--version <version|latest>] [--bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--edge <host> | --no-edge] [--exposure <loopback|private|public>] [--tenancy <single|multi>] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
2810
+ "[--context <name>] [--version <version|latest>] [--bind <ip>] [--ssh-bind <ip>] [--url <origin>] [--origin <origin>...] [--port <n>] [--ssh-port <n>] [--edge <host> | --no-edge] [--exposure <loopback|private|public>] [--tenancy <single|multi>] [--declare <item>...] [--docker-socket <path>] [--assets-dir <dir>] [--offline]"
2800
2811
  ],
2801
2812
  description: [
2802
2813
  "Checks a local Unix-socket Docker context and the Compose plugin, downloads the compose and Postgres initialization assets for one Mend release, preserves existing data and secrets, and starts the server. Re-running repairs the same pinned version. A changed --version is refused; use mend server upgrade. Updating this CLI never updates an existing server pin.",
@@ -2816,6 +2827,10 @@ var COMMANDS = [
2816
2827
  text: "exact Mend server version, or latest; omitted keeps the current pin"
2817
2828
  },
2818
2829
  { flag: "--bind <ip>", text: "published listen address. Default: 127.0.0.1" },
2830
+ {
2831
+ flag: "--ssh-bind <ip>",
2832
+ text: "where workspace SSH is published, when not on --bind; with --edge, Remote-SSH from another machine needs it. Kept across reruns; the --bind address takes it away"
2833
+ },
2819
2834
  { flag: "--url <origin>", text: "advertised browser URL, required with a non-loopback bind" },
2820
2835
  {
2821
2836
  flag: "--origin <origin>",
@@ -2839,6 +2854,10 @@ var COMMANDS = [
2839
2854
  flag: "--tenancy <v>",
2840
2855
  text: "declare single or multi; kept across reruns and upgrades. multi also sets the multi mode gate's variables"
2841
2856
  },
2857
+ {
2858
+ flag: "--declare <item>",
2859
+ text: "state a gate item you verified from outside: core-private, edge-tls or workspace-ssh; repeat for more, none clears. Kept across reruns. public with --ssh-bind beyond loopback needs workspace-ssh"
2860
+ },
2842
2861
  {
2843
2862
  flag: "--assets-dir <dir>",
2844
2863
  text: "copy compose.v2.yaml and postgres-init.sh from a release directory; fresh setup requires --version"
@@ -2859,6 +2878,10 @@ var COMMANDS = [
2859
2878
  command: "mend server setup --version 0.25.0 --assets-dir ./release-assets --offline",
2860
2879
  text: "install from local release assets and preloaded images"
2861
2880
  },
2881
+ {
2882
+ command: "mend server setup --edge mend.example.com --ssh-bind 0.0.0.0 --exposure public --declare workspace-ssh",
2883
+ text: "the TLS edge for the browser and the API, and workspace SSH for Remote-SSH from other machines"
2884
+ },
2862
2885
  {
2863
2886
  command: "mend server setup --edge mend.example.com",
2864
2887
  text: "a TLS edge for mend.example.com; its DNS points at this machine and 80 and 443 reach it"
@@ -4256,7 +4279,7 @@ var gitAuthorLine = (author) => `${author.name} <${author.email}> \xB7 ${author.
4256
4279
 
4257
4280
  // src/http-client.ts
4258
4281
  import { Agent, setGlobalDispatcher } from "undici";
4259
- var cliDispatcher = (connect2) => new Agent({ allowH2: false, ...connect2 === void 0 ? {} : { connect: connect2 } });
4282
+ var cliDispatcher = (connect3) => new Agent({ allowH2: false, ...connect3 === void 0 ? {} : { connect: connect3 } });
4260
4283
  var useHttp1 = () => {
4261
4284
  setGlobalDispatcher(cliDispatcher());
4262
4285
  };
@@ -5241,13 +5264,13 @@ var renderExposure = (report) => {
5241
5264
  const mark = (item) => marks[item.established];
5242
5265
  const open = report.items.filter((item) => item.established === "open");
5243
5266
  const blocking = open.filter((item) => item.blocksStart).length;
5244
- const unobservable = open.length - blocking;
5267
+ const notBlocking = open.length - blocking;
5245
5268
  return [
5246
5269
  `exposure \xB7 declared ${report.declared} \xB7 ${EXPOSURE_WORDS[report.declared]}`,
5247
5270
  ...report.items.map(
5248
5271
  (item) => `${mark(item)} ${item.id.padEnd(width)} ${item.established.padEnd(8)} ${item.detail}${item.fix === null ? "" : dim4(` \xB7 ${item.fix}`)}`
5249
5272
  ),
5250
- open.length === 0 ? "nothing open \xB7 every item was observed here, is carried by this build, or was declared by the operator" : `${open.length} of ${report.items.length} items open \xB7 ${blocking} this build can observe${blocking === 0 ? "" : "; MEND_EXPOSURE=public refuses to start"} \xB7 ${unobservable} it cannot`
5273
+ open.length === 0 ? "nothing open \xB7 every item was observed here, is carried by this build, or was declared by the operator" : `${open.length} of ${report.items.length} items open \xB7 ${blocking} refuse a public start${blocking === 0 ? "" : "; MEND_EXPOSURE=public refuses to start"} \xB7 ${notBlocking} do not`
5251
5274
  ];
5252
5275
  };
5253
5276
  var renderOrganizations = (rows) => {
@@ -6817,6 +6840,7 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
6817
6840
  "--context",
6818
6841
  "--version",
6819
6842
  "--bind",
6843
+ "--ssh-bind",
6820
6844
  "--url",
6821
6845
  "--origin",
6822
6846
  "--port",
@@ -6827,7 +6851,8 @@ var SETUP_FLAGS = /* @__PURE__ */ new Set([
6827
6851
  "--edge",
6828
6852
  "--no-edge",
6829
6853
  "--exposure",
6830
- "--tenancy"
6854
+ "--tenancy",
6855
+ "--declare"
6831
6856
  ]);
6832
6857
  var parseExposure = (value) => {
6833
6858
  if (!isExposure(value))
@@ -6839,6 +6864,21 @@ var parseTenancy = (value) => {
6839
6864
  throw setupError(`--tenancy must be one of ${TENANCIES.join(", ")}, not "${value}".`);
6840
6865
  return value;
6841
6866
  };
6867
+ var parseDeclared = (values) => {
6868
+ if (values.length === 1 && values[0] === "none") return [];
6869
+ return [
6870
+ ...new Set(
6871
+ values.map((value) => {
6872
+ if (!isDeclarableItem(value)) {
6873
+ throw setupError(
6874
+ `--declare takes ${DECLARABLE_ITEMS.join(", ")} or none, not "${value}". Every other gate item is observed by the server, never stated.`
6875
+ );
6876
+ }
6877
+ return value;
6878
+ })
6879
+ )
6880
+ ];
6881
+ };
6842
6882
  var parseEdge = (value) => {
6843
6883
  const host = parseEdgeHost(value);
6844
6884
  if (host === null) {
@@ -6863,7 +6903,7 @@ var parseSetupOptions = (args) => {
6863
6903
  const [value, valueIndex] = nextFlagValue(args, index, flag);
6864
6904
  index = valueIndex;
6865
6905
  const previous = values.get(flag) ?? [];
6866
- if (flag !== "--origin" && previous.length > 0) {
6906
+ if (flag !== "--origin" && flag !== "--declare" && previous.length > 0) {
6867
6907
  throw setupError(`${flag} may be supplied only once.`);
6868
6908
  }
6869
6909
  previous.push(value);
@@ -6877,12 +6917,14 @@ var parseSetupOptions = (args) => {
6877
6917
  const edge = flagValue3("--edge");
6878
6918
  const exposure = flagValue3("--exposure");
6879
6919
  const tenancy = flagValue3("--tenancy");
6920
+ const declared = values.get("--declare");
6880
6921
  if (edge !== void 0 && values.has("--no-edge"))
6881
6922
  throw setupError("--edge and --no-edge contradict each other.");
6882
6923
  return {
6883
6924
  context: flagValue3("--context"),
6884
6925
  version: version === void 0 ? void 0 : parseVersion(version),
6885
6926
  bind: flagValue3("--bind"),
6927
+ sshBind: flagValue3("--ssh-bind"),
6886
6928
  url: flagValue3("--url"),
6887
6929
  origins: origins === void 0 ? void 0 : origins,
6888
6930
  appPort: appPort === void 0 ? void 0 : parsePort(appPort, "--port"),
@@ -6893,7 +6935,8 @@ var parseSetupOptions = (args) => {
6893
6935
  edge: edge === void 0 ? void 0 : parseEdge(edge),
6894
6936
  noEdge: values.has("--no-edge"),
6895
6937
  exposure: exposure === void 0 ? void 0 : parseExposure(exposure),
6896
- tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy)
6938
+ tenancy: tenancy === void 0 ? void 0 : parseTenancy(tenancy),
6939
+ declared: declared === void 0 ? void 0 : parseDeclared(declared)
6897
6940
  };
6898
6941
  };
6899
6942
  var parseUrl = (input, label) => {
@@ -6927,9 +6970,13 @@ var resolveAppUrl = (existing, options, appPort, edgeHost) => {
6927
6970
  }
6928
6971
  return origin;
6929
6972
  }
6930
- const oldLocalUrl = existing === null ? null : `http://localhost:${existing.appPort}`;
6931
- const portChangedOnLocalhost = options.appPort !== void 0 && existing?.appUrl === oldLocalUrl;
6932
- const fallback = portChangedOnLocalhost || existing?.edgeHost !== void 0 ? `http://localhost:${appPort}` : existing?.appUrl ?? `http://localhost:${appPort}`;
6973
+ if (existing?.edgeHost !== void 0) {
6974
+ return parseHttpOrigin(options.url ?? `http://localhost:${appPort}`, "--url");
6975
+ }
6976
+ const saved = existing === null ? null : new URL(existing.appUrl);
6977
+ const savedPointsAtOldPort = saved !== null && existing !== null && options.appPort !== void 0 && saved.protocol === "http:" && saved.port !== "" && Number(saved.port) === existing.appPort;
6978
+ if (saved !== null && savedPointsAtOldPort) saved.port = String(appPort);
6979
+ const fallback = saved === null ? `http://localhost:${appPort}` : saved.origin;
6933
6980
  return parseHttpOrigin(options.url ?? fallback, "--url");
6934
6981
  };
6935
6982
  var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
@@ -6944,7 +6991,7 @@ var checkExposurePair = (bind, appUrl, requireExplicitUrl) => {
6944
6991
  );
6945
6992
  }
6946
6993
  };
6947
- var checkPosture = (bind, ports, edgeHost, exposure) => {
6994
+ var checkPosture = (bind, sshBind, ports, edgeHost, exposure, declared) => {
6948
6995
  if (edgeHost !== void 0 && [ports.appPort, ports.sshPort].some((p) => p === 80 || p === 443)) {
6949
6996
  throw setupError(
6950
6997
  "With an edge, --port and --ssh-port must not be 80 or 443: the edge publishes both on every interface."
@@ -6965,6 +7012,16 @@ var checkPosture = (bind, ports, edgeHost, exposure) => {
6965
7012
  "--exposure loopback contradicts a non-loopback --bind: the port is published beyond this machine."
6966
7013
  );
6967
7014
  }
7015
+ if (exposure === "loopback" && !isLoopbackBind(sshBind)) {
7016
+ throw setupError(
7017
+ "--exposure loopback contradicts a non-loopback --ssh-bind: the SSH port is published beyond this machine."
7018
+ );
7019
+ }
7020
+ if (exposure === "public" && !isLoopbackBind(sshBind) && !declared.includes("workspace-ssh")) {
7021
+ throw setupError(
7022
+ `--exposure public with workspace SSH published on ${publishedAddress(sshBind, ports.sshPort)} beside the edge needs a statement Mend cannot observe: who reaches that port. Check it from each network that should not reach it, then add --declare workspace-ssh; or publish SSH on loopback.`
7023
+ );
7024
+ }
6968
7025
  if (exposure === "public" && edgeHost === void 0) {
6969
7026
  throw setupError(
6970
7027
  "--exposure public needs the edge: add --edge <host>. Without it nothing sets MEND_TRUSTED_PROXIES or an https origin, and the server refuses to start as public."
@@ -6983,10 +7040,18 @@ var validateExposure = (existing, options) => {
6983
7040
  "--bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
6984
7041
  );
6985
7042
  }
7043
+ const requestedSshBind = options.sshBind ?? existing?.sshBind;
7044
+ if (requestedSshBind !== void 0 && net3.isIP(requestedSshBind) === 0) {
7045
+ throw setupError(
7046
+ "--ssh-bind must be a literal IPv4 or IPv6 address, such as 127.0.0.1 or 0.0.0.0."
7047
+ );
7048
+ }
7049
+ const sshBind = requestedSshBind === bind ? void 0 : requestedSshBind;
6986
7050
  const edgeHost = options.noEdge ? void 0 : options.edge ?? existing?.edgeHost;
6987
7051
  const exposure = options.exposure ?? existing?.exposure;
6988
7052
  const tenancy = options.tenancy ?? existing?.tenancy;
6989
- checkPosture(bind, { appPort, sshPort }, edgeHost, exposure);
7053
+ const declared = options.declared ?? existing?.declared ?? [];
7054
+ checkPosture(bind, sshBind ?? bind, { appPort, sshPort }, edgeHost, exposure, declared);
6990
7055
  const appUrl = resolveAppUrl(existing, options, appPort, edgeHost);
6991
7056
  if (edgeHost === void 0)
6992
7057
  checkExposurePair(bind, appUrl, options.url === void 0 && existing === null);
@@ -6997,13 +7062,15 @@ var validateExposure = (existing, options) => {
6997
7062
  ];
6998
7063
  return {
6999
7064
  bind,
7065
+ ...sshBind === void 0 ? {} : { sshBind },
7000
7066
  appUrl,
7001
7067
  allowedOrigins,
7002
7068
  appPort,
7003
7069
  sshPort,
7004
7070
  ...edgeHost === void 0 ? {} : { edgeHost },
7005
7071
  ...exposure === void 0 ? {} : { exposure },
7006
- ...tenancy === void 0 ? {} : { tenancy }
7072
+ ...tenancy === void 0 ? {} : { tenancy },
7073
+ ...declared.length === 0 ? {} : { declared }
7007
7074
  };
7008
7075
  };
7009
7076
  var parseServerConfig = (raw) => {
@@ -7042,6 +7109,13 @@ var parseServerConfig = (raw) => {
7042
7109
  if (tenancy !== void 0 && !isTenancy(tenancy)) {
7043
7110
  throw setupError(`Server config is corrupt: tenancy must be one of ${TENANCIES.join(", ")}.`);
7044
7111
  }
7112
+ const declaredField = fields.get("declared");
7113
+ if (declaredField !== void 0 && (!Array.isArray(declaredField) || !declaredField.every((item) => typeof item === "string" && isDeclarableItem(item)))) {
7114
+ throw setupError(
7115
+ `Server config is corrupt: declared must be an array of ${DECLARABLE_ITEMS.join(", ")}.`
7116
+ );
7117
+ }
7118
+ const declared = Array.isArray(declaredField) ? declaredField.filter((item) => isDeclarableItem(String(item))) : [];
7045
7119
  const config = {
7046
7120
  schemaVersion: requiredInteger(fields, "schemaVersion"),
7047
7121
  assetContract: requiredString(fields, "assetContract"),
@@ -7051,6 +7125,7 @@ var parseServerConfig = (raw) => {
7051
7125
  dockerSocket: parseDockerSocketPath(requiredString(fields, "dockerSocket")),
7052
7126
  dockerSocketSource,
7053
7127
  bind: requiredString(fields, "bind"),
7128
+ ...fields.has("sshBind") ? { sshBind: requiredString(fields, "sshBind") } : {},
7054
7129
  appUrl: parseHttpOrigin(requiredString(fields, "appUrl"), "Server config appUrl"),
7055
7130
  allowedOrigins: origins.map(
7056
7131
  (origin) => parseHttpOrigin(origin, "Server config allowedOrigins")
@@ -7061,7 +7136,8 @@ var parseServerConfig = (raw) => {
7061
7136
  ...fields.has("bucket") ? { bucket: requiredBucket(fields) } : {},
7062
7137
  ...edgeHost === void 0 ? {} : { edgeHost },
7063
7138
  ...exposure === void 0 ? {} : { exposure },
7064
- ...tenancy === void 0 ? {} : { tenancy }
7139
+ ...tenancy === void 0 ? {} : { tenancy },
7140
+ ...declared.length === 0 ? {} : { declared }
7065
7141
  };
7066
7142
  if (config.schemaVersion !== CONFIG_SCHEMA_VERSION || config.assetContract !== ASSET_CONTRACT && !LEGACY_ASSET_CONTRACTS.has(config.assetContract)) {
7067
7143
  throw setupError(
@@ -7076,6 +7152,7 @@ var parseServerConfig = (raw) => {
7076
7152
  context: void 0,
7077
7153
  version: void 0,
7078
7154
  bind: config.bind,
7155
+ sshBind: config.sshBind,
7079
7156
  url: config.appUrl,
7080
7157
  origins: config.allowedOrigins,
7081
7158
  appPort: config.appPort,
@@ -7086,7 +7163,8 @@ var parseServerConfig = (raw) => {
7086
7163
  edge: config.edgeHost,
7087
7164
  noEdge: false,
7088
7165
  exposure: config.exposure,
7089
- tenancy: config.tenancy
7166
+ tenancy: config.tenancy,
7167
+ declared: config.declared
7090
7168
  });
7091
7169
  return config;
7092
7170
  };
@@ -7423,9 +7501,10 @@ var renderGarage = (secrets, config) => {
7423
7501
  ];
7424
7502
  };
7425
7503
  var namespaceOf = (config) => config.bucket === "garage" ? MEND_DOCKER_NAMESPACE_WITH_GARAGE : MEND_DOCKER_NAMESPACE;
7504
+ var composeHost = (address) => net3.isIP(address) === 6 ? `[${address}]` : address;
7426
7505
  var renderSecrets = (secrets, config) => {
7427
7506
  const sshHost = parseUrl(config.appUrl, "Server config appUrl").hostname.replace(/^\[|\]$/g, "");
7428
- const composeBind = net3.isIP(config.bind) === 6 ? `[${config.bind}]` : config.bind;
7507
+ const composeBind = composeHost(config.bind);
7429
7508
  return [
7430
7509
  `MEND_VERSION=${config.serverVersion}`,
7431
7510
  "MEND_IMAGE_REPOSITORY=ghcr.io/sealant-sh/mend",
@@ -7433,6 +7512,7 @@ var renderSecrets = (secrets, config) => {
7433
7512
  `APP_URL=${config.appUrl}`,
7434
7513
  `MEND_ALLOWED_ORIGINS=${JSON.stringify(config.allowedOrigins)}`,
7435
7514
  `MEND_BIND_HOST=${composeBind}`,
7515
+ ...config.sshBind === void 0 ? [] : [`MEND_SSH_BIND_HOST=${composeHost(config.sshBind)}`],
7436
7516
  `MEND_PORT=${config.appPort}`,
7437
7517
  `MEND_SSH_PORT=${config.sshPort}`,
7438
7518
  ...config.registryPort === void 0 ? [] : [`MEND_REGISTRY_PORT=${config.registryPort}`],
@@ -7632,6 +7712,13 @@ var resolveDockerSocket = (runtime, options, existing, selectedContext) => {
7632
7712
  dockerSocketSource: override === void 0 ? "detected" : "override"
7633
7713
  };
7634
7714
  };
7715
+ var checkSshBindAsset = (config, compose) => {
7716
+ if (config.sshBind !== void 0 && !compose.includes("MEND_SSH_BIND_HOST")) {
7717
+ throw setupError(
7718
+ `Mend ${config.serverVersion}'s ${COMPOSE_ASSET} publishes workspace SSH on --bind and cannot honour --ssh-bind ${config.sshBind}. Use a release that supports --ssh-bind, or pass --ssh-bind with the --bind address.`
7719
+ );
7720
+ }
7721
+ };
7635
7722
  var resolveAssets = async (runtime, serverVersion, existing, store, options) => {
7636
7723
  if (options.assetsDir !== void 0) {
7637
7724
  try {
@@ -7865,7 +7952,28 @@ var startCompose = async (runtime, installation) => {
7865
7952
  );
7866
7953
  if (compose.status !== 0) throw commandFailure("Mend containers did not start", compose);
7867
7954
  };
7868
- var edgeStartedLine = (config) => config.edgeHost === void 0 ? null : `The edge for ${config.edgeHost} is up on 80 and 443. Caddy asks for its certificate once ${config.edgeHost} resolves to this machine and both ports reach it from the Internet. mend server status says whether it holds one, and mend server logs shows what Caddy tried.`;
7955
+ var SSH_PROBE_BOUND_MS = 1e4;
7956
+ var sshPublicationLine = async (runtime, config) => {
7957
+ if (config.sshBind === void 0 || isLoopbackBind(config.sshBind)) return null;
7958
+ const published = publishedAddress(config.sshBind, config.sshPort);
7959
+ if (runtime.probeSsh === void 0) return null;
7960
+ const boundMs = runtime.sshProbeBoundMs ?? SSH_PROBE_BOUND_MS;
7961
+ let bound;
7962
+ const probes = await Promise.race([
7963
+ runtime.probeSsh(config.sshBind, config.sshPort),
7964
+ new Promise((resolve8) => {
7965
+ bound = setTimeout(() => resolve8(null), boundMs);
7966
+ })
7967
+ ]).finally(() => clearTimeout(bound));
7968
+ if (probes === null) {
7969
+ return `Workspace SSH is published on ${published}. This machine's look at it did not finish within ${boundMs / 1e3} s. Who reaches it is up to the network and its firewall; mend operator exposure reports it as workspace-ssh.`;
7970
+ }
7971
+ const observed = probes.length === 0 ? "this machine has no address to try it at" : probes.map(
7972
+ (probe) => probe.banner === null ? `${publishedAddress(probe.address, config.sshPort)} did not answer` : `${publishedAddress(probe.address, config.sshPort)} answers (${probe.banner})`
7973
+ ).join(", ");
7974
+ return `Workspace SSH is published on ${published}. From this machine: ${observed}. Who else reaches it is up to the network and its firewall; mend operator exposure reports it as workspace-ssh.`;
7975
+ };
7976
+ var edgeStartedLine = (config) => config.edgeHost === void 0 ? null : `The edge for ${config.edgeHost} is up on 80 and 443. Caddy asks for its certificate once ${config.edgeHost} resolves to this machine and both ports reach it from the Internet. mend server status says whether it holds one, and mend server logs shows what Caddy tried.${config.sshBind === void 0 ? ` Workspace SSH is published on ${config.bind}:${config.sshPort} only, so Remote-SSH and mend ssh from another machine cannot reach it; --ssh-bind 0.0.0.0 publishes it.` : ` Workspace SSH is published on ${config.sshBind}:${config.sshPort}.`}`;
7869
7977
  var setupServer = async (args, runtime, store) => {
7870
7978
  if (runtime.platform !== "linux" && runtime.platform !== "darwin") {
7871
7979
  throw setupError(`mend server setup supports Linux and macOS, not ${runtime.platform}.`);
@@ -7904,6 +8012,7 @@ var setupServer = async (args, runtime, store) => {
7904
8012
  ...validateExposure(existing?.config ?? null, options)
7905
8013
  };
7906
8014
  const assets = await resolveAssets(runtime, serverVersion, existing, store, options);
8015
+ checkSshBindAsset(configWithoutBucket, assets.compose);
7907
8016
  const bucket = composeBucket(assets.compose);
7908
8017
  if (bucket !== void 0 && runtime.dockerDaemonFacts !== void 0) {
7909
8018
  const info = await runtime.run("docker", [
@@ -7954,6 +8063,8 @@ var setupServer = async (args, runtime, store) => {
7954
8063
  runtime.writeLine(reachableLine(config));
7955
8064
  const edgeStarted = edgeStartedLine(config);
7956
8065
  if (edgeStarted !== null) runtime.writeLine(edgeStarted);
8066
+ const sshPublication = await sshPublicationLine(runtime, config);
8067
+ if (sshPublication !== null) runtime.writeLine(sshPublication);
7957
8068
  if (existing?.config.edgeHost !== void 0 && config.edgeHost === void 0) {
7958
8069
  runtime.writeLine(
7959
8070
  `The edge for ${existing.config.edgeHost} is gone. Its certificate volumes stay until you remove them: docker --context ${config.dockerContext} volume rm mend_mend-edge-data mend_mend-edge-config`
@@ -8276,6 +8387,7 @@ var upgradeServer = async (args, runtime, store, existing) => {
8276
8387
  const previous = storeValue(store.readActive());
8277
8388
  if (previous === null) throw setupError("The active server generation is missing.");
8278
8389
  const assets = await resolveAssets(runtime, version, existing, store, options);
8390
+ checkSshBindAsset({ ...existing.config, serverVersion: version }, assets.compose);
8279
8391
  const {
8280
8392
  registryPort: _legacyRegistryPort,
8281
8393
  bucket: _previousBucket,
@@ -8615,9 +8727,45 @@ var nodeServerRuntime = () => {
8615
8727
  writeLine: (line) => process.stdout.write(`${redactCredentials(line)}
8616
8728
  `),
8617
8729
  dockerDaemonFacts: hostDockerDaemonFacts,
8618
- readLogin: (configDir) => savedLogin(configDir, environment)
8730
+ readLogin: (configDir) => savedLogin(configDir, environment),
8731
+ probeSsh: probeSshFromHere
8619
8732
  };
8620
8733
  };
8734
+ var sshBannerAt = async (host, port, timeoutMs) => {
8735
+ const socket = net3.connect({ host, port });
8736
+ let deadline;
8737
+ try {
8738
+ return await Promise.race([
8739
+ new Promise((resolve8) => {
8740
+ socket.once(
8741
+ "data",
8742
+ (bytes) => resolve8(bytes.toString("utf8").split(/\r?\n/)[0]?.trim() ?? "")
8743
+ );
8744
+ socket.once("end", () => resolve8(null));
8745
+ socket.once("close", () => resolve8(null));
8746
+ socket.once("error", () => resolve8(null));
8747
+ }),
8748
+ new Promise((resolve8) => {
8749
+ deadline = setTimeout(() => resolve8(null), timeoutMs);
8750
+ })
8751
+ ]);
8752
+ } finally {
8753
+ clearTimeout(deadline);
8754
+ socket.destroy();
8755
+ }
8756
+ };
8757
+ var probeSshFromHere = async (bind, port) => {
8758
+ const unspecified = bind === "0.0.0.0" || bind === "::";
8759
+ const addresses = unspecified ? Object.values(os10.networkInterfaces()).flatMap((entries) => entries ?? []).filter(
8760
+ (entry) => !entry.internal && (entry.family === "IPv4" || bind === "::" && !entry.address.startsWith("fe80"))
8761
+ ).map((entry) => entry.address).slice(0, 4) : [bind];
8762
+ return Promise.all(
8763
+ addresses.map(async (address) => ({
8764
+ address,
8765
+ banner: await sshBannerAt(address, port, 3e3)
8766
+ }))
8767
+ );
8768
+ };
8621
8769
  var savedLogin = (configDir, environment) => {
8622
8770
  let saved = null;
8623
8771
  try {
@@ -10844,7 +10992,7 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
10844
10992
  process.stdout.write("\x07");
10845
10993
  }
10846
10994
  };
10847
- const connect2 = async () => {
10995
+ const connect3 = async () => {
10848
10996
  let url;
10849
10997
  try {
10850
10998
  url = await socketUrl(
@@ -10913,7 +11061,7 @@ ${dim7(`read-only \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C detaches`)}
10913
11061
  `
10914
11062
  );
10915
11063
  }
10916
- const connected = await connect2();
11064
+ const connected = await connect3();
10917
11065
  stopConnecting();
10918
11066
  if (connected === "unavailable") return "unavailable";
10919
11067
  if (connected === "aborted") return abortReason ?? "interrupted";
@@ -13674,7 +13822,7 @@ var dashboard = async (config, options = {}) => {
13674
13822
  );
13675
13823
  process.exit(rerun.status ?? 0);
13676
13824
  }
13677
- const { runDashboard } = await import("./dashboard-CEJN6IOO.js");
13825
+ const { runDashboard } = await import("./dashboard-DQB5RNXJ.js");
13678
13826
  const agentShare = await startShareIfBridge(config);
13679
13827
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
13680
13828
  listServices: () => fetchServices(config),
@@ -1,4 +1,4 @@
1
- .TH MEND-ACCOUNTS 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-ACCOUNTS 1 "" "mend 0.36.0\-next.665" "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.663" "mend manual"
1
+ .TH MEND-ADOPT 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-adopt \- adopt a repository into the store
4
4
  .SH SYNOPSIS
package/man/mend-attach.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-ATTACH 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-ATTACH 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-attach \- reattach this terminal to a running session
4
4
  .SH SYNOPSIS
package/man/mend-codex.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-CODEX 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-CODEX 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-codex \- launch codex, claude, opencode or pi in a recorded worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-COMPLETIONS 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-COMPLETIONS 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-completions \- print the TAB\-completion hook
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-CONNECT-PI 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-CONNECT-PI 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-connect\-pi \- send your pi setup to every pi session of yours
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-CONNECT 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-CONNECT 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-connect \- send this machine's provider credential to the platform
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-CONTINUE 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-CONTINUE 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-continue \- resume a session with its pending review follow\-up
4
4
  .SH SYNOPSIS
package/man/mend-doctor.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-DOCTOR 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-DOCTOR 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-doctor \- check this machine's setup, or bundle it for a bug report
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-DOTFILES-REPO 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-DOTFILES-REPO 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-dotfiles\-repo \- set or clear the repository the server clones at launch
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-DOTFILES-SYNC 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-DOTFILES-SYNC 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-dotfiles\-sync \- capture config files from this machine into your store
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-DOTFILES 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-DOTFILES 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-dotfiles \- your dotfiles on the server: repo and synced files
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-ENV-CLUSTER 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-ENV-CLUSTER 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-env\-cluster \- bind Kubernetes secrets and configmaps to the workspaces
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-ENV-LOAD 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-ENV-LOAD 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-env\-load \- load a .env into the project
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-ENV-SHOW 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-ENV-SHOW 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-env\-show \- what the project store holds: names only, never values
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-FOLDER-CREATE 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-FOLDER-CREATE 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-folder\-create \- create an organization folder
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-FOLDER-LIST 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-FOLDER-LIST 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-folder\-list \- list your organization's folders
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-FOLDER-PUSH 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-FOLDER-PUSH 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-folder\-push \- upload a local directory into a folder
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH MEND-FOLDER-RM 1 "" "mend 0.36.0\-next.663" "mend manual"
1
+ .TH MEND-FOLDER-RM 1 "" "mend 0.36.0\-next.665" "mend manual"
2
2
  .SH NAME
3
3
  mend\-folder\-rm \- remove an organization folder
4
4
  .SH SYNOPSIS